diff options
Diffstat (limited to 'core/modules')
660 files changed, 7713 insertions, 5167 deletions
diff --git a/core/modules/announcements_feed/config/schema/announcements_feed.schema.yml b/core/modules/announcements_feed/config/schema/announcements_feed.schema.yml index 416ad458020..b338b925822 100644 --- a/core/modules/announcements_feed/config/schema/announcements_feed.schema.yml +++ b/core/modules/announcements_feed/config/schema/announcements_feed.schema.yml @@ -8,17 +8,14 @@ announcements_feed.settings: type: integer label: 'Cache announcements for max-age seconds.' constraints: - Range: - min: 0 + PositiveOrZero: ~ cron_interval: type: integer label: 'Cron interval for fetching announcements in seconds.' constraints: - Range: - min: 0 + PositiveOrZero: ~ limit: type: integer label: 'Number of announcements that will be displayed.' constraints: - Range: - min: 0 + PositiveOrZero: ~ diff --git a/core/modules/announcements_feed/tests/src/FunctionalJavascript/AccessAnnouncementTest.php b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AccessAnnouncementTest.php index 02718fe8e40..4876bf779d2 100644 --- a/core/modules/announcements_feed/tests/src/FunctionalJavascript/AccessAnnouncementTest.php +++ b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AccessAnnouncementTest.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace Drupal\Tests\announcements_feed\FunctionalJavascript; -use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase; use Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware; +use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Test the access announcement permissions to get access announcement icon. - * - * @group announcements_feed */ +#[Group('announcements_feed')] class AccessAnnouncementTest extends OffCanvasTestBase { /** diff --git a/core/modules/announcements_feed/tests/src/FunctionalJavascript/AlertsJsonFeedTest.php b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AlertsJsonFeedTest.php index 39edf53e90c..77a8df8b32b 100644 --- a/core/modules/announcements_feed/tests/src/FunctionalJavascript/AlertsJsonFeedTest.php +++ b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AlertsJsonFeedTest.php @@ -4,15 +4,15 @@ declare(strict_types=1); namespace Drupal\Tests\announcements_feed\FunctionalJavascript; -use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase; use Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware; +use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase; use Drupal\user\UserInterface; +use PHPUnit\Framework\Attributes\Group; /** * Test the access announcement according to json feed changes. - * - * @group announcements_feed */ +#[Group('announcements_feed')] class AlertsJsonFeedTest extends OffCanvasTestBase { /** diff --git a/core/modules/announcements_feed/tests/src/FunctionalJavascript/AnnounceBlockTest.php b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AnnounceBlockTest.php index a796ccf0b8d..dd7f4f74791 100644 --- a/core/modules/announcements_feed/tests/src/FunctionalJavascript/AnnounceBlockTest.php +++ b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AnnounceBlockTest.php @@ -10,12 +10,12 @@ use Drupal\Core\Access\AccessResultAllowed; use Drupal\Core\Access\AccessResultNeutral; use Drupal\Core\Session\AnonymousUserSession; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Test the announcement block test visibility. - * - * @group announcements_feed */ +#[Group('announcements_feed')] class AnnounceBlockTest extends WebDriverTestBase { /** diff --git a/core/modules/announcements_feed/tests/src/Kernel/AnnounceTestBase.php b/core/modules/announcements_feed/tests/src/Kernel/AnnounceTestBase.php index b70ad5d6c27..e017014afe5 100644 --- a/core/modules/announcements_feed/tests/src/Kernel/AnnounceTestBase.php +++ b/core/modules/announcements_feed/tests/src/Kernel/AnnounceTestBase.php @@ -14,7 +14,7 @@ use GuzzleHttp\Psr7\Response; /** * Base class for Announce Kernel tests. */ -class AnnounceTestBase extends KernelTestBase { +abstract class AnnounceTestBase extends KernelTestBase { /** * {@inheritdoc} diff --git a/core/modules/automated_cron/config/schema/automated_cron.schema.yml b/core/modules/automated_cron/config/schema/automated_cron.schema.yml index 810ae0ed8d7..aee2c4df47f 100644 --- a/core/modules/automated_cron/config/schema/automated_cron.schema.yml +++ b/core/modules/automated_cron/config/schema/automated_cron.schema.yml @@ -10,5 +10,4 @@ automated_cron.settings: type: integer label: 'Run cron every' constraints: - Range: - min: 0 + PositiveOrZero: ~ diff --git a/core/modules/big_pipe/big_pipe.module b/core/modules/big_pipe/big_pipe.module deleted file mode 100644 index d04104799cd..00000000000 --- a/core/modules/big_pipe/big_pipe.module +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -/** - * @file - */ - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function big_pipe_theme_suggestions_big_pipe_interface_preview(array $variables): array { - $common_callbacks_simplified_suggestions = [ - 'Drupal_block_BlockViewBuilder__lazyBuilder' => 'block', - ]; - - $suggestions = []; - $suggestion = 'big_pipe_interface_preview'; - if ($variables['callback']) { - $callback = preg_replace('/[^a-zA-Z0-9]/', '_', $variables['callback']); - if (is_array($callback)) { - $callback = implode('__', $callback); - } - - // Use simplified template suggestion, if any. - // For example, this simplifies - // phpcs:ignore Drupal.Files.LineLength - // big-pipe-interface-preview--Drupal-block-BlockViewBuilder--lazyBuilder--<BLOCK ID>.html.twig - // to - // big-pipe-interface-preview--block--<BLOCK ID>.html.twig - if (isset($common_callbacks_simplified_suggestions[$callback])) { - $callback = $common_callbacks_simplified_suggestions[$callback]; - } - - $suggestions[] = $suggestion .= '__' . $callback; - if (is_array($variables['arguments'])) { - $arguments = preg_replace('/[^a-zA-Z0-9]/', '_', $variables['arguments']); - foreach ($arguments as $argument) { - if (empty($argument)) { - continue; - } - $suggestions[] = $suggestion . '__' . $argument; - } - } - } - return $suggestions; -} diff --git a/core/modules/big_pipe/src/Hook/BigPipeThemeHooks.php b/core/modules/big_pipe/src/Hook/BigPipeThemeHooks.php new file mode 100644 index 00000000000..5bced86d68d --- /dev/null +++ b/core/modules/big_pipe/src/Hook/BigPipeThemeHooks.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\big_pipe\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for big_pipe. + */ +class BigPipeThemeHooks { + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_big_pipe_interface_preview')] + public function themeSuggestionsBigPipeInterfacePreview(array $variables): array { + $common_callbacks_simplified_suggestions = [ + 'Drupal_block_BlockViewBuilder__lazyBuilder' => 'block', + ]; + $suggestions = []; + $suggestion = 'big_pipe_interface_preview'; + if ($variables['callback']) { + $callback = preg_replace('/[^a-zA-Z0-9]/', '_', $variables['callback']); + if (is_array($callback)) { + $callback = implode('__', $callback); + } + // Use simplified template suggestion, if any. + // For example, this simplifies + // phpcs:ignore Drupal.Files.LineLength + // big-pipe-interface-preview--Drupal-block-BlockViewBuilder--lazyBuilder--<BLOCK ID>.html.twig + // to + // big-pipe-interface-preview--block--<BLOCK ID>.html.twig. + if (isset($common_callbacks_simplified_suggestions[$callback])) { + $callback = $common_callbacks_simplified_suggestions[$callback]; + } + $suggestions[] = $suggestion .= '__' . $callback; + if (is_array($variables['arguments'])) { + $arguments = preg_replace('/[^a-zA-Z0-9]/', '_', $variables['arguments']); + foreach ($arguments as $argument) { + if (empty($argument)) { + continue; + } + $suggestions[] = $suggestion . '__' . $argument; + } + } + } + return $suggestions; + } + +} diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php index 48471501c40..b36cd0a0929 100644 --- a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php +++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php @@ -366,7 +366,7 @@ class BigPipePlaceholderTestCases { ]; $exception->embeddedHtmlResponse = NULL; - // cSpell:disable-next-line. + // cSpell:disable-next-line $token = 'PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU'; // 8. Edge case: response filter throwing an exception for this placeholder. $embedded_response_exception = new BigPipePlaceholderTestCase( diff --git a/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipePreviewTest.php b/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipePreviewTest.php index 3579ca7b2f4..ad064369206 100644 --- a/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipePreviewTest.php +++ b/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipePreviewTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\big_pipe\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests placeholder preview functionality. - * - * @group big_pipe */ +#[Group('big_pipe')] class BigPipePreviewTest extends WebDriverTestBase { /** diff --git a/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipeRegressionTest.php b/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipeRegressionTest.php index 8e4abff5734..e94279d9404 100644 --- a/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipeRegressionTest.php +++ b/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipeRegressionTest.php @@ -8,13 +8,13 @@ use Drupal\big_pipe\Render\BigPipe; use Drupal\big_pipe_regression_test\BigPipeRegressionTestController; use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * BigPipe regression tests. - * - * @group big_pipe - * @group #slow */ +#[Group('big_pipe')] +#[Group('#slow')] class BigPipeRegressionTest extends WebDriverTestBase { /** diff --git a/core/modules/big_pipe/tests/src/Kernel/BigPipeInterfacePreviewThemeSuggestionsTest.php b/core/modules/big_pipe/tests/src/Kernel/BigPipeInterfacePreviewThemeSuggestionsTest.php index 73ccd3144f1..c2e4ab9782b 100644 --- a/core/modules/big_pipe/tests/src/Kernel/BigPipeInterfacePreviewThemeSuggestionsTest.php +++ b/core/modules/big_pipe/tests/src/Kernel/BigPipeInterfacePreviewThemeSuggestionsTest.php @@ -8,7 +8,7 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\block\Entity\Block; /** - * Tests the big_pipe_theme_suggestions_big_pipe_interface_preview() function. + * Tests the big pipe theme suggestions. * * @group big_pipe */ @@ -58,7 +58,7 @@ class BigPipeInterfacePreviewThemeSuggestionsTest extends KernelTestBase { } /** - * Tests template suggestions from big_pipe_theme_suggestions_big_pipe_interface_preview(). + * Tests theme suggestions from big_pipe. */ public function testBigPipeThemeHookSuggestions(): void { $entity = $this->controller->create([ @@ -77,10 +77,11 @@ class BigPipeInterfacePreviewThemeSuggestionsTest extends KernelTestBase { $variables = []; // In turn this is what createBigPipeJsPlaceholder() uses to // build the BigPipe JS placeholder render array which is used as input - // for big_pipe_theme_suggestions_big_pipe_interface_preview(). + // for big_pipe theme suggestions(). $variables['callback'] = $build['#lazy_builder'][0]; $variables['arguments'] = $build['#lazy_builder'][1]; - $suggestions = big_pipe_theme_suggestions_big_pipe_interface_preview($variables); + $module_handler = $this->container->get('module_handler'); + $suggestions = $module_handler->invoke('big_pipe', 'theme_suggestions_big_pipe_interface_preview', [$variables]); $suggested_id = preg_replace('/[^a-zA-Z0-9]/', '_', $block->id()); $this->assertSame([ 'big_pipe_interface_preview__block', diff --git a/core/modules/block/block.module b/core/modules/block/block.module index 24e28589491..b5e9f56a465 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -5,36 +5,6 @@ */ use Drupal\block\Hook\BlockHooks; -use Drupal\Core\Installer\InstallerKernel; - -/** - * Initializes blocks for installed themes. - * - * @param string[] $theme_list - * An array of theme names. - * - * @see block_modules_installed() - */ -function block_themes_installed($theme_list): void { - // Do not create blocks during config sync. - if (\Drupal::service('config.installer')->isSyncing()) { - return; - } - // Disable this functionality prior to install profile installation because - // block configuration is often optional or provided by the install profile - // itself. block_theme_initialize() will be called when the install profile is - // installed. - if (InstallerKernel::installationAttempted() && \Drupal::config('core.extension')->get('module.' . \Drupal::installProfile()) === NULL) { - return; - } - - foreach ($theme_list as $theme) { - // Don't initialize themes that are not displayed in the UI. - if (\Drupal::service('theme_handler')->hasUi($theme)) { - block_theme_initialize($theme); - } - } -} /** * Assigns an initial, default set of blocks for a theme. @@ -75,37 +45,6 @@ function block_theme_initialize($theme): void { } /** - * Implements hook_theme_suggestions_HOOK(). - */ -function block_theme_suggestions_block(array $variables): array { - $suggestions = []; - - $suggestions[] = 'block__' . $variables['elements']['#configuration']['provider']; - // Hyphens (-) and underscores (_) play a special role in theme suggestions. - // Theme suggestions should only contain underscores, because within - // drupal_find_theme_templates(), underscores are converted to hyphens to - // match template file names, and then converted back to underscores to match - // pre-processing and other function names. So if your theme suggestion - // contains a hyphen, it will end up as an underscore after this conversion, - // and your function names won't be recognized. So, we need to convert - // hyphens to underscores in block deltas for the theme suggestions. - - // We can safely explode on : because we know the Block plugin type manager - // enforces that delimiter for all derivatives. - $parts = explode(':', $variables['elements']['#plugin_id']); - $suggestion = 'block'; - while ($part = array_shift($parts)) { - $suggestions[] = $suggestion .= '__' . strtr($part, '-', '_'); - } - - if (!empty($variables['elements']['#id'])) { - $suggestions[] = 'block__' . $variables['elements']['#id']; - } - - return $suggestions; -} - -/** * Prepares variables for block templates. * * Default template: block.html.twig. diff --git a/core/modules/block/block.services.yml b/core/modules/block/block.services.yml index b73f9642231..97623ff523a 100644 --- a/core/modules/block/block.services.yml +++ b/core/modules/block/block.services.yml @@ -11,3 +11,4 @@ services: class: Drupal\block\BlockRepository arguments: ['@entity_type.manager', '@theme.manager', '@context.handler'] Drupal\block\BlockRepositoryInterface: '@block.repository' + Drupal\block\BlockConfigUpdater: ~ diff --git a/core/modules/block/src/BlockConfigUpdater.php b/core/modules/block/src/BlockConfigUpdater.php new file mode 100644 index 00000000000..317d19ddc64 --- /dev/null +++ b/core/modules/block/src/BlockConfigUpdater.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\block; + +/** + * Provides a BC layer for modules providing old configurations. + * + * @internal + */ +class BlockConfigUpdater { + + /** + * Flag determining whether deprecations should be triggered. + * + * @var bool + */ + protected bool $deprecationsEnabled = TRUE; + + /** + * Stores which deprecations were triggered. + * + * @var array + */ + protected array $triggeredDeprecations = []; + + /** + * Sets the deprecations enabling status. + * + * @param bool $enabled + * Whether deprecations should be enabled. + */ + public function setDeprecationsEnabled(bool $enabled): void { + $this->deprecationsEnabled = $enabled; + } + + /** + * Performs the required update. + * + * @param \Drupal\block\BlockInterface $block + * The block to update. + * + * @return bool + * Whether the block was updated. + */ + public function updateBlock(BlockInterface $block): bool { + $changed = FALSE; + if ($this->needsInfoStatusSettingsRemoved($block)) { + $settings = $block->get('settings'); + unset($settings['info'], $settings['status']); + $block->set('settings', $settings); + $changed = TRUE; + } + return $changed; + } + + /** + * Checks if the block contains deprecated info and status settings. + * + * @param \Drupal\block\BlockInterface $block + * The block to update. + * + * @return bool + * TRUE if the block has deprecated settings. + */ + public function needsInfoStatusSettingsRemoved(BlockInterface $block): bool { + if (!str_starts_with($block->getPluginId(), 'block_content')) { + return FALSE; + } + $settings = $block->get('settings'); + if (!isset($settings['info']) && !isset($settings['status'])) { + return FALSE; + } + + $deprecations_triggered = &$this->triggeredDeprecations['3426302'][$block->id()]; + if ($this->deprecationsEnabled && !$deprecations_triggered) { + $deprecations_triggered = TRUE; + @trigger_error('Block content blocks with the "status" and "info" settings is deprecated in drupal:11.3.0 and will be removed in drupal:12.0.0. They were unused, so there is no replacement. Profile, module and theme provided configuration should be updated. See https://www.drupal.org/node/3499836', E_USER_DEPRECATED); + } + + return TRUE; + } + +} diff --git a/core/modules/block/src/Hook/BlockHooks.php b/core/modules/block/src/Hook/BlockHooks.php index 802a60bccb1..3dfa51bfebb 100644 --- a/core/modules/block/src/Hook/BlockHooks.php +++ b/core/modules/block/src/Hook/BlockHooks.php @@ -12,6 +12,7 @@ use Drupal\Core\Link; use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Installer\InstallerKernel; /** * Hook implementations for block. @@ -148,7 +149,7 @@ class BlockHooks { /** * Implements hook_modules_installed(). * - * @see block_themes_installed() + * @see BlockHooks::themesInstalled() */ #[Hook('modules_installed')] public function modulesInstalled($modules, bool $is_syncing): void { @@ -170,6 +171,38 @@ class BlockHooks { } /** + * Implements hook_themes_installed(). + * + * Initializes blocks for installed themes. + * + * @param string[] $theme_list + * An array of theme names. + * + * @see BlockHooks::modulesInstalled() + */ + #[Hook('themes_installed')] + public function themesInstalled($theme_list): void { + // Do not create blocks during config sync. + if (\Drupal::service('config.installer')->isSyncing()) { + return; + } + // Disable this functionality prior to install profile installation because + // block configuration is often optional or provided by the install profile + // itself. block_theme_initialize() will be called when the install profile + // is installed. + if (InstallerKernel::installationAttempted() && \Drupal::config('core.extension')->get('module.' . \Drupal::installProfile()) === NULL) { + return; + } + + foreach ($theme_list as $theme) { + // Don't initialize themes that are not displayed in the UI. + if (\Drupal::service('theme_handler')->hasUi($theme)) { + block_theme_initialize($theme); + } + } + } + + /** * Implements hook_rebuild(). */ #[Hook('rebuild')] diff --git a/core/modules/block/src/Hook/BlockThemeHooks.php b/core/modules/block/src/Hook/BlockThemeHooks.php new file mode 100644 index 00000000000..8b5d34fff83 --- /dev/null +++ b/core/modules/block/src/Hook/BlockThemeHooks.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\block\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for block. + */ +class BlockThemeHooks { + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_block')] + public function themeSuggestionsBlock(array $variables): array { + $suggestions = []; + $suggestions[] = 'block__' . $variables['elements']['#configuration']['provider']; + // Hyphens (-) and underscores (_) play a special role in theme + // suggestions. Theme suggestions should only contain underscores, because + // within drupal_find_theme_templates(), underscores are converted to + // hyphens to match template file names, and then converted back to + // underscores to match pre-processing and other function names. So if your + // theme suggestion contains a hyphen, it will end up as an underscore + // after this conversion, and your function names won't be recognized. So, + // we need to convert hyphens to underscores in block deltas for the theme + // suggestions. We can safely explode on : because we know the Block plugin + // type manager enforces that delimiter for all derivatives. + $parts = explode(':', $variables['elements']['#plugin_id']); + $suggestion = 'block'; + while ($part = array_shift($parts)) { + $suggestions[] = $suggestion .= '__' . strtr($part, '-', '_'); + } + if (!empty($variables['elements']['#id'])) { + $suggestions[] = 'block__' . $variables['elements']['#id']; + } + return $suggestions; + } + +} diff --git a/core/modules/block/tests/src/FunctionalJavascript/BlockAddTest.php b/core/modules/block/tests/src/FunctionalJavascript/BlockAddTest.php index 9be17d58a17..1f92a4fdcc1 100644 --- a/core/modules/block/tests/src/FunctionalJavascript/BlockAddTest.php +++ b/core/modules/block/tests/src/FunctionalJavascript/BlockAddTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\block\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JS functionality in the block add form. - * - * @group block */ +#[Group('block')] class BlockAddTest extends WebDriverTestBase { /** @@ -52,7 +52,7 @@ class BlockAddTest extends WebDriverTestBase { $assert_session->elementTextContains('css', '.vertical-tabs__menu-item-title', 'Response status'); $assert_session->elementTextNotContains('css', '.vertical-tabs__menu-item-title', $summary_text); - // Search for the "Pages" tab link and click it + // Search for the "Pages" tab link and click it. $this->getSession()->getPage()->find('css', 'a[href="#edit-visibility-request-path"]')->click(); // Check that the corresponding form section is open and visible. $form_section = $this->getSession()->getPage()->find('css', '#edit-visibility-request-path'); diff --git a/core/modules/block/tests/src/FunctionalJavascript/BlockContextualLinksTest.php b/core/modules/block/tests/src/FunctionalJavascript/BlockContextualLinksTest.php index c425731e844..038542b4455 100644 --- a/core/modules/block/tests/src/FunctionalJavascript/BlockContextualLinksTest.php +++ b/core/modules/block/tests/src/FunctionalJavascript/BlockContextualLinksTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\block\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the contextual links added while rendering the block. - * - * @group block */ +#[Group('block')] class BlockContextualLinksTest extends WebDriverTestBase { /** diff --git a/core/modules/block/tests/src/FunctionalJavascript/BlockDragTest.php b/core/modules/block/tests/src/FunctionalJavascript/BlockDragTest.php index 4de5edebdd2..6fcf9d54f1e 100644 --- a/core/modules/block/tests/src/FunctionalJavascript/BlockDragTest.php +++ b/core/modules/block/tests/src/FunctionalJavascript/BlockDragTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\block\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests drag and drop blocks on block layout page. - * - * @group block */ +#[Group('block')] class BlockDragTest extends WebDriverTestBase { /** diff --git a/core/modules/block/tests/src/FunctionalJavascript/BlockFilterTest.php b/core/modules/block/tests/src/FunctionalJavascript/BlockFilterTest.php index 3502e65a195..a96930f375d 100644 --- a/core/modules/block/tests/src/FunctionalJavascript/BlockFilterTest.php +++ b/core/modules/block/tests/src/FunctionalJavascript/BlockFilterTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\block\FunctionalJavascript; use Behat\Mink\Element\NodeElement; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript functionality of the block add filter. - * - * @group block */ +#[Group('block')] class BlockFilterTest extends WebDriverTestBase { /** diff --git a/core/modules/block/tests/src/Kernel/BlockTemplateSuggestionsTest.php b/core/modules/block/tests/src/Kernel/BlockTemplateSuggestionsTest.php index 049b9f74ccc..7fd894a85bb 100644 --- a/core/modules/block/tests/src/Kernel/BlockTemplateSuggestionsTest.php +++ b/core/modules/block/tests/src/Kernel/BlockTemplateSuggestionsTest.php @@ -8,7 +8,7 @@ use Drupal\block\Entity\Block; use Drupal\KernelTests\KernelTestBase; /** - * Tests the block_theme_suggestions_block() function. + * Tests the block theme suggestions. * * @group block */ @@ -23,7 +23,7 @@ class BlockTemplateSuggestionsTest extends KernelTestBase { ]; /** - * Tests template suggestions from block_theme_suggestions_block(). + * Tests template suggestions from the block module. */ public function testBlockThemeHookSuggestions(): void { $this->installConfig(['system']); @@ -44,7 +44,8 @@ class BlockTemplateSuggestionsTest extends KernelTestBase { $variables['elements']['#base_plugin_id'] = $plugin->getBaseId(); $variables['elements']['#derivative_plugin_id'] = $plugin->getDerivativeId(); $variables['elements']['content'] = []; - $suggestions = block_theme_suggestions_block($variables); + $module_handler = $this->container->get('module_handler'); + $suggestions = $module_handler->invoke('block', 'theme_suggestions_block', [$variables]); $this->assertSame([ 'block__system', 'block__system_menu_block', diff --git a/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php b/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php index 377cd31deac..a6b66adee2b 100644 --- a/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php +++ b/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php @@ -348,17 +348,17 @@ class BlockViewBuilderTest extends KernelTestBase { $required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; - // Check that the expected cacheability metadata is present in: - // - the built render array; + // Check that the expected cacheability metadata is present in the built + // render array. $build = $this->getBlockRenderArray(); $this->assertSame($expected_keys, $build['#cache']['keys']); $this->assertEqualsCanonicalizing($expected_contexts, $build['#cache']['contexts']); $this->assertEqualsCanonicalizing($expected_tags, $build['#cache']['tags']); $this->assertSame($expected_max_age, $build['#cache']['max-age']); $this->assertFalse(isset($build['#create_placeholder'])); - // - the rendered render array; + // And also in the rendered render array. $this->renderer->renderRoot($build); - // - the render cache item. + // And also in the render cache item. $final_cache_contexts = Cache::mergeContexts($expected_contexts, $required_cache_contexts); $cache_item = $cache_bin->get($expected_keys, CacheableMetadata::createFromRenderArray($build)); $this->assertNotEmpty($cache_item, 'The block render element has been cached with the expected cache keys.'); diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php index dc96d95e699..cb94233ebaf 100644 --- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php +++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Drupal\Tests\block\Kernel\Migrate\d6; use Drupal\block\Entity\Block; -use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase; use Drupal\block\Hook\BlockHooks; +use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase; /** * Tests migration of blocks to configuration entities. @@ -265,8 +265,6 @@ class MigrateBlockTest extends MigrateDrupal6TestBase { 'label' => 'Static Block', 'provider' => 'block_content', 'label_display' => 'visible', - 'status' => TRUE, - 'info' => '', 'view_mode' => 'full', ]; $this->assertEntity('block', $visibility, 'content', 'olivero', 0, $settings); @@ -283,8 +281,6 @@ class MigrateBlockTest extends MigrateDrupal6TestBase { 'label' => 'Another Static Block', 'provider' => 'block_content', 'label_display' => 'visible', - 'status' => TRUE, - 'info' => '', 'view_mode' => 'full', ]; // We expect this block to be disabled because '' is not a valid region, @@ -296,8 +292,6 @@ class MigrateBlockTest extends MigrateDrupal6TestBase { 'label' => '', 'provider' => 'block_content', 'label_display' => '0', - 'status' => TRUE, - 'info' => '', 'view_mode' => 'full', ]; $this->assertEntity('block_2', [], 'right', 'test_theme', -7, $settings); diff --git a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php index 871af6072fd..e36eaebfbda 100644 --- a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php +++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php @@ -72,7 +72,7 @@ class BlockPageVariantTest extends UnitTestCase { public static function providerBuild() { $blocks_config = [ 'block1' => [ - // region, is main content block, is messages block, is title block + // region, is main content block, is messages block, is title block. 'top', FALSE, FALSE, FALSE, ], // Test multiple blocks in the same region. diff --git a/core/modules/block_content/block_content.post_update.php b/core/modules/block_content/block_content.post_update.php index 592b3ee9714..53021a078f6 100644 --- a/core/modules/block_content/block_content.post_update.php +++ b/core/modules/block_content/block_content.post_update.php @@ -5,6 +5,10 @@ * Post update functions for Content Block. */ +use Drupal\block\BlockConfigUpdater; +use Drupal\block\BlockInterface; +use Drupal\Core\Config\Entity\ConfigEntityUpdater; + /** * Implements hook_removed_post_updates(). */ @@ -18,3 +22,16 @@ function block_content_removed_post_updates(): array { 'block_content_post_update_revision_type' => '11.0.0', ]; } + +/** + * Remove deprecated status and info keys from block_content blocks. + */ +function block_content_post_update_remove_block_content_status_info_keys(array &$sandbox = []): void { + /** @var \Drupal\block\BlockConfigUpdater $blockConfigUpdater */ + $blockConfigUpdater = \Drupal::service(BlockConfigUpdater::class); + $blockConfigUpdater->setDeprecationsEnabled(FALSE); + \Drupal::classResolver(ConfigEntityUpdater::class) + ->update($sandbox, 'block', function (BlockInterface $block) use ($blockConfigUpdater): bool { + return $blockConfigUpdater->needsInfoStatusSettingsRemoved($block); + }); +} diff --git a/core/modules/block_content/config/schema/block_content.schema.yml b/core/modules/block_content/config/schema/block_content.schema.yml index 8c4161df5b5..6d7b2aeadc8 100644 --- a/core/modules/block_content/config/schema/block_content.schema.yml +++ b/core/modules/block_content/config/schema/block_content.schema.yml @@ -32,12 +32,12 @@ block.settings.block_content:*: FullyValidatable: ~ mapping: # @see \Drupal\block_content\Plugin\Block\BlockContentBlock::defaultConfiguration() - # @todo Deprecate this in https://www.drupal.org/project/drupal/issues/3426302 status: + deprecated: "The 'status' setting for content blocks is deprecated in drupal:11.3.0 and is removed from drupal 12.0.0. It was unused, so there is no replacement. See https://www.drupal.org/node/3499836." type: boolean label: 'Status' - # @todo Deprecate this in https://www.drupal.org/project/drupal/issues/3426302 info: + deprecated: "The 'info' setting for content blocks is deprecated in drupal:11.3.0 and is removed from drupal 12.0.0. It was unused, so there is no replacement. See https://www.drupal.org/node/3499836." type: label label: 'Admin info' view_mode: diff --git a/core/modules/block_content/src/BlockContentTypeInterface.php b/core/modules/block_content/src/BlockContentTypeInterface.php index aaf7957b895..9c12709b2a5 100644 --- a/core/modules/block_content/src/BlockContentTypeInterface.php +++ b/core/modules/block_content/src/BlockContentTypeInterface.php @@ -3,19 +3,12 @@ namespace Drupal\block_content; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Entity\EntityDescriptionInterface; use Drupal\Core\Entity\RevisionableEntityBundleInterface; /** * Provides an interface defining a block type entity. */ -interface BlockContentTypeInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface { - - /** - * Returns the description of the block type. - * - * @return string - * The description of the type of this block. - */ - public function getDescription(); +interface BlockContentTypeInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface, EntityDescriptionInterface { } diff --git a/core/modules/block_content/src/Entity/BlockContentType.php b/core/modules/block_content/src/Entity/BlockContentType.php index ecbc6c3866d..fa6fe395503 100644 --- a/core/modules/block_content/src/Entity/BlockContentType.php +++ b/core/modules/block_content/src/Entity/BlockContentType.php @@ -100,6 +100,13 @@ class BlockContentType extends ConfigEntityBundleBase implements BlockContentTyp /** * {@inheritdoc} */ + public function setDescription($description): static { + return $this->set('description', $description); + } + + /** + * {@inheritdoc} + */ public function shouldCreateNewRevision() { return $this->revision; } diff --git a/core/modules/block_content/src/Hook/BlockContentHooks.php b/core/modules/block_content/src/Hook/BlockContentHooks.php index 4eef9bb8580..d960c0a22d6 100644 --- a/core/modules/block_content/src/Hook/BlockContentHooks.php +++ b/core/modules/block_content/src/Hook/BlockContentHooks.php @@ -2,6 +2,7 @@ namespace Drupal\block_content\Hook; +use Drupal\block\BlockConfigUpdater; use Drupal\block\BlockInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\block_content\BlockContentInterface; @@ -160,4 +161,15 @@ class BlockContentHooks { return $operations; } + /** + * Implements hook_ENTITY_TYPE_presave(). + */ + #[Hook('block_presave')] + public function blockPreSave(BlockInterface $block): void { + // Use an inline service since DI would require enabling the block module + // in any Kernel test that installs block_content. This is BC code so will + // be removed in Drupal 12 anyway. + \Drupal::service(BlockConfigUpdater::class)->updateBlock($block); + } + } diff --git a/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php b/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php index c42fb2f5de5..131a56b516f 100644 --- a/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php +++ b/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php @@ -132,8 +132,6 @@ class BlockContentBlock extends BlockBase implements ContainerFactoryPluginInter */ public function defaultConfiguration() { return [ - 'status' => TRUE, - 'info' => '', 'view_mode' => 'full', ]; } diff --git a/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml b/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml index 998c43340eb..48147809560 100644 --- a/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml +++ b/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml @@ -16,8 +16,6 @@ settings: label: 'Foobar Gorilla' label_display: visible provider: block_content - status: true - info: '' view_mode: full visibility: request_path: diff --git a/core/modules/block_content/tests/modules/block_content_theme_suggestions_test/block_content_theme_suggestions_test.module b/core/modules/block_content/tests/modules/block_content_theme_suggestions_test/block_content_theme_suggestions_test.module deleted file mode 100644 index 8df5abdcd55..00000000000 --- a/core/modules/block_content/tests/modules/block_content_theme_suggestions_test/block_content_theme_suggestions_test.module +++ /dev/null @@ -1,20 +0,0 @@ -<?php - -/** - * @file - * Support module for testing. - */ - -declare(strict_types=1); - -use Drupal\block_content\BlockContentInterface; - -/** - * Implements hook_preprocess_block(). - */ -function block_content_theme_suggestions_test_preprocess_block(&$variables): void { - $block_content = $variables['elements']['content']['#block_content'] ?? NULL; - if ($block_content instanceof BlockContentInterface) { - $variables['label'] = $block_content->label(); - } -} diff --git a/core/modules/block_content/tests/modules/block_content_theme_suggestions_test/src/Hook/BlockContentThemeSuggestionsTestThemeHooks.php b/core/modules/block_content/tests/modules/block_content_theme_suggestions_test/src/Hook/BlockContentThemeSuggestionsTestThemeHooks.php new file mode 100644 index 00000000000..ec999172c83 --- /dev/null +++ b/core/modules/block_content/tests/modules/block_content_theme_suggestions_test/src/Hook/BlockContentThemeSuggestionsTestThemeHooks.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\block_content_theme_suggestions_test\Hook; + +use Drupal\block_content\BlockContentInterface; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for block_content_theme_suggestions_test. + */ +class BlockContentThemeSuggestionsTestThemeHooks { + + /** + * Implements hook_preprocess_block(). + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + $block_content = $variables['elements']['content']['#block_content'] ?? NULL; + if ($block_content instanceof BlockContentInterface) { + $variables['label'] = $block_content->label(); + } + } + +} diff --git a/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php b/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php index 364b5f4524d..e0f77406bb9 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentCreationTest.php @@ -106,7 +106,7 @@ class BlockContentCreationTest extends BlockContentTestBase { $this->drupalGet('block/add/basic'); $this->submitForm($edit, 'Save and configure'); - // Save our block permanently + // Save our block permanently. $this->submitForm(['region' => 'content'], 'Save block'); // Set test_view_mode as a custom display to be available on the list. diff --git a/core/modules/block_content/tests/src/Functional/BlockContentTestBase.php b/core/modules/block_content/tests/src/Functional/BlockContentTestBase.php index d0a3794b978..ffee97e4679 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentTestBase.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentTestBase.php @@ -4,8 +4,7 @@ declare(strict_types=1); namespace Drupal\Tests\block_content\Functional; -use Drupal\block_content\Entity\BlockContent; -use Drupal\block_content\Entity\BlockContentType; +use Drupal\Tests\block_content\Traits\BlockContentCreationTrait; use Drupal\Tests\BrowserTestBase; /** @@ -13,6 +12,8 @@ use Drupal\Tests\BrowserTestBase; */ abstract class BlockContentTestBase extends BrowserTestBase { + use BlockContentCreationTrait; + /** * Profile to use. * @@ -64,80 +65,4 @@ abstract class BlockContentTestBase extends BrowserTestBase { $this->drupalPlaceBlock('local_actions_block'); } - /** - * Creates a content block. - * - * @param bool|string $title - * (optional) Title of block. When no value is given uses a random name. - * Defaults to FALSE. - * @param string $bundle - * (optional) Bundle name. Defaults to 'basic'. - * @param bool $save - * (optional) Whether to save the block. Defaults to TRUE. - * - * @return \Drupal\block_content\Entity\BlockContent - * Created content block. - */ - protected function createBlockContent($title = FALSE, $bundle = 'basic', $save = TRUE) { - $title = $title ?: $this->randomMachineName(); - $block_content = BlockContent::create([ - 'info' => $title, - 'type' => $bundle, - 'langcode' => 'en', - ]); - if ($block_content && $save === TRUE) { - $block_content->save(); - } - return $block_content; - } - - /** - * Creates a block type (bundle). - * - * @param array|string $values - * (deprecated) The variable $values as string is deprecated. Provide as an - * array as parameter. The value to create the block content type. If - * $values is an array it should be like: ['id' => 'foo', 'label' => 'Foo']. - * If $values is a string, it will be considered that it represents the - * label. - * @param bool $create_body - * Whether or not to create the body field. - * - * @return \Drupal\block_content\Entity\BlockContentType - * Created block type. - */ - protected function createBlockContentType($values, $create_body = FALSE) { - if (is_string($values)) { - @trigger_error('Using the variable $values as string is deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Provide an array as parameter. See https://www.drupal.org/node/3473739', E_USER_DEPRECATED); - } - if (is_array($values)) { - if (!isset($values['id'])) { - do { - $id = $this->randomMachineName(8); - } while (BlockContentType::load($id)); - } - else { - $id = $values['id']; - } - $values += [ - 'id' => $id, - 'label' => $id, - 'revision' => FALSE, - ]; - $bundle = BlockContentType::create($values); - } - else { - $bundle = BlockContentType::create([ - 'id' => $values, - 'label' => $values, - 'revision' => FALSE, - ]); - } - $bundle->save(); - if ($create_body) { - block_content_add_body_field($bundle->id()); - } - return $bundle; - } - } diff --git a/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php b/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php index fba8362cd2c..2ec82c2195b 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php @@ -58,14 +58,26 @@ class BlockContentTypeTest extends BlockContentTestBase { } /** - * Tests the order of the block content types on the add page. + * Tests the block types on the block/add page. */ - public function testBlockContentAddPageOrder(): void { - $this->createBlockContentType(['id' => 'bundle_1', 'label' => 'Bundle 1']); - $this->createBlockContentType(['id' => 'bundle_2', 'label' => 'Aaa Bundle 2']); + public function testBlockContentAddPage(): void { + $this->createBlockContentType([ + 'id' => 'bundle_1', + 'label' => 'Bundle 1', + 'description' => 'Bundle 1 description', + ]); + $this->createBlockContentType([ + 'id' => 'bundle_2', + 'label' => 'Aaa Bundle 2', + 'description' => 'Bundle 2 description', + ]); $this->drupalLogin($this->adminUser); $this->drupalGet('block/add'); + // Ensure bundles are ordered by their label, not id. $this->assertSession()->pageTextMatches('/Aaa Bundle 2(.*)Bundle 1/'); + // Block type descriptions should display. + $this->assertSession()->pageTextContains('Bundle 1 description'); + $this->assertSession()->pageTextContains('Bundle 2 description'); } /** diff --git a/core/modules/block_content/tests/src/Functional/Update/BlockContentStatusInfoUpdatePathTest.php b/core/modules/block_content/tests/src/Functional/Update/BlockContentStatusInfoUpdatePathTest.php new file mode 100644 index 00000000000..fb4bff9e63b --- /dev/null +++ b/core/modules/block_content/tests/src/Functional/Update/BlockContentStatusInfoUpdatePathTest.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\block_content\Functional\Update; + +use Drupal\block\Entity\Block; +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +// cspell:ignore anotherblock + +/** + * Tests block_content_post_update_remove_block_content_status_info_keys. + * + * @group block_content + */ +class BlockContentStatusInfoUpdatePathTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles(): void { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz', + ]; + } + + /** + * Tests block_content_post_update_remove_block_content_status_info_keys. + */ + public function testRunUpdates(): void { + $this->assertArrayHasKey('info', Block::load('anotherblock')->get('settings')); + $this->assertArrayHasKey('status', Block::load('anotherblock')->get('settings')); + + $this->runUpdates(); + + $this->assertArrayNotHasKey('info', Block::load('anotherblock')->get('settings')); + $this->assertArrayNotHasKey('status', Block::load('anotherblock')->get('settings')); + } + +} diff --git a/core/modules/block_content/tests/src/Functional/Views/BlockContentTestBase.php b/core/modules/block_content/tests/src/Functional/Views/BlockContentTestBase.php index 1b8af1195ee..04c54c637d1 100644 --- a/core/modules/block_content/tests/src/Functional/Views/BlockContentTestBase.php +++ b/core/modules/block_content/tests/src/Functional/Views/BlockContentTestBase.php @@ -4,8 +4,7 @@ declare(strict_types=1); namespace Drupal\Tests\block_content\Functional\Views; -use Drupal\block_content\Entity\BlockContent; -use Drupal\block_content\Entity\BlockContentType; +use Drupal\Tests\block_content\Traits\BlockContentCreationTrait; use Drupal\Tests\views\Functional\ViewTestBase; /** @@ -13,6 +12,11 @@ use Drupal\Tests\views\Functional\ViewTestBase; */ abstract class BlockContentTestBase extends ViewTestBase { + use BlockContentCreationTrait { + createBlockContent as baseCreateBlockContent; + createBlockContentType as baseCreateBlockContentType; + } + /** * Admin user. * @@ -67,7 +71,7 @@ abstract class BlockContentTestBase extends ViewTestBase { 'type' => 'basic', 'langcode' => 'en', ]; - if ($block_content = BlockContent::create($values)) { + if ($block_content = $this->baseCreateBlockContent(save: FALSE, values: $values)) { $status = $block_content->save(); } $this->assertEquals(SAVED_NEW, $status, "Created block content {$block_content->label()}."); @@ -84,26 +88,7 @@ abstract class BlockContentTestBase extends ViewTestBase { * Created block type. */ protected function createBlockContentType(array $values = []) { - // Find a non-existent random type name. - if (!isset($values['id'])) { - do { - $id = $this->randomMachineName(8); - } while (BlockContentType::load($id)); - } - else { - $id = $values['id']; - } - $values += [ - 'id' => $id, - 'label' => $id, - 'revision' => FALSE, - ]; - $bundle = BlockContentType::create($values); - $status = $bundle->save(); - block_content_add_body_field($bundle->id()); - - $this->assertEquals(SAVED_NEW, $status, sprintf('Created block content type %s.', $bundle->id())); - return $bundle; + return $this->baseCreateBlockContentType($values, TRUE); } } diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentTest.php index bb0186431e9..fe07b3d5652 100644 --- a/core/modules/block_content/tests/src/Kernel/BlockContentTest.php +++ b/core/modules/block_content/tests/src/Kernel/BlockContentTest.php @@ -38,6 +38,19 @@ class BlockContentTest extends KernelTestBase { } /** + * Tests BlockContentType functionality. + */ + public function testBlockContentType(): void { + $type = BlockContentType::create([ + 'id' => 'foo', + 'label' => 'Foo', + ]); + $this->assertSame('', $type->getDescription()); + $type->setDescription('Test description'); + $this->assertSame('Test description', $type->getDescription()); + } + + /** * Tests the editing links for BlockContentBlock. */ public function testOperationLinks(): void { diff --git a/core/modules/block_content/tests/src/Traits/BlockContentCreationTrait.php b/core/modules/block_content/tests/src/Traits/BlockContentCreationTrait.php new file mode 100644 index 00000000000..0322a3ebf17 --- /dev/null +++ b/core/modules/block_content/tests/src/Traits/BlockContentCreationTrait.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\block_content\Traits; + +use Drupal\block_content\Entity\BlockContent; +use Drupal\block_content\Entity\BlockContentType; + +/** + * Provides methods for creating block_content entities and types. + */ +trait BlockContentCreationTrait { + + /** + * Creates a content block. + * + * @param bool|string $title + * (optional) Title of block. When no value is given uses a random name. + * Defaults to FALSE. + * @param string $bundle + * (optional) Bundle name. Defaults to 'basic'. + * @param bool $save + * (optional) Whether to save the block. Defaults to TRUE. + * @param array $values + * (optional) Additional values for the block_content entity. + * + * @return \Drupal\block_content\Entity\BlockContent + * Created content block. + */ + protected function createBlockContent(bool|string $title = FALSE, string $bundle = 'basic', bool $save = TRUE, array $values = []): BlockContent { + $title = $title ?: $this->randomMachineName(); + $values += [ + 'info' => $title, + 'type' => $bundle, + 'langcode' => 'en', + ]; + $block_content = BlockContent::create($values); + if ($block_content && $save === TRUE) { + $block_content->save(); + } + return $block_content; + } + + /** + * Creates a block type (bundle). + * + * @param array|string $values + * (deprecated) The variable $values as string is deprecated. Provide as an + * array as parameter. The value to create the block content type. If + * $values is an array it should be like: ['id' => 'foo', 'label' => 'Foo']. + * If $values is a string, it will be considered that it represents the + * label. + * @param bool $create_body + * Whether or not to create the body field. + * + * @return \Drupal\block_content\Entity\BlockContentType + * Created block type. + */ + protected function createBlockContentType(array|string $values, bool $create_body = FALSE): BlockContentType { + if (is_string($values)) { + @trigger_error('Using the variable $values as string is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Provide an array as parameter. See https://www.drupal.org/node/3473739', E_USER_DEPRECATED); + } + if (is_array($values)) { + if (!isset($values['id'])) { + do { + $id = $this->randomMachineName(8); + } while (BlockContentType::load($id)); + } + else { + $id = $values['id']; + } + $values += [ + 'id' => $id, + 'label' => $id, + 'revision' => FALSE, + ]; + $bundle = BlockContentType::create($values); + } + else { + $bundle = BlockContentType::create([ + 'id' => $values, + 'label' => $values, + 'revision' => FALSE, + ]); + } + $bundle->save(); + if ($create_body) { + block_content_add_body_field($bundle->id()); + } + return $bundle; + } + +} diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml index 7624fbfd708..d7ad4e12e48 100644 --- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml +++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml @@ -394,11 +394,8 @@ ckeditor5_list: plugins: - list.List - list.ListProperties - config: - list: - properties: - # @todo Make this configurable in https://www.drupal.org/project/drupal/issues/3274635 - styles: false + # @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ListPlugin::getDynamicPluginConfig() + config: {} drupal: label: List library: core/ckeditor5.list @@ -414,6 +411,8 @@ ckeditor5_list: - <ol> - <ol reversed start> - <li> + - <ol type> + - <ul type> ckeditor5_horizontalLine: ckeditor5: diff --git a/core/modules/ckeditor5/ckeditor5.post_update.php b/core/modules/ckeditor5/ckeditor5.post_update.php index db795a147d0..5b60cf18a3e 100644 --- a/core/modules/ckeditor5/ckeditor5.post_update.php +++ b/core/modules/ckeditor5/ckeditor5.post_update.php @@ -6,6 +6,8 @@ */ // cspell:ignore multiblock +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\editor\Entity\Editor; /** * Implements hook_removed_post_updates(). @@ -20,3 +22,21 @@ function ckeditor5_removed_post_updates(): array { 'ckeditor5_post_update_list_start_reversed' => '11.0.0', ]; } + +/** + * Updates Text Editors using CKEditor 5 to native List "type" functionality. + */ +function ckeditor5_post_update_list_type(array &$sandbox = []): void { + $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class); + $config_entity_updater->update($sandbox, 'editor', function (Editor $editor): bool { + // Only try to update editors using CKEditor 5. + if ($editor->getEditor() !== 'ckeditor5') { + return FALSE; + } + $settings = $editor->getSettings(); + + // @see Ckeditor5Hooks::editorPresave() + return array_key_exists('ckeditor5_list', $settings['plugins']) + && array_key_exists('ckeditor5_sourceEditing', $settings['plugins']); + }); +} diff --git a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml index 68344494841..3ec380d4fe6 100644 --- a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml +++ b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml @@ -142,14 +142,22 @@ ckeditor5.plugin.ckeditor5_list: mapping: reversed: type: boolean + # @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol#reversed label: 'Allow reverse list' constraints: NotNull: [] startIndex: type: boolean + # @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol#start label: 'Allow start index' constraints: NotNull: [] + styles: + type: boolean + # @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol#type + label: 'Allow list style type' + constraints: + NotNull: [] multiBlock: type: boolean label: 'Allow blocks to be created in list items' diff --git a/core/modules/ckeditor5/js/ckeditor5.js b/core/modules/ckeditor5/js/ckeditor5.js index f5af26de838..de97e49a77d 100644 --- a/core/modules/ckeditor5/js/ckeditor5.js +++ b/core/modules/ckeditor5/js/ckeditor5.js @@ -324,7 +324,6 @@ const addedCss = [ `${prefix} .ck.ck-content * {display:revert;background:revert;color:initial;padding:revert;}`, `${prefix} .ck.ck-content li {display:list-item}`, - `${prefix} .ck.ck-content ol li {list-style-type: decimal}`, ]; const prefixedCss = [...addedCss].join('\n'); @@ -625,6 +624,30 @@ }, }; + Drupal.behaviors.editorStyleFix = { + attach(context) { + // CKEditor's DLL injects a style tag that overrides native list + // type styling. The following find the style(s) causing the problem + // and removes them. + // @todo remove this entire behavior when this issue is fixed + // https://github.com/ckeditor/ckeditor5/issues/14613 + [...document.styleSheets] + .filter((sheet) => sheet.ownerNode.hasAttribute('data-cke')) + .forEach((sheet) => { + [...sheet.cssRules].forEach((rule, ruleIndex) => { + if ( + rule?.selectorText && + (rule.selectorText.includes(' ol') || + rule.selectorText.includes(' ul')) && + !rule.selectorText.includes('type') + ) { + sheet.cssRules[ruleIndex].style['list-style-type'] = null; + } + }); + }); + }, + }; + // Redirect on hash change when the original hash has an associated CKEditor 5. function redirectTextareaFragmentToCKEditor5Instance() { const hash = window.location.hash.substring(1); diff --git a/core/modules/ckeditor5/src/HTMLRestrictions.php b/core/modules/ckeditor5/src/HTMLRestrictions.php index e10e81ef46d..1d3c15298d3 100644 --- a/core/modules/ckeditor5/src/HTMLRestrictions.php +++ b/core/modules/ckeditor5/src/HTMLRestrictions.php @@ -492,7 +492,7 @@ final class HTMLRestrictions { } // When allowing all tags on an attribute, transform FilterHtml output from - // ['tag' => ['*'=> TRUE]] to ['tag' => TRUE] + // "['tag' => ['*'=> TRUE]]" to "['tag' => TRUE]". $allowed = $restrictions['allowed']; foreach ($allowed as $element => $attributes) { if (is_array($attributes) && isset($attributes['*']) && $attributes['*'] === TRUE) { @@ -580,7 +580,7 @@ final class HTMLRestrictions { } // When allowing all tags on an attribute, transform FilterHtml output from - // ['tag' => ['*'=> TRUE]] to ['tag' => TRUE] + // "['tag' => ['*'=> TRUE]]" to "['tag' => TRUE]". foreach ($allowed_elements as $element => $attributes) { if (is_array($attributes) && isset($attributes['*']) && $attributes['*'] === TRUE) { $allowed_elements[$element] = TRUE; diff --git a/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php b/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php index 7eb7601e3d8..adae3e8ebdd 100644 --- a/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php +++ b/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php @@ -6,12 +6,14 @@ use Drupal\Core\Hook\Order\OrderAfter; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Render\Element; +use Drupal\ckeditor5\HTMLRestrictions; use Drupal\ckeditor5\Plugin\Editor\CKEditor5; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\editor\EditorInterface; /** * Hook implementations for ckeditor5. @@ -377,4 +379,41 @@ class Ckeditor5Hooks { $definitions['ckeditor5_valid_pair__format_and_editor']['mapping']['image_upload'] = $definitions['editor.editor.*']['mapping']['image_upload']; } + /** + * Implements hook_ENTITY_TYPE_presave() for editor entities. + */ + #[Hook('editor_presave')] + public function editorPresave(EditorInterface $editor): void { + if ($editor->getEditor() === 'ckeditor5') { + $settings = $editor->getSettings(); + // @see ckeditor5_post_update_list_type() + if (array_key_exists('ckeditor5_list', $settings['plugins']) && array_key_exists('ckeditor5_sourceEditing', $settings['plugins'])) { + $source_edited = HTMLRestrictions::fromString(implode(' ', $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'])); + $format_restrictions = HTMLRestrictions::fromTextFormat($editor->getFilterFormat()); + + // If neither <ol type> or <ul type> are allowed through Source Editing + // (the only way it could possibly be supported until now), and it is + // not an unrestricted text format (such as "Full HTML"), then set the + // new "styles" setting for the List plugin to false. + $ol_type = HTMLRestrictions::fromString('<ol type>'); + $ul_type = HTMLRestrictions::fromString('<ul type>'); + if (!array_key_exists('styles', $settings['plugins']['ckeditor5_list']['properties'])) { + $settings['plugins']['ckeditor5_list']['properties']['styles'] = + $ol_type->diff($source_edited)->allowsNothing() || + $ul_type->diff($source_edited)->allowsNothing() || + $format_restrictions->isUnrestricted(); + } + + // Update the Source Editing configuration too. + $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = $source_edited + ->diff($ol_type) + ->diff($ul_type) + ->toCKEditor5ElementsArray(); + } + + $editor->setSettings($settings); + + } + } + } diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ListPlugin.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ListPlugin.php index 7be9c85684d..523d55e76cf 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ListPlugin.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ListPlugin.php @@ -26,7 +26,11 @@ class ListPlugin extends CKEditor5PluginDefault implements CKEditor5PluginConfig */ public function defaultConfiguration() { return [ - 'properties' => ['reversed' => TRUE, 'startIndex' => TRUE], + 'properties' => [ + 'reversed' => TRUE, + 'startIndex' => TRUE, + 'styles' => TRUE, + ], 'multiBlock' => TRUE, ]; } @@ -50,6 +54,12 @@ class ListPlugin extends CKEditor5PluginDefault implements CKEditor5PluginConfig '#title' => $this->t('Allow the user to create paragraphs in list items (or other block elements)'), '#default_value' => $this->configuration['multiBlock'], ]; + $form['styles'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Allow the user to choose a list style type'), + '#description' => $this->t('Available list style types for ordered lists: letters and Roman numerals instead of only numbers. Available list style types for unordered lists: circles and squares instead of only discs.'), + '#default_value' => $this->configuration['properties']['styles'], + ]; return $form; } @@ -62,6 +72,8 @@ class ListPlugin extends CKEditor5PluginDefault implements CKEditor5PluginConfig $form_state->setValue('reversed', (bool) $form_value); $form_value = $form_state->getValue('startIndex'); $form_state->setValue('startIndex', (bool) $form_value); + $form_value = $form_state->getValue('styles'); + $form_state->setValue('styles', (bool) $form_value); $form_value = $form_state->getValue('multiBlock'); $form_state->setValue('multiBlock', (bool) $form_value); } @@ -72,6 +84,7 @@ class ListPlugin extends CKEditor5PluginDefault implements CKEditor5PluginConfig public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { $this->configuration['properties']['reversed'] = $form_state->getValue('reversed'); $this->configuration['properties']['startIndex'] = $form_state->getValue('startIndex'); + $this->configuration['properties']['styles'] = $form_state->getValue('styles'); $this->configuration['multiBlock'] = $form_state->getValue('multiBlock'); } @@ -79,7 +92,13 @@ class ListPlugin extends CKEditor5PluginDefault implements CKEditor5PluginConfig * {@inheritdoc} */ public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array { - $static_plugin_config['list']['properties'] = $this->getConfiguration()['properties'] + $static_plugin_config['list']['properties']; + $static_plugin_config['list']['properties'] = $this->getConfiguration()['properties']; + // Generate configuration to use `type` attribute-based list styles on <ul> + // and <ol> elements. + // @see https://ckeditor.com/docs/ckeditor5/latest/api/module_list_listconfig-ListPropertiesStyleConfig.html#member-useAttribute + if ($this->getConfiguration()['properties']['styles']) { + $static_plugin_config['list']['properties']['styles'] = ['useAttribute' => TRUE]; + } $static_plugin_config['list']['multiBlock'] = $this->getConfiguration()['multiBlock']; return $static_plugin_config; } @@ -89,6 +108,12 @@ class ListPlugin extends CKEditor5PluginDefault implements CKEditor5PluginConfig */ public function getElementsSubset(): array { $subset = $this->getPluginDefinition()->getElements(); + if (!$this->getConfiguration()['properties']['styles']) { + $subset = array_diff($subset, [ + '<ul type>', + '<ol type>', + ]); + } $subset = array_diff($subset, ['<ol reversed start>']); $reversed_enabled = $this->getConfiguration()['properties']['reversed']; $start_index_enabled = $this->getConfiguration()['properties']['startIndex']; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/AdminUiTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/AdminUiTest.php index d380114dfe7..402aaf17158 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/AdminUiTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/AdminUiTest.php @@ -4,14 +4,15 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; -// cspell:ignore sourceediting xmlhttprequest +use PHPUnit\Framework\Attributes\Group; +// cspell:ignore sourceediting xmlhttprequest /** * Tests for CKEditor 5 in the admin UI. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class AdminUiTest extends CKEditor5TestBase { /** diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php index ca9cb17ca62..63dfe37555c 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php @@ -7,16 +7,16 @@ namespace Drupal\Tests\ckeditor5\FunctionalJavascript; use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Yaml\Yaml; // cspell:ignore esque imageUpload sourceediting Editing's - /** * Tests for CKEditor 5. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5AllowedTagsTest extends CKEditor5TestBase { /** @@ -52,7 +52,7 @@ class CKEditor5AllowedTagsTest extends CKEditor5TestBase { * * @var string */ - protected $defaultElementsAfterUpdatingToCkeditor5 = '<br> <p> <h2 id="jump-*"> <h3 id> <h4 id> <h5 id> <h6 id> <cite> <dl> <dt> <dd> <a hreflang href> <blockquote cite> <ul type> <ol type="1 A I" reversed start> <strong> <em> <code> <li>'; + protected $defaultElementsAfterUpdatingToCkeditor5 = '<br> <p> <h2 id="jump-*"> <h3 id> <h4 id> <h5 id> <h6 id> <cite> <dl> <dt> <dd> <a hreflang href> <blockquote cite> <strong> <em> <code> <ul type> <ol type reversed start> <li>'; /** * Test enabling CKEditor 5 in a way that triggers validation. diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5CodeSyntaxTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5CodeSyntaxTest.php index a0d16cfb2c3..ad9feb4d19b 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5CodeSyntaxTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5CodeSyntaxTest.php @@ -7,13 +7,14 @@ namespace Drupal\Tests\ckeditor5\FunctionalJavascript; use Behat\Mink\Element\NodeElement; use Drupal\editor\Entity\Editor; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests code block configured languages are respected. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5CodeSyntaxTest extends CKEditor5TestBase { use CKEditor5TestTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5DialogTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5DialogTest.php index 9ed2be2d85b..599a6b68b29 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5DialogTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5DialogTest.php @@ -9,14 +9,15 @@ use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; /** * Tests for CKEditor 5 to ensure correct focus management in dialogs. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5DialogTest extends CKEditor5TestBase { use CKEditor5TestTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5FragmentLinkTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5FragmentLinkTest.php index fc36e1799bd..c04c3349186 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5FragmentLinkTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5FragmentLinkTest.php @@ -14,13 +14,14 @@ use Drupal\node\Entity\NodeType; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\Entity\User; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; /** * Tests that the fragment link points to CKEditor 5. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5FragmentLinkTest extends WebDriverTestBase { use TestFileCreationTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5HeightTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5HeightTest.php index 81928b1642b..12e54868a8a 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5HeightTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5HeightTest.php @@ -6,13 +6,14 @@ namespace Drupal\Tests\ckeditor5\FunctionalJavascript; use Drupal\editor\Entity\Editor; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests ckeditor height respects field rows config. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5HeightTest extends CKEditor5TestBase { use CKEditor5TestTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5MarkupTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5MarkupTest.php index 3557f2df244..c673f1e43f4 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5MarkupTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5MarkupTest.php @@ -12,16 +12,16 @@ use Drupal\node\Entity\Node; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; // cspell:ignore esque māori sourceediting splitbutton upcasted - /** * Tests for CKEditor 5. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5MarkupTest extends CKEditor5TestBase { use TestFileCreationTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5OffCanvasTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5OffCanvasTest.php index f52ebb874c5..fd65022c5c2 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5OffCanvasTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5OffCanvasTest.php @@ -4,12 +4,14 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; +use PHPUnit\Framework\Attributes\Group; + /** * Tests for CKEditor 5 to ensure correct styling in off-canvas. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5OffCanvasTest extends CKEditor5TestBase { /** diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5ReadOnlyModeTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5ReadOnlyModeTest.php index c52ae83c911..fef1e9db8c2 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5ReadOnlyModeTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5ReadOnlyModeTest.php @@ -6,13 +6,14 @@ namespace Drupal\Tests\ckeditor5\FunctionalJavascript; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use PHPUnit\Framework\Attributes\Group; /** * Tests read-only mode for CKEditor 5. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5ReadOnlyModeTest extends CKEditor5TestBase { /** diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php index 10a32513fb2..a4102bda453 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php @@ -14,16 +14,16 @@ use Drupal\node\Entity\Node; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; // cspell:ignore esque māori sourceediting splitbutton upcasted - /** * Tests for CKEditor 5. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5Test extends CKEditor5TestBase { use TestFileCreationTrait; @@ -566,13 +566,14 @@ JS; ], 'settings' => [ 'toolbar' => [ - 'items' => ['sourceEditing', 'numberedList'], + 'items' => ['sourceEditing', 'numberedList', 'bulletedList'], ], 'plugins' => [ 'ckeditor5_list' => [ 'properties' => [ 'reversed' => FALSE, 'startIndex' => FALSE, + 'styles' => FALSE, ], 'multiBlock' => TRUE, ], @@ -605,7 +606,7 @@ JS; $numbered_list_dropdown_selector = '.ck-splitbutton__arrow'; // Check that there is no dropdown available for the numbered list because - // both reversed and startIndex are FALSE. + // reversed, startIndex and styles are FALSE. $assert_session->elementNotExists('css', $numbered_list_dropdown_selector); // Save content so source content is kept after changing the editor config. $page->pressButton('Save'); @@ -640,6 +641,70 @@ JS; $assert_session->elementExists('css', $reversed_order_button_selector); $assert_session->elementTextEquals('css', $reversed_order_button_selector, 'Reversed order'); $assert_session->elementExists('css', $start_index_element_selector); + + // Enable list style types. + $editor = Editor::load('test_format'); + $settings = $editor->getSettings(); + $settings['plugins']['ckeditor5_list']['properties']['styles'] = TRUE; + $editor->setSettings($settings); + $editor->save(); + $this->getSession()->reload(); + $this->waitForEditor(); + + $list_types = [ + 'ol' => [ + 'Lower-roman' => 'i', + 'Upper-roman' => 'I', + 'Lower-latin' => 'a', + 'Upper-latin' => 'A', + ], + 'ul' => [ + 'Square' => 'square', + 'Disc' => 'disc', + 'Circle' => 'circle', + ], + ]; + + foreach ($list_types as $list_tag => $types) { + foreach ($types as $type => $type_attribute) { + $list_to_edit = $assert_session->waitForElementVisible('css', ".ck-editor__editable_inline > *:first-child"); + $list_to_edit->click(); + + // Open the list type toolbar and choose a type. + $list_button_tip_text = $list_tag === 'ol' ? 'Numbered List' : 'Bulleted List'; + $toolbar_selector = '[aria-label="' . str_replace(' L', ' l', $list_button_tip_text) . ' styles toolbar"]'; + $button_selector = '[data-cke-tooltip-text="' . $list_button_tip_text . '"]'; + $page->find('css', '[aria-expanded="false"]' . $button_selector)->click(); + $open_splitbutton = $assert_session->waitForElementVisible('css', '[aria-expanded="true"]' . $button_selector); + $this->assertNotNull($open_splitbutton, "$list_button_tip_text splitbutton is open"); + $toolbar = $assert_session->waitForElementVisible('css', $toolbar_selector); + $this->assertNotNull($toolbar, "Toolbar for selecting $type is available at $toolbar_selector "); + $toolbar_with_tips = $assert_session->waitForElementVisible('css', $toolbar_selector . ' [data-cke-tooltip-text]'); + $this->assertNotNull($toolbar_with_tips); + $toolbar_buttons = $toolbar->findAll('css', 'button'); + // While this is a bit of an indirect way to find the correct button, it + // accounts for the mixed dash characters and worked better than other + // attempts. + $toolbar_button_tips = array_map(fn($item) => str_replace('–', '-', $item->getAttribute('data-cke-tooltip-text')), $toolbar_buttons); + $this->assertNotFalse(array_search($type, $toolbar_button_tips)); + $toolbar_buttons[array_search($type, $toolbar_button_tips)]->click(); + $widget_selector = '.ck-editor__editable_inline > ' . $list_tag . '[type="' . $type_attribute . '"]'; + $widget = $assert_session->waitForElementVisible('css', $widget_selector); + $this->assertNotNull($widget, "The widget exists at $widget_selector"); + + // Confirm the style applied in-editor is for the type of list chosen. + $list_style_type = $this->getSession()->evaluateScript('window.getComputedStyle(document.querySelector(\'' . $widget_selector . '\')).listStyleType'); + $this->assertSame(str_replace('latin', 'alpha', strtolower($type)), $list_style_type, "The $list_style_type list should have the correct style."); + $page->pressButton('Save'); + + $fe_list_style_type = $this->getSession()->evaluateScript('window.getComputedStyle(document.querySelector(\'' . $list_tag . '[type]\')).listStyleType'); + // Confirm the style applied on the default theme is for the type of + // list chosen. + $this->assertSame(str_replace('latin', 'alpha', strtolower($type)), strtolower($fe_list_style_type)); + $this->drupalGet($edit_url); + $this->waitForEditor(); + } + } } /** diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5ToolbarTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5ToolbarTest.php index 3cea9188509..5d6905ff2a0 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5ToolbarTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5ToolbarTest.php @@ -9,14 +9,15 @@ use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\user\Entity\User; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; /** * Tests for CKEditor 5 editor UI with Toolbar module. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class CKEditor5ToolbarTest extends WebDriverTestBase { /** diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/EmphasisTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/EmphasisTest.php index e5c772e93e9..d8dda21e28e 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/EmphasisTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/EmphasisTest.php @@ -9,6 +9,7 @@ use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; /** @@ -17,9 +18,9 @@ use Symfony\Component\Validator\ConstraintViolationInterface; * CKEditor's use of <i> is converted to <em> in Drupal, so additional coverage * is provided here to verify successful conversion. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class EmphasisTest extends WebDriverTestBase { use CKEditor5TestTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php index 79c37f0c6e2..ea1b61b8416 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php @@ -4,14 +4,19 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; -// cspell:ignore imageresize imageupload +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +// cspell:ignore imageresize imageupload /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image - * @group ckeditor5 - * @group #slow + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image. + * * @internal */ +#[CoversClass(Image::class)] +#[Group('ckeditor5')] +#[Group('#slow')] class ImageTest extends ImageTestTestBase { use ImageTestBaselineTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestBase.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestBase.php index 8efebd4de81..cf9c14b2963 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestBase.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestBase.php @@ -4,16 +4,20 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; -use Drupal\Tests\TestFileCreationTrait; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; +use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore imageresize - /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image - * @group ckeditor5 + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image. + * * @internal */ +#[CoversClass(Image::class)] +#[Group('ckeditor5')] abstract class ImageTestBase extends CKEditor5TestBase { use CKEditor5TestTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestProviderTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestProviderTest.php index 82a5c503b76..932e0ad1a75 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestProviderTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestProviderTest.php @@ -4,12 +4,18 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; + /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image - * @group ckeditor5 - * @group #slow + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image. + * * @internal */ +#[CoversClass(Image::class)] +#[Group('ckeditor5')] +#[Group('#slow')] class ImageTestProviderTest extends ImageTestTestBase { use ImageTestProviderTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestProviderTrait.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestProviderTrait.php index 2588cce74b0..fc16afe6cdb 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestProviderTrait.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestProviderTrait.php @@ -6,6 +6,7 @@ namespace Drupal\Tests\ckeditor5\FunctionalJavascript; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; +use PHPUnit\Framework\Attributes\DataProvider; // cspell:ignore imageresize @@ -18,9 +19,8 @@ trait ImageTestProviderTrait { * Tests that alt text is required for images. * * @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#conversion - * - * @dataProvider providerAltTextRequired */ + #[DataProvider('providerAltTextRequired')] public function testAltTextRequired(bool $unrestricted): void { // Disable filter_html. if ($unrestricted) { @@ -122,9 +122,8 @@ trait ImageTestProviderTrait { /** * Tests alignment integration. - * - * @dataProvider providerAlignment */ + #[DataProvider('providerAlignment')] public function testAlignment(string $image_type): void { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); @@ -198,9 +197,8 @@ trait ImageTestProviderTrait { * * @param string $width * The width input for the image. - * - * @dataProvider providerWidth */ + #[DataProvider('providerWidth')] public function testWidth(string $width): void { $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); @@ -264,9 +262,8 @@ trait ImageTestProviderTrait { * * @param bool $is_resize_enabled * Boolean flag to test enabled or disabled. - * - * @dataProvider providerResize */ + #[DataProvider('providerResize')] public function testResize(bool $is_resize_enabled): void { // Disable resize plugin because it is enabled by default. if (!$is_resize_enabled) { diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestTestBase.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestTestBase.php index 070ae0e90ca..a6230938d12 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestTestBase.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestTestBase.php @@ -13,12 +13,9 @@ use Symfony\Component\Validator\ConstraintViolationInterface; // cspell:ignore imageresize imageupload /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image - * @group ckeditor5 - * @group #slow * @internal */ -class ImageTestTestBase extends ImageTestBase { +abstract class ImageTestTestBase extends ImageTestBase { /** * The sample image File entity to embed. diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlProviderTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlProviderTest.php index 9ddcf4eec3e..6ce56094bb7 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlProviderTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlProviderTest.php @@ -4,12 +4,18 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; + /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image - * @group ckeditor5 - * @group #slow + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image. + * * @internal */ +#[CoversClass(Image::class)] +#[Group('ckeditor5')] +#[Group('#slow')] class ImageUrlProviderTest extends ImageUrlTestBase { use ImageTestProviderTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlTest.php index d2ed0f991e8..5812b9c05bb 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlTest.php @@ -4,12 +4,18 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; + /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image - * @group ckeditor5 - * @group #slow + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image. + * * @internal */ +#[CoversClass(Image::class)] +#[Group('ckeditor5')] +#[Group('#slow')] class ImageUrlTest extends ImageUrlTestBase { use ImageTestBaselineTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlTestBase.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlTestBase.php index 5d0d6c50c82..1638a44cd28 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlTestBase.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageUrlTestBase.php @@ -12,12 +12,9 @@ use Symfony\Component\Validator\ConstraintViolationInterface; // cspell:ignore imageresize /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image - * @group ckeditor5 - * @group #slow * @internal */ -class ImageUrlTestBase extends ImageTestBase { +abstract class ImageUrlTestBase extends ImageTestBase { /** * {@inheritdoc} diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/JSTranslationTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/JSTranslationTest.php index fe74ff25997..09debb23b16 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/JSTranslationTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/JSTranslationTest.php @@ -6,15 +6,15 @@ namespace Drupal\Tests\ckeditor5\FunctionalJavascript; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\media\Traits\MediaTypeCreationTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore drupalmediatoolbar - /** * Tests for CKEditor 5 plugins using Drupal's translation system. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class JSTranslationTest extends CKEditor5TestBase { use MediaTypeCreationTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/LanguageTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/LanguageTest.php index 47ed0b6569b..f57ab070f89 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/LanguageTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/LanguageTest.php @@ -5,15 +5,16 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; use Drupal\language\Entity\ConfigurableLanguage; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore คำพูดบล็อก sourceediting - /** * Tests for CKEditor 5 UI translations. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class LanguageTest extends CKEditor5TestBase { /** @@ -33,9 +34,8 @@ class LanguageTest extends CKEditor5TestBase { * The CKEditor 5 plugin to enable. * @param string $toolbar_item_translation * The expected translation for CKEditor 5 plugin toolbar button. - * - * @dataProvider provider */ + #[DataProvider('provider')] public function test(string $langcode, string $toolbar_item_name, string $toolbar_item_translation): void { $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLibraryTest.php index c95baf47e0c..87672972834 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLibraryTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLibraryTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\MediaLibrary; use Drupal\ckeditor5\Plugin\Editor\CKEditor5; use Drupal\editor\Entity\Editor; use Drupal\file\Entity\File; @@ -13,15 +14,18 @@ use Drupal\media\Entity\Media; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; // cspell:ignore arrakis complote détruire harkonnen - /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\MediaLibrary - * @group ckeditor5 + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\MediaLibrary. + * * @internal */ +#[CoversClass(MediaLibrary::class)] +#[Group('ckeditor5')] class MediaLibraryTest extends WebDriverTestBase { use MediaTypeCreationTrait; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLinkabilityTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLinkabilityTest.php index 2d581b36ee5..c80cb5fd26d 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLinkabilityTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaLinkabilityTest.php @@ -4,23 +4,28 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media; +use Drupal\ckeditor5\Plugin\Editor\CKEditor5; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; -use Drupal\ckeditor5\Plugin\Editor\CKEditor5; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media - * @group ckeditor5 + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media. + * * @internal */ +#[CoversClass(Media::class)] +#[Group('ckeditor5')] class MediaLinkabilityTest extends MediaTestBase { /** * Ensures arbitrary attributes can be added on links wrapping media via GHS. - * - * @dataProvider providerLinkability */ + #[DataProvider('providerLinkability')] public function testLinkedMediaArbitraryHtml(bool $unrestricted): void { $assert_session = $this->assertSession(); @@ -84,9 +89,8 @@ class MediaLinkabilityTest extends MediaTestBase { * "dataDowncast" results. These are CKEditor 5 concepts. * * @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#conversion - * - * @dataProvider providerLinkability */ + #[DataProvider('providerLinkability')] public function testLinkability(bool $unrestricted): void { // Disable filter_html. if ($unrestricted) { @@ -235,9 +239,8 @@ class MediaLinkabilityTest extends MediaTestBase { /** * Ensure that manual link decorators work with linkable media. - * - * @dataProvider providerLinkability */ + #[DataProvider('providerLinkability')] public function testLinkManualDecorator(bool $unrestricted): void { \Drupal::service('module_installer')->install(['ckeditor5_manual_decorator_test']); $this->resetAll(); diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaPreviewTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaPreviewTest.php index 80e6d94373c..bcf930530a1 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaPreviewTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaPreviewTest.php @@ -4,18 +4,23 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media; use Drupal\Core\Database\Database; use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\filter\Entity\FilterFormat; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore drupalmediaediting - /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media - * @group ckeditor5 - * @group #slow + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media. + * * @internal */ +#[CoversClass(Media::class)] +#[Group('ckeditor5')] +#[Group('#slow')] class MediaPreviewTest extends MediaTestBase { /** @@ -138,9 +143,8 @@ class MediaPreviewTest extends MediaTestBase { * Whether to test with media_embed filter enabled on the text format. * @param bool $can_use_format * Whether the logged in user is allowed to use the text format. - * - * @dataProvider previewAccessProvider */ + #[DataProvider('previewAccessProvider')] public function testEmbedPreviewAccess($media_embed_enabled, $can_use_format): void { // Reconfigure the host entity's text format to suit our needs. /** @var \Drupal\filter\FilterFormatInterface $format */ diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php index 5ba28449708..a53685e2ecd 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; +use Drupal\ckeditor5\Plugin\Editor\CKEditor5; use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Entity\Entity\EntityViewMode; -use Drupal\ckeditor5\Plugin\Editor\CKEditor5; use Drupal\editor\Entity\Editor; use Drupal\field\Entity\FieldConfig; use Drupal\filter\Entity\FilterFormat; @@ -15,17 +15,21 @@ use Drupal\language\Entity\ContentLanguageSettings; use Drupal\media\Entity\Media; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; // cspell:ignore alternatif drupalelementstyle hurlant layercake tatou texte // cspell:ignore zartan - /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media - * @group ckeditor5 - * @group #slow + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media. + * * @internal */ +#[CoversClass(\Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media::class)] +#[Group('ckeditor5')] +#[Group('#slow')] class MediaTest extends MediaTestBase { /** @@ -99,6 +103,7 @@ class MediaTest extends MediaTestBase { 'properties' => [ 'reversed' => FALSE, 'startIndex' => FALSE, + 'styles' => FALSE, ], 'multiBlock' => TRUE, ]; @@ -759,9 +764,8 @@ class MediaTest extends MediaTestBase { * Tests that view mode is reflected onto the CKEditor 5 Widget wrapper, that * the media style toolbar allows changing the view mode and that the changes * are reflected on the widget and downcast drupal-media tag. - * - * @dataProvider providerTestViewMode */ + #[DataProvider('providerTestViewMode')] public function testViewMode(bool $with_alignment): void { EntityViewMode::create([ 'id' => 'media.view_mode_3', diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php index 31e6d07ca31..ac5596a752a 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php @@ -5,23 +5,29 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; use Drupal\ckeditor5\HTMLRestrictions; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing; +use Drupal\ckeditor5\Plugin\Editor\CKEditor5; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; -use Drupal\ckeditor5\Plugin\Editor\CKEditor5; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; // cspell:ignore gramma sourceediting - /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing - * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig - * @group ckeditor5 + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing. + * * @internal + * @legacy-covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig */ +#[CoversClass(SourceEditing::class)] +#[Group('ckeditor5')] class SourceEditingTest extends SourceEditingTestBase { /** - * @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing::buildConfigurationForm + * Tests source editing settings form. + * + * @legacy-covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing::buildConfigurationForm */ public function testSourceEditingSettingsForm(): void { $this->drupalLogin($this->drupalCreateUser(['administer filters'])); @@ -229,39 +235,6 @@ JS; // Edge case: `style`. // @todo https://www.drupal.org/project/drupal/issues/3304832 - - // Edge case: `type` attribute on lists. - // @todo Remove in https://www.drupal.org/project/drupal/issues/3274635. - 'no numberedList-related additions to the Source Editing configuration' => [ - '<ol type="A"><li>foo</li><li>bar</li></ol>', - '<ol><li>foo</li><li>bar</li></ol>', - '', - ], - '<ol type>' => [ - '<ol type="A"><li>foo</li><li>bar</li></ol>', - '<ol type="A"><li>foo</li><li>bar</li></ol>', - '<ol type>', - ], - '<ol type="A">' => [ - '<ol type="A"><li>foo</li><li>bar</li></ol>', - '<ol type="A"><li>foo</li><li>bar</li></ol>', - '<ol type="A">', - ], - 'no bulletedList-related additions to the Source Editing configuration' => [ - '<ul type="circle"><li>foo</li><li>bar</li></ul>', - '<ul><li>foo</li><li>bar</li></ul>', - '', - ], - '<ul type>' => [ - '<ul type="circle"><li>foo</li><li>bar</li></ul>', - '<ul type="circle"><li>foo</li><li>bar</li></ul>', - '<ul type>', - ], - '<ul type="circle">' => [ - '<ul type="circle"><li>foo</li><li>bar</li></ul>', - '<ul type="circle"><li>foo</li><li>bar</li></ul>', - '<ul type="circle">', - ], ]; } diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTestBase.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTestBase.php index 9442f379ee7..740e4515961 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTestBase.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTestBase.php @@ -87,6 +87,7 @@ abstract class SourceEditingTestBase extends CKEditor5TestBase { 'properties' => [ 'reversed' => FALSE, 'startIndex' => FALSE, + 'styles' => FALSE, ], 'multiBlock' => TRUE, ], diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/StyleTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/StyleTest.php index 2de2d3201dc..71368486a67 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/StyleTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/StyleTest.php @@ -4,25 +4,31 @@ declare(strict_types=1); namespace Drupal\Tests\ckeditor5\FunctionalJavascript; -// cspell:ignore sourceediting - +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style; use Drupal\ckeditor5\Plugin\Editor\CKEditor5; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; +// cspell:ignore sourceediting +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; /** - * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style - * @group ckeditor5 + * Tests Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style. + * * @internal */ +#[CoversClass(Style::class)] +#[Group('ckeditor5')] class StyleTest extends CKEditor5TestBase { use CKEditor5TestTrait; /** - * @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style::buildConfigurationForm + * Tests style settings form. + * + * @legacy-covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style::buildConfigurationForm */ public function testStyleSettingsForm(): void { $this->drupalLogin($this->drupalCreateUser(['administer filters'])); @@ -190,6 +196,7 @@ JS; 'properties' => [ 'reversed' => FALSE, 'startIndex' => FALSE, + 'styles' => FALSE, ], 'multiBlock' => TRUE, ], @@ -400,7 +407,7 @@ JS; // Close the dropdown. $style_dropdown->click(); - // Select the <ul> and check the available styles + // Select the <ul> and check the available styles. $this->selectTextInsideElement('ul'); $this->assertSame('Styles', $style_dropdown->getText()); $style_dropdown->click(); @@ -441,7 +448,7 @@ JS; $this->assertTrue($buttons[8]->hasClass('ck-off')); $this->assertSame('Items', $style_dropdown->getText()); - // Select the <ol> and check the available styles + // Select the <ol> and check the available styles. $this->selectTextInsideElement('ol'); $this->assertSame('Styles', $style_dropdown->getText()); $style_dropdown->click(); @@ -482,7 +489,7 @@ JS; $this->assertTrue($buttons[8]->hasClass('ck-off')); $this->assertSame('Steps', $style_dropdown->getText()); - // Select the table and check the available styles + // Select the table and check the available styles. $this->selectTextInsideElement('table td'); $this->assertSame('Styles', $style_dropdown->getText()); $style_dropdown->click(); diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/TableTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/TableTest.php index 1d3dadbe4c3..0dc9e847bdd 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/TableTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/TableTest.php @@ -9,14 +9,15 @@ use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Validator\ConstraintViolationInterface; /** * For testing the table plugin. * - * @group ckeditor5 * @internal */ +#[Group('ckeditor5')] class TableTest extends WebDriverTestBase { use CKEditor5TestTrait; diff --git a/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php b/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php index 16d571cf980..976e388f3ea 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php @@ -94,6 +94,7 @@ class ConfigurablePluginTest extends KernelTestBase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], diff --git a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php index 2b9d4d1fde8..76818df9f78 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php @@ -609,6 +609,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -621,8 +622,6 @@ class SmartDefaultSettingsTest extends KernelTestBase { '<span>', '<a hreflang>', '<blockquote cite>', - '<ul type>', - '<ol type>', '<h2 id>', '<h3 id>', '<h4 id>', @@ -656,13 +655,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'status' => [ [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:10:"Basic HTML";s:19:"@missing_attributes";s:90:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:10:"Basic HTML";s:19:"@missing_attributes";s:70:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -684,9 +683,9 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'ckeditor5_list' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_list'], 'ckeditor5_sourceEditing' => [ 'allowed_tags' => array_merge( - array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 0, 9), + array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 0, 7), ['<img data-caption>'], - array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 9), + array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 7), ), ], ], @@ -698,13 +697,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ...$basic_html_test_case['expected_db_logs']['status'], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:10:"Basic HTML";s:19:"@missing_attributes";s:109:"<a hreflang> <blockquote cite> <ul type> <ol type> <img data-caption> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:10:"Basic HTML";s:19:"@missing_attributes";s:89:"<a hreflang> <blockquote cite> <img data-caption> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <img data-caption> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <img data-caption> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -725,9 +724,9 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'ckeditor5_list' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_list'], 'ckeditor5_sourceEditing' => [ 'allowed_tags' => array_merge( - array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 0, 9), + array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 0, 7), ['<img data-align>'], - array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 9), + array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 7), ), ], ], @@ -739,13 +738,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ...$basic_html_test_case['expected_db_logs']['status'], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:10:"Basic HTML";s:19:"@missing_attributes";s:107:"<a hreflang> <blockquote cite> <ul type> <ol type> <img data-align> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:10:"Basic HTML";s:19:"@missing_attributes";s:87:"<a hreflang> <blockquote cite> <img data-align> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <img data-align> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <img data-align> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -764,9 +763,9 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'ckeditor5_list' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_list'], 'ckeditor5_sourceEditing' => [ 'allowed_tags' => array_merge( - array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 0, 9), + array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 0, 7), ['<img data-entity-type data-entity-uuid>'], - array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 9), + array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 7), ), ], ], @@ -785,13 +784,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:34:"Basic HTML (without image uploads)";s:19:"@missing_attributes";s:130:"<a hreflang> <blockquote cite> <ul type> <ol type> <img data-entity-type data-entity-uuid> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:34:"Basic HTML (without image uploads)";s:19:"@missing_attributes";s:110:"<a hreflang> <blockquote cite> <img data-entity-type data-entity-uuid> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <img data-entity-type data-entity-uuid> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <img data-entity-type data-entity-uuid> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -819,6 +818,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -844,13 +844,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:30:"Basic HTML (without H4 and H6)";s:19:"@missing_attributes";s:74:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h5 id>";}', + 'a:2:{s:12:"%text_format";s:30:"Basic HTML (without H4 and H6)";s:19:"@missing_attributes";s:54:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h5 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h5 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h5 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: The tags <em class="placeholder"><h4>, <h6></em>; This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -878,6 +878,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -904,13 +905,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:22:"Basic HTML (with <h1>)";s:19:"@missing_attributes";s:90:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:22:"Basic HTML (with <h1>)";s:19:"@missing_attributes";s:70:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <h1> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <h1> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -932,6 +933,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -957,13 +959,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:23:"Basic HTML (without H*)";s:19:"@missing_attributes";s:50:"<a hreflang> <blockquote cite> <ul type> <ol type>";}', + 'a:2:{s:12:"%text_format";s:23:"Basic HTML (without H*)";s:19:"@missing_attributes";s:30:"<a hreflang> <blockquote cite>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: The tags <em class="placeholder"><h2>, <h3>, <h4>, <h5>, <h6></em>; This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -1003,7 +1005,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], ] + $basic_html_test_case['expected_ckeditor5_settings']['plugins'], ], - 'expected_superset' => '<ol reversed> <code class="language-*">', + 'expected_superset' => '<code class="language-*"> <ol reversed>', 'expected_fundamental_compatibility_violations' => $basic_html_test_case['expected_fundamental_compatibility_violations'], 'expected_db_logs' => [ 'status' => [ @@ -1017,16 +1019,16 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:23:"Basic HTML (with <pre>)";s:19:"@missing_attributes";s:90:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:23:"Basic HTML (with <pre>)";s:19:"@missing_attributes";s:70:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Code Block, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Code Block, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ - 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: These attributes: <em class="placeholder"> reversed (for <ol>), class (for <code>)</em>; Additional details are available in your logs.', + 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: These attributes: <em class="placeholder"> class (for <code>), reversed (for <ol>)</em>; Additional details are available in your logs.', ], ], ]; @@ -1083,13 +1085,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:45:"Basic HTML (with alignable paragraph support)";s:19:"@missing_attributes";s:90:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:45:"Basic HTML (with alignable paragraph support)";s:19:"@missing_attributes";s:70:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption, Alignment</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption, Alignment</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: These attributes: <em class="placeholder"> class (for <p>, <h2>, <h3>, <h4>, <h5>, <h6>), reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -1120,13 +1122,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:37:"Basic HTML (with Media Embed support)";s:19:"@missing_attributes";s:90:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:37:"Basic HTML (with Media Embed support)";s:19:"@missing_attributes";s:70:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -1168,13 +1170,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:74:"(with Media Embed support, view mode enabled but no view modes configured)";s:19:"@missing_attributes";s:120:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-view-mode>";}', + 'a:2:{s:12:"%text_format";s:74:"(with Media Embed support, view mode enabled but no view modes configured)";s:19:"@missing_attributes";s:100:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-view-mode>";}', ], ], ], 'expected_messages' => array_merge_recursive($basic_html_test_case['expected_messages'], [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-view-mode>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-view-mode>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -1182,7 +1184,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { ]), 'expected_post_filter_drop_fundamental_compatibility_violations' => [], 'expected_post_update_text_editor_violations' => [ - 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.14' => 'The following attribute(s) can optionally be supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">Media (<drupal-media data-view-mode>)</em>.', + 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.12' => 'The following attribute(s) can optionally be supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">Media (<drupal-media data-view-mode>)</em>.', ], ]; @@ -1220,13 +1222,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:76:"(with Media Embed support, view mode enabled and two view modes configured )";s:19:"@missing_attributes";s:120:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-view-mode>";}', + 'a:2:{s:12:"%text_format";s:76:"(with Media Embed support, view mode enabled and two view modes configured )";s:19:"@missing_attributes";s:100:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-view-mode>";}', ], ], ], 'expected_messages' => array_merge_recursive($basic_html_test_case['expected_messages'], [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-view-mode>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload, Image align, Image caption</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-view-mode>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -1234,7 +1236,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { ]), 'expected_post_filter_drop_fundamental_compatibility_violations' => [], 'expected_post_update_text_editor_violations' => [ - 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.14' => 'The following attribute(s) can optionally be supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">Media (<drupal-media data-view-mode>)</em>.', + 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.12' => 'The following attribute(s) can optionally be supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">Media (<drupal-media data-view-mode>)</em>.', ], ]; @@ -1249,9 +1251,9 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'ckeditor5_list' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_list'], 'ckeditor5_sourceEditing' => [ 'allowed_tags' => array_merge( - array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 0, 9), + array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 0, 7), ['<img data-*>'], - array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 9), + array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], 7), ), ], ] + $basic_html_test_case['expected_ckeditor5_settings']['plugins'], @@ -1270,13 +1272,13 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:48:"Basic HTML (with any data-* attribute on images)";s:19:"@missing_attributes";s:103:"<a hreflang> <blockquote cite> <ul type> <ol type> <img data-*> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:48:"Basic HTML (with any data-* attribute on images)";s:19:"@missing_attributes";s:83:"<a hreflang> <blockquote cite> <img data-*> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <ul type> <ol type> <img data-*> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List, Image, Image Upload</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <span> <a hreflang> <blockquote cite> <img data-*> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: This attribute: <em class="placeholder"> reversed (for <ol>)</em>; Additional details are available in your logs.', @@ -1325,6 +1327,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -1336,8 +1339,6 @@ class SmartDefaultSettingsTest extends KernelTestBase { '<dd>', '<a hreflang>', '<blockquote cite>', - '<ul type>', - '<ol type>', '<h2 id>', '<h3 id>', '<h4 id>', @@ -1363,7 +1364,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:15:"Restricted HTML";s:19:"@missing_attributes";s:90:"<a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:15:"Restricted HTML";s:19:"@missing_attributes";s:70:"<a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], 'warning' => [ @@ -1375,10 +1376,10 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <a hreflang> <blockquote cite> <ul type> <ol type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <a hreflang> <blockquote cite> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ - 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: The <br>, <p> tags were added because they are <a target="_blank" href="/admin/help/ckeditor5#required-tags">required by CKEditor 5</a>. The tags <em class="placeholder"><h2>, <h3>, <h4>, <h5>, <h6>, <*>, <cite>, <dl>, <dt>, <dd>, <a>, <blockquote>, <ul>, <ol>, <strong>, <em>, <code>, <li></em>; These attributes: <em class="placeholder"> id (for <h2>, <h3>, <h4>, <h5>, <h6>), dir (for <*>), lang (for <*>), hreflang (for <a>), href (for <a>), cite (for <blockquote>), type (for <ul>, <ol>), reversed (for <ol>), start (for <ol>)</em>; Additional details are available in your logs.', + 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: The <br>, <p> tags were added because they are <a target="_blank" href="/admin/help/ckeditor5#required-tags">required by CKEditor 5</a>. The tags <em class="placeholder"><h2>, <h3>, <h4>, <h5>, <h6>, <*>, <cite>, <dl>, <dt>, <dd>, <a>, <blockquote>, <strong>, <em>, <code>, <ul>, <ol>, <li></em>; These attributes: <em class="placeholder"> id (for <h2>, <h3>, <h4>, <h5>, <h6>), dir (for <*>), lang (for <*>), hreflang (for <a>), href (for <a>), cite (for <blockquote>), type (for <ul>, <ol>), reversed (for <ol>), start (for <ol>)</em>; Additional details are available in your logs.', ], ], 'expected_post_filter_drop_fundamental_compatibility_violations' => [], @@ -1456,6 +1457,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -1467,8 +1469,6 @@ class SmartDefaultSettingsTest extends KernelTestBase { '<dd>', '<a hreflang>', '<blockquote cite>', - '<ul type>', - '<ol type="1 A I">', '<h2 id="jump-*">', '<h3 id>', '<h4 id>', @@ -1478,7 +1478,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], ], ], - 'expected_superset' => '<br> <p> <ol reversed>', + 'expected_superset' => '<br> <p> <ol type reversed>', 'expected_fundamental_compatibility_violations' => [ '' => 'CKEditor 5 needs at least the <p> and <br> tags to be allowed to be able to function. They are not allowed by the "<em class="placeholder">Limit allowed HTML tags and correct faulty HTML</em>" (<em class="placeholder">filter_html</em>) filter.', ], @@ -1494,7 +1494,7 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], [ 'As part of migrating to CKEditor 5, it was found that the %text_format text format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: @missing_attributes. The text format must be saved to make these changes active.', - 'a:2:{s:12:"%text_format";s:54:"Only the "filter_html" filter and its default settings";s:19:"@missing_attributes";s:107:"<a hreflang> <blockquote cite> <ul type> <ol type="1 A I"> <h2 id="jump-*"> <h3 id> <h4 id> <h5 id> <h6 id>";}', + 'a:2:{s:12:"%text_format";s:54:"Only the "filter_html" filter and its default settings";s:19:"@missing_attributes";s:79:"<a hreflang> <blockquote cite> <h2 id="jump-*"> <h3 id> <h4 id> <h5 id> <h6 id>";}', ], ], 'warning' => [ @@ -1506,10 +1506,10 @@ class SmartDefaultSettingsTest extends KernelTestBase { ], 'expected_messages' => [ 'status' => [ - 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <a hreflang> <blockquote cite> <ul type> <ol type="1 A I"> <h2 id="jump-*"> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', + 'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Enabled these plugins: (<em class="placeholder">Link, Block quote, Code, List</em>). Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <cite> <dl> <dt> <dd> <a hreflang> <blockquote cite> <h2 id="jump-*"> <h3 id> <h4 id> <h5 id> <h6 id>. Additional details are available in your logs.', ], 'warning' => [ - 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: The <br>, <p> tags were added because they are <a target="_blank" href="/admin/help/ckeditor5#required-tags">required by CKEditor 5</a>. The tags <em class="placeholder"><h2>, <h3>, <h4>, <h5>, <h6>, <*>, <cite>, <dl>, <dt>, <dd>, <a>, <blockquote>, <ul>, <ol>, <strong>, <em>, <code>, <li></em>; These attributes: <em class="placeholder"> id (for <h2>, <h3>, <h4>, <h5>, <h6>), dir (for <*>), lang (for <*>), hreflang (for <a>), href (for <a>), cite (for <blockquote>), type (for <ul>, <ol>), reversed (for <ol>), start (for <ol>)</em>; Additional details are available in your logs.', + 'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following: The <br>, <p> tags were added because they are <a target="_blank" href="/admin/help/ckeditor5#required-tags">required by CKEditor 5</a>. The tags <em class="placeholder"><h2>, <h3>, <h4>, <h5>, <h6>, <*>, <cite>, <dl>, <dt>, <dd>, <a>, <blockquote>, <strong>, <em>, <code>, <ul>, <ol>, <li></em>; These attributes: <em class="placeholder"> id (for <h2>, <h3>, <h4>, <h5>, <h6>), dir (for <*>), lang (for <*>), hreflang (for <a>), href (for <a>), cite (for <blockquote>), type (for <ul>, <ol>), reversed (for <ol>), start (for <ol>)</em>; Additional details are available in your logs.', ], ], ]; diff --git a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php index 75735a0cc51..7b9bff386d8 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php @@ -135,6 +135,7 @@ class ValidatorsTest extends KernelTestBase { 'properties' => [ 'reversed' => FALSE, 'startIndex' => FALSE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -582,7 +583,7 @@ class ValidatorsTest extends KernelTestBase { ], 'expected_violations' => [], ]; - $data['INVALID: SourceEditing plugin configuration: <ol start type> must not be allowed because List can generate <ol reversed start>'] = [ + $data['INVALID: SourceEditing plugin configuration: <ol start type> must not be allowed because List can generate <ol reversed start type>'] = [ 'ckeditor5_settings' => [ 'toolbar' => [ 'items' => [ @@ -595,6 +596,7 @@ class ValidatorsTest extends KernelTestBase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => FALSE, ], 'multiBlock' => TRUE, ], @@ -606,10 +608,13 @@ class ValidatorsTest extends KernelTestBase { ], ], 'expected_violations' => [ - 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.0' => 'The following attribute(s) are already supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">List (<ol start>)</em>.', + 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.0' => [ + 'The following attribute(s) are already supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">List (<ol start>)</em>.', + 'The following attribute(s) can optionally be supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">List (<ol type>)</em>.', + ], ], ]; - $data['INVALID: SourceEditing plugin configuration: <ol start type> must not be allowed because List can generate <ol start>'] = [ + $data['INVALID: SourceEditing plugin configuration: <ol start type> must not be allowed because List can generate <ol start type>'] = [ 'ckeditor5_settings' => [ 'toolbar' => [ 'items' => [ @@ -622,6 +627,7 @@ class ValidatorsTest extends KernelTestBase { 'properties' => [ 'reversed' => FALSE, 'startIndex' => FALSE, + 'styles' => FALSE, ], 'multiBlock' => TRUE, ], @@ -633,7 +639,7 @@ class ValidatorsTest extends KernelTestBase { ], ], 'expected_violations' => [ - 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.0' => 'The following attribute(s) can optionally be supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">List (<ol start>)</em>.', + 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.0' => 'The following attribute(s) can optionally be supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">List (<ol start type>)</em>.', ], ]; diff --git a/core/modules/ckeditor5/tests/src/Unit/CKEditor5ImageControllerTest.php b/core/modules/ckeditor5/tests/src/Unit/CKEditor5ImageControllerTest.php new file mode 100644 index 00000000000..bb69c28c6fb --- /dev/null +++ b/core/modules/ckeditor5/tests/src/Unit/CKEditor5ImageControllerTest.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ckeditor5\Unit; + +use Drupal\ckeditor5\Controller\CKEditor5ImageController; +use Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface; +use Drupal\Core\Entity\EntityConstraintViolationList; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityTypeRepositoryInterface; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Lock\LockBackendInterface; +use Drupal\editor\EditorInterface; +use Drupal\file\Entity\File; +use Drupal\file\FileInterface; +use Drupal\file\Upload\FileUploadHandlerInterface; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\HttpException; + +/** + * Tests CKEditor5ImageController. + * + * @group ckeditor5 + * @coversDefaultClass \Drupal\ckeditor5\Controller\CKEditor5ImageController + */ +final class CKEditor5ImageControllerTest extends UnitTestCase { + + /** + * Tests that upload fails correctly when the file is too large. + */ + public function testInvalidFile(): void { + $file_system = $this->prophesize(FileSystemInterface::class); + $file_system->move(Argument::any())->shouldNotBeCalled(); + $directory = 'public://'; + $file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)->willReturn(TRUE); + $file_system->getDestinationFilename(Argument::cetera())->willReturn('/tmp/foo.txt'); + $lock = $this->prophesize(LockBackendInterface::class); + $lock->acquire(Argument::any())->willReturn(TRUE); + $container = $this->prophesize(ContainerInterface::class); + $file_storage = $this->prophesize(EntityStorageInterface::class); + $file = $this->prophesize(FileInterface::class); + $violations = $this->prophesize(EntityConstraintViolationList::class); + $violations->count()->willReturn(0); + $file->validate()->willReturn($violations->reveal()); + $file_storage->create(Argument::any())->willReturn($file->reveal()); + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getStorage('file')->willReturn($file_storage->reveal()); + $container->get('entity_type.manager')->willReturn($entity_type_manager->reveal()); + $entity_type_repository = $this->prophesize(EntityTypeRepositoryInterface::class); + $entity_type_repository->getEntityTypeFromClass(File::class)->willReturn('file'); + $container->get('entity_type.repository')->willReturn($entity_type_repository->reveal()); + \Drupal::setContainer($container->reveal()); + $controller = new CKEditor5ImageController( + $file_system->reveal(), + $this->prophesize(FileUploadHandlerInterface::class)->reveal(), + $lock->reveal(), + $this->prophesize(CKEditor5PluginManagerInterface::class)->reveal(), + ); + // We can't use vfsstream here because of how Symfony request works. + $file_uri = tempnam(sys_get_temp_dir(), 'tmp'); + $fp = fopen($file_uri, 'w'); + fwrite($fp, 'foo'); + fclose($fp); + $request = Request::create('/', files: [ + 'upload' => [ + 'name' => 'foo.txt', + 'type' => 'text/plain', + 'size' => 42, + 'tmp_name' => $file_uri, + 'error' => \UPLOAD_ERR_FORM_SIZE, + ], + ]); + $editor = $this->prophesize(EditorInterface::class); + $request->attributes->set('editor', $editor->reveal()); + $this->expectException(HttpException::class); + $this->expectExceptionMessage('The file "foo.txt" exceeds the upload limit defined in your form.'); + $controller->upload($request); + } + +} diff --git a/core/modules/ckeditor5/tests/src/Unit/ListPluginTest.php b/core/modules/ckeditor5/tests/src/Unit/ListPluginTest.php index 1efae1f5b7f..f4cc007ac1a 100644 --- a/core/modules/ckeditor5/tests/src/Unit/ListPluginTest.php +++ b/core/modules/ckeditor5/tests/src/Unit/ListPluginTest.php @@ -26,6 +26,7 @@ class ListPluginTest extends UnitTestCase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => FALSE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -34,7 +35,9 @@ class ListPluginTest extends UnitTestCase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => FALSE, - 'styles' => FALSE, + 'styles' => [ + 'useAttribute' => TRUE, + ], ], 'multiBlock' => TRUE, ], @@ -45,6 +48,7 @@ class ListPluginTest extends UnitTestCase { 'properties' => [ 'reversed' => FALSE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -53,17 +57,40 @@ class ListPluginTest extends UnitTestCase { 'properties' => [ 'reversed' => FALSE, 'startIndex' => TRUE, + 'styles' => [ + 'useAttribute' => TRUE, + ], + ], + 'multiBlock' => TRUE, + ], + ], + ], + 'styles is false' => [ + [ + 'properties' => [ + 'reversed' => TRUE, + 'startIndex' => TRUE, + 'styles' => FALSE, + ], + 'multiBlock' => TRUE, + ], + [ + 'list' => [ + 'properties' => [ + 'reversed' => TRUE, + 'startIndex' => TRUE, 'styles' => FALSE, ], 'multiBlock' => TRUE, ], ], ], - 'both disabled' => [ + 'all disabled' => [ [ 'properties' => [ 'reversed' => FALSE, 'startIndex' => FALSE, + 'styles' => FALSE, ], 'multiBlock' => TRUE, ], @@ -78,11 +105,12 @@ class ListPluginTest extends UnitTestCase { ], ], ], - 'both enabled' => [ + 'all enabled' => [ [ 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, + 'styles' => TRUE, ], 'multiBlock' => TRUE, ], @@ -91,7 +119,9 @@ class ListPluginTest extends UnitTestCase { 'properties' => [ 'reversed' => TRUE, 'startIndex' => TRUE, - 'styles' => FALSE, + 'styles' => [ + 'useAttribute' => TRUE, + ], ], 'multiBlock' => TRUE, ], diff --git a/core/modules/comment/src/Hook/CommentTokensHooks.php b/core/modules/comment/src/Hook/CommentTokensHooks.php index 7d7e9d0c03f..630e74e8752 100644 --- a/core/modules/comment/src/Hook/CommentTokensHooks.php +++ b/core/modules/comment/src/Hook/CommentTokensHooks.php @@ -47,7 +47,7 @@ class CommentTokensHooks { ]; } } - // Core comment tokens + // Core comment tokens. $comment['cid'] = ['name' => $this->t("Comment ID"), 'description' => $this->t("The unique ID of the comment.")]; $comment['uuid'] = ['name' => $this->t('UUID'), 'description' => $this->t("The UUID of the comment.")]; $comment['hostname'] = [ @@ -76,7 +76,7 @@ class CommentTokensHooks { 'name' => $this->t("Edit URL"), 'description' => $this->t("The URL of the comment's edit page."), ]; - // Chained tokens for comments + // Chained tokens for comments. $comment['created'] = [ 'name' => $this->t("Date created"), 'description' => $this->t("The date the comment was posted."), diff --git a/core/modules/comment/src/Plugin/views/field/StatisticsLastCommentName.php b/core/modules/comment/src/Plugin/views/field/StatisticsLastCommentName.php index 098b6855c56..045258af50a 100644 --- a/core/modules/comment/src/Plugin/views/field/StatisticsLastCommentName.php +++ b/core/modules/comment/src/Plugin/views/field/StatisticsLastCommentName.php @@ -39,7 +39,7 @@ class StatisticsLastCommentName extends FieldPluginBase { // last_comment_name only contains data if the user is anonymous. So we // have to join in a specially related user table. $this->ensureMyTable(); - // Join 'users' to this table via vid + // Join 'users' to this table via vid. $definition = [ 'table' => 'users_field_data', 'field' => 'uid', diff --git a/core/modules/comment/tests/src/Functional/CommentAdminTest.php b/core/modules/comment/tests/src/Functional/CommentAdminTest.php index 69c634ba0f9..7d443963bdf 100644 --- a/core/modules/comment/tests/src/Functional/CommentAdminTest.php +++ b/core/modules/comment/tests/src/Functional/CommentAdminTest.php @@ -45,7 +45,7 @@ class CommentAdminTest extends CommentTestBase { // Ensure that doesn't require contact info. $this->setCommentAnonymous(CommentInterface::ANONYMOUS_MAYNOT_CONTACT); - // Test that the comments page loads correctly when there are no comments + // Test that the comments page loads correctly when there are no comments. $this->drupalGet('admin/content/comment'); $this->assertSession()->pageTextContains('No comments available.'); diff --git a/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php b/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php index 203a0fff425..73e645c8918 100644 --- a/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php +++ b/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php @@ -171,7 +171,7 @@ class CommentNonNodeTest extends BrowserTestBase { break; } $match = []; - // Get comment ID + // Get comment ID. preg_match('/#comment-([0-9]+)/', $this->getURL(), $match); // Get comment. diff --git a/core/modules/comment/tests/src/Functional/CommentPagerTest.php b/core/modules/comment/tests/src/Functional/CommentPagerTest.php index 4927803208b..f476526eeef 100644 --- a/core/modules/comment/tests/src/Functional/CommentPagerTest.php +++ b/core/modules/comment/tests/src/Functional/CommentPagerTest.php @@ -287,17 +287,17 @@ class CommentPagerTest extends CommentTestBase { $this->setCommentSettings('default_mode', CommentManagerInterface::COMMENT_MODE_FLAT, 'Comment paging changed.'); $expected_pages = [ - // Page of comment 5 + // Page of comment 5. 1 => 5, - // Page of comment 4 + // Page of comment 4. 2 => 4, - // Page of comment 3 + // Page of comment 3. 3 => 3, - // Page of comment 2 + // Page of comment 2. 4 => 2, - // Page of comment 1 + // Page of comment 1. 5 => 1, - // Page of comment 0 + // Page of comment 0. 6 => 0, ]; @@ -311,17 +311,17 @@ class CommentPagerTest extends CommentTestBase { $this->setCommentSettings('default_mode', CommentManagerInterface::COMMENT_MODE_THREADED, 'Switched to threaded mode.'); $expected_pages = [ - // Page of comment 5 + // Page of comment 5. 1 => 5, - // Page of comment 4 + // Page of comment 4. 2 => 1, - // Page of comment 4 + // Page of comment 4. 3 => 1, - // Page of comment 4 + // Page of comment 4. 4 => 1, - // Page of comment 4 + // Page of comment 4. 5 => 1, - // Page of comment 0 + // Page of comment 0. 6 => 0, ]; diff --git a/core/modules/comment/tests/src/Functional/CommentTestBase.php b/core/modules/comment/tests/src/Functional/CommentTestBase.php index 1744bdd9cde..c4932d47826 100644 --- a/core/modules/comment/tests/src/Functional/CommentTestBase.php +++ b/core/modules/comment/tests/src/Functional/CommentTestBase.php @@ -168,7 +168,7 @@ abstract class CommentTestBase extends BrowserTestBase { break; } $match = []; - // Get comment ID + // Get comment ID. preg_match('/#comment-([0-9]+)/', $this->getURL(), $match); // Get comment. diff --git a/core/modules/comment/tests/src/Functional/Views/DefaultViewRecentCommentsTest.php b/core/modules/comment/tests/src/Functional/Views/DefaultViewRecentCommentsTest.php index 12a781d4c0e..89596107bfe 100644 --- a/core/modules/comment/tests/src/Functional/Views/DefaultViewRecentCommentsTest.php +++ b/core/modules/comment/tests/src/Functional/Views/DefaultViewRecentCommentsTest.php @@ -70,7 +70,7 @@ class DefaultViewRecentCommentsTest extends ViewTestBase { protected function setUp($import_test_views = TRUE, $modules = []): void { parent::setUp($import_test_views, $modules); - // Create a new content type + // Create a new content type. $content_type = $this->drupalCreateContentType(); // Add a node of the new content type. diff --git a/core/modules/comment/tests/src/Kernel/CommentOrphanTest.php b/core/modules/comment/tests/src/Kernel/CommentOrphanTest.php index 8c31076f475..a3e63629bd4 100644 --- a/core/modules/comment/tests/src/Kernel/CommentOrphanTest.php +++ b/core/modules/comment/tests/src/Kernel/CommentOrphanTest.php @@ -77,7 +77,7 @@ class CommentOrphanTest extends EntityKernelTestBase { 'label' => 'Comment', ])->save(); - // Make two comments + // Make two comments. $comment1 = $comment_storage->create([ 'field_name' => 'comment', 'comment_body' => 'test', diff --git a/core/modules/comment/tests/src/Kernel/Views/FilterAndArgumentUserUidTest.php b/core/modules/comment/tests/src/Kernel/Views/FilterAndArgumentUserUidTest.php index 40eaeeaa3cb..d332080e7c8 100644 --- a/core/modules/comment/tests/src/Kernel/Views/FilterAndArgumentUserUidTest.php +++ b/core/modules/comment/tests/src/Kernel/Views/FilterAndArgumentUserUidTest.php @@ -90,7 +90,7 @@ class FilterAndArgumentUserUidTest extends KernelTestBase { 'entity_type' => 'node', 'field_name' => 'comment', ])->save(); - // Comment added by $other_account on $node_commented_by_account + // Comment added by $other_account on $node_commented_by_account. Comment::create([ 'uid' => $other_account->id(), 'entity_id' => $node_commented_by_account->id(), diff --git a/core/modules/config/tests/config_override_test/config/install/block.block.call_to_action.yml b/core/modules/config/tests/config_override_test/config/install/block.block.call_to_action.yml index 2fe777a1da3..8ac15c8f1bc 100644 --- a/core/modules/config/tests/config_override_test/config/install/block.block.call_to_action.yml +++ b/core/modules/config/tests/config_override_test/config/install/block.block.call_to_action.yml @@ -16,8 +16,6 @@ settings: label: 'Shop for cheap now!' label_display: visible provider: block_content - status: true - info: '' view_mode: full visibility: request_path: diff --git a/core/modules/config/tests/src/Functional/ConfigDraggableListBuilderTest.php b/core/modules/config/tests/src/Functional/ConfigDraggableListBuilderTest.php index 45e041758d6..38b60f247de 100644 --- a/core/modules/config/tests/src/Functional/ConfigDraggableListBuilderTest.php +++ b/core/modules/config/tests/src/Functional/ConfigDraggableListBuilderTest.php @@ -40,7 +40,7 @@ class ConfigDraggableListBuilderTest extends BrowserTestBase { $role->save(); } - // Navigate to Roles page + // Navigate to Roles page. $this->drupalGet('admin/people/roles'); // Test for the page title. diff --git a/core/modules/config/tests/src/Functional/ConfigImportUITest.php b/core/modules/config/tests/src/Functional/ConfigImportUITest.php index b4e4585e68d..bc1399ae561 100644 --- a/core/modules/config/tests/src/Functional/ConfigImportUITest.php +++ b/core/modules/config/tests/src/Functional/ConfigImportUITest.php @@ -327,7 +327,7 @@ class ConfigImportUITest extends BrowserTestBase { // Verify diff colors are displayed. $this->assertSession()->elementsCount('xpath', '//table[contains(@class, "diff")]', 1); - // Reset data back to original, and remove a key + // Reset data back to original, and remove a key. $sync_data = $original_data; unset($sync_data[$remove_key]); $sync->write($config_name, $sync_data); @@ -340,7 +340,7 @@ class ConfigImportUITest extends BrowserTestBase { // Removed key is escaped. $this->assertSession()->pageTextContains("404: '<em>herp</em>'"); - // Reset data back to original and add a key + // Reset data back to original and add a key. $sync_data = $original_data; $sync_data[$add_key] = $add_data; $sync->write($config_name, $sync_data); diff --git a/core/modules/config/tests/src/FunctionalJavascript/ConfigEntityTest.php b/core/modules/config/tests/src/FunctionalJavascript/ConfigEntityTest.php index 612a654bcf5..42f671b9311 100644 --- a/core/modules/config/tests/src/FunctionalJavascript/ConfigEntityTest.php +++ b/core/modules/config/tests/src/FunctionalJavascript/ConfigEntityTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\config\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the Config operations through the UI. - * - * @group config */ +#[Group('config')] class ConfigEntityTest extends WebDriverTestBase { /** diff --git a/core/modules/config/tests/src/FunctionalJavascript/ConfigExportTest.php b/core/modules/config/tests/src/FunctionalJavascript/ConfigExportTest.php index 2489d31ae95..31042be6c35 100644 --- a/core/modules/config/tests/src/FunctionalJavascript/ConfigExportTest.php +++ b/core/modules/config/tests/src/FunctionalJavascript/ConfigExportTest.php @@ -4,16 +4,18 @@ declare(strict_types=1); namespace Drupal\Tests\config\FunctionalJavascript; -use Drupal\block_content\Entity\BlockContent; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\Tests\block_content\Traits\BlockContentCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests the config export form. - * - * @group config */ +#[Group('config')] class ConfigExportTest extends WebDriverTestBase { + use BlockContentCreationTrait; + /** * {@inheritdoc} */ @@ -58,26 +60,6 @@ class ConfigExportTest extends WebDriverTestBase { } /** - * Creates test blocks. - * - * @param string $title - * Title of the block. - * - * @return \Drupal\block_content\Entity\BlockContent - * The created block content entity. - * - * @throws \Drupal\Core\Entity\EntityStorageException - */ - protected function createBlockContent($title) { - $block_content = BlockContent::create([ - 'info' => $title, - 'type' => 'basic', - ]); - $block_content->save(); - return $block_content; - } - - /** * Tests Ajax form functionality on the config export page. */ public function testAjaxOnExportPage(): void { diff --git a/core/modules/config/tests/src/FunctionalJavascript/ConfigImportUIAjaxTest.php b/core/modules/config/tests/src/FunctionalJavascript/ConfigImportUIAjaxTest.php index 61787d01db0..16c613c9fd4 100644 --- a/core/modules/config/tests/src/FunctionalJavascript/ConfigImportUIAjaxTest.php +++ b/core/modules/config/tests/src/FunctionalJavascript/ConfigImportUIAjaxTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\config\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the user interface for importing configuration. - * - * @group config */ +#[Group('config')] class ConfigImportUIAjaxTest extends WebDriverTestBase { /** diff --git a/core/modules/config_translation/tests/src/Functional/ConfigTranslationListUiTest.php b/core/modules/config_translation/tests/src/Functional/ConfigTranslationListUiTest.php index 230081cf9b3..0cf13d30231 100644 --- a/core/modules/config_translation/tests/src/Functional/ConfigTranslationListUiTest.php +++ b/core/modules/config_translation/tests/src/Functional/ConfigTranslationListUiTest.php @@ -511,7 +511,7 @@ class ConfigTranslationListUiTest extends BrowserTestBase { $this->doFieldListTest(); // Views is tested in - // Drupal\config_translation\Tests\ConfigTranslationViewListUiTest + // Drupal\config_translation\Tests\ConfigTranslationViewListUiTest. // Test the maintenance settings page. $this->doSettingsPageTest('admin/config/development/maintenance'); diff --git a/core/modules/config_translation/tests/src/Functional/ConfigTranslationUiTest.php b/core/modules/config_translation/tests/src/Functional/ConfigTranslationUiTest.php index a7fdd9aa872..371a5b046f9 100644 --- a/core/modules/config_translation/tests/src/Functional/ConfigTranslationUiTest.php +++ b/core/modules/config_translation/tests/src/Functional/ConfigTranslationUiTest.php @@ -253,7 +253,7 @@ class ConfigTranslationUiTest extends ConfigTranslationUiTestBase { public function testSingleLanguageUI(): void { $this->drupalLogin($this->adminUser); - // Delete French language + // Delete French language. $this->drupalGet('admin/config/regional/language/delete/fr'); $this->submitForm([], 'Delete'); $this->assertSession()->pageTextContains('The French (fr) language has been removed.'); @@ -266,7 +266,7 @@ class ConfigTranslationUiTest extends ConfigTranslationUiTestBase { $this->submitForm($edit, 'Save configuration'); $this->assertSession()->pageTextContains('Configuration saved.'); - // Delete English language + // Delete English language. $this->drupalGet('admin/config/regional/language/delete/en'); $this->submitForm([], 'Delete'); $this->assertSession()->pageTextContains('The English (en) language has been removed.'); diff --git a/core/modules/config_translation/tests/src/FunctionalJavascript/ConfigTranslationUiTest.php b/core/modules/config_translation/tests/src/FunctionalJavascript/ConfigTranslationUiTest.php index 5d8b08bc54d..2a26a7696f0 100644 --- a/core/modules/config_translation/tests/src/FunctionalJavascript/ConfigTranslationUiTest.php +++ b/core/modules/config_translation/tests/src/FunctionalJavascript/ConfigTranslationUiTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\config_translation\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\language\Entity\ConfigurableLanguage; +use PHPUnit\Framework\Attributes\Group; /** * Translate settings and entities to various languages. - * - * @group config_translation */ +#[Group('config_translation')] class ConfigTranslationUiTest extends WebDriverTestBase { /** diff --git a/core/modules/contact/tests/src/Functional/ContactSitewideTest.php b/core/modules/contact/tests/src/Functional/ContactSitewideTest.php index b3cba2bd1ba..260fdb95371 100644 --- a/core/modules/contact/tests/src/Functional/ContactSitewideTest.php +++ b/core/modules/contact/tests/src/Functional/ContactSitewideTest.php @@ -70,7 +70,7 @@ class ContactSitewideTest extends BrowserTestBase { // Ensure that there is no textfield for email. $this->assertSession()->fieldNotExists('mail'); - // Logout and retrieve the page as an anonymous user + // Logout and retrieve the page as an anonymous user. $this->drupalLogout(); user_role_grant_permissions('anonymous', ['access site-wide contact form']); $this->drupalGet('contact'); diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module deleted file mode 100644 index fdd55eaec3b..00000000000 --- a/core/modules/content_moderation/content_moderation.module +++ /dev/null @@ -1,16 +0,0 @@ -<?php - -/** - * @file - */ - -use Drupal\content_moderation\ContentPreprocess; - -/** - * Implements hook_preprocess_HOOK(). - */ -function content_moderation_preprocess_node(&$variables): void { - \Drupal::service('class_resolver') - ->getInstanceFromDefinition(ContentPreprocess::class) - ->preprocessNode($variables); -} diff --git a/core/modules/content_moderation/src/Hook/ContentModerationThemeHooks.php b/core/modules/content_moderation/src/Hook/ContentModerationThemeHooks.php new file mode 100644 index 00000000000..8b2c829dc1d --- /dev/null +++ b/core/modules/content_moderation/src/Hook/ContentModerationThemeHooks.php @@ -0,0 +1,21 @@ +<?php + +namespace Drupal\content_moderation\Hook; + +use Drupal\content_moderation\ContentPreprocess; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for content_moderation. + */ +class ContentModerationThemeHooks { + + /** + * Implements hook_preprocess_HOOK(). + */ + #[Hook('preprocess_node')] + public function preprocessNode(&$variables): void { + \Drupal::service('class_resolver')->getInstanceFromDefinition(ContentPreprocess::class)->preprocessNode($variables); + } + +} diff --git a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php index 4e8e77ff7da..5a130bf160c 100644 --- a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php +++ b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\content_moderation\ModerationInformation; use Drupal\content_moderation\StateTransitionValidationInterface; @@ -66,6 +67,8 @@ class ModerationStateWidget extends OptionsSelectWidget { * Field settings. * @param array $third_party_settings * Third party settings. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfoManager + * The element info manager. * @param \Drupal\Core\Session\AccountInterface $current_user * Current user service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager @@ -75,8 +78,8 @@ class ModerationStateWidget extends OptionsSelectWidget { * @param \Drupal\content_moderation\StateTransitionValidationInterface $validator * Moderation state transition validation service. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information, StateTransitionValidationInterface $validator) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $elementInfoManager, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information, StateTransitionValidationInterface $validator) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $this->entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; $this->moderationInformation = $moderation_information; @@ -93,6 +96,7 @@ class ModerationStateWidget extends OptionsSelectWidget { $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], + $container->get('plugin.manager.element_info'), $container->get('current_user'), $container->get('entity_type.manager'), $container->get('content_moderation.moderation_information'), diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationActionsTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationActionsTest.php index c801e08f4a0..81e44862d5e 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationActionsTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationActionsTest.php @@ -80,11 +80,12 @@ class ModerationActionsTest extends BrowserTestBase { ], 'Apply to selected items'); if ($warning_appears) { + $typeLabel = $node->getBundleEntity()->label(); if ($action == 'node_publish_action') { - $this->assertSession()->statusMessageContains(node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly published.', 'warning'); + $this->assertSession()->statusMessageContains($typeLabel . ' content items were skipped as they are under moderation and may not be directly published.', 'warning'); } else { - $this->assertSession()->statusMessageContains(node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly unpublished.', 'warning'); + $this->assertSession()->statusMessageContains($typeLabel . ' content items were skipped as they are under moderation and may not be directly unpublished.', 'warning'); } } else { diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 49b15bb022b..7749def59ff 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -159,11 +159,3 @@ function content_translation_language_configuration_element_submit(array $form, \Drupal::service('router.builder')->setRebuildNeeded(); } } - -/** - * Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig. - */ -function content_translation_preprocess_language_content_settings_table(&$variables): void { - \Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin'); - _content_translation_preprocess_language_content_settings_table($variables); -} diff --git a/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php b/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php index 5f1933714bc..476eaaaf72f 100644 --- a/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php +++ b/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php @@ -11,7 +11,6 @@ use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\language\Entity\ContentLanguageSettings; -use Drupal\workflows\Entity\Workflow; /** * Access check for entity translation deletion. @@ -83,13 +82,9 @@ class ContentTranslationDeleteAccess implements AccessInterface { $entity_type_id = $entity->getEntityTypeId(); $result->addCacheableDependency($entity); - // Add the cache dependencies used by - // ContentTranslationManager::isPendingRevisionSupportEnabled(). - if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { - foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { - $result->addCacheableDependency($workflow); - } - } + // The information about workflows is stored in entity bundle info, depend + // on that cache tag. + $result->addCacheTags(['entity_bundles']); if (!ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) { return $result; } diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php index ad21415ac0a..5ce09394849 100644 --- a/core/modules/content_translation/src/ContentTranslationHandler.php +++ b/core/modules/content_translation/src/ContentTranslationHandler.php @@ -487,7 +487,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E '#default_value' => User::load($uid), // Validation is done by static::entityFormValidate(). '#validate_reference' => FALSE, - '#maxlength' => 60, + '#maxlength' => 1024, '#description' => $this->t('Leave blank for %anonymous.', ['%anonymous' => \Drupal::config('user.settings')->get('anonymous')]), ]; diff --git a/core/modules/content_translation/src/ContentTranslationManager.php b/core/modules/content_translation/src/ContentTranslationManager.php index 56ac55cf7e7..0718a4cbf9d 100644 --- a/core/modules/content_translation/src/ContentTranslationManager.php +++ b/core/modules/content_translation/src/ContentTranslationManager.php @@ -3,7 +3,6 @@ namespace Drupal\content_translation; use Drupal\Core\Entity\EntityInterface; -use Drupal\workflows\Entity\Workflow; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -168,24 +167,13 @@ class ContentTranslationManager implements ContentTranslationManagerInterface, B return FALSE; } - foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { - /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */ - $plugin = $workflow->getTypePlugin(); - $entity_type_ids = array_flip($plugin->getEntityTypes()); - if (isset($entity_type_ids[$entity_type_id])) { - if (!isset($bundle_id)) { - return TRUE; - } - else { - $bundle_ids = array_flip($plugin->getBundlesForEntityType($entity_type_id)); - if (isset($bundle_ids[$bundle_id])) { - return TRUE; - } - } - } + $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); + if ($bundle_id) { + return \Drupal::service('content_moderation.moderation_information')->shouldModerateEntitiesOfBundle($entity_type, $bundle_id); + } + else { + return \Drupal::service('content_moderation.moderation_information')->canModerateEntitiesOfEntityType($entity_type); } - - return FALSE; } } diff --git a/core/modules/content_translation/src/Hook/ContentTranslationHooks.php b/core/modules/content_translation/src/Hook/ContentTranslationHooks.php index c681b07c059..9842c7085fd 100644 --- a/core/modules/content_translation/src/Hook/ContentTranslationHooks.php +++ b/core/modules/content_translation/src/Hook/ContentTranslationHooks.php @@ -10,7 +10,6 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\content_translation\ContentTranslationManager; use Drupal\content_translation\BundleTranslationSettingsInterface; use Drupal\language\ContentLanguageSettingsInterface; use Drupal\Core\Language\LanguageInterface; @@ -18,6 +17,7 @@ use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Order\Order; +use Drupal\workflows\Entity\Workflow; /** * Hook implementations for content_translation. @@ -231,11 +231,27 @@ class ContentTranslationHooks { $bundle_info['translatable'] = $content_translation_manager->isEnabled($entity_type_id, $bundle); if ($bundle_info['translatable'] && $content_translation_manager instanceof BundleTranslationSettingsInterface) { $settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle); - // If pending revision support is enabled for this bundle, we need to - // hide untranslatable field widgets, otherwise changes in pending - // revisions might be overridden by changes in later default - // revisions. - $bundle_info['untranslatable_fields.default_translation_affected'] = !empty($settings['untranslatable_fields_hide']) || ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle); + $bundle_info['untranslatable_fields.default_translation_affected'] = !empty($settings['untranslatable_fields_hide']); + } + } + } + + // Always hide untranslatable field widgets if pending revision support is + // enabled otherwise changes in pending + // revisions might be overridden by changes in later default revisions. + // This can't use + // Drupal\content_translation\ContentTranslationManager::isPendingRevisionSupportEnabled() + // since that depends on entity bundle information to be completely built. + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { + /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */ + $plugin = $workflow->getTypePlugin(); + foreach ($plugin->getEntityTypes() as $entity_type_id) { + foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) { + if (isset($bundles[$entity_type_id][$bundle_id])) { + $bundles[$entity_type_id][$bundle_id]['untranslatable_fields.default_translation_affected'] = TRUE; + } + } } } } diff --git a/core/modules/content_translation/src/Hook/ContentTranslationThemeHooks.php b/core/modules/content_translation/src/Hook/ContentTranslationThemeHooks.php new file mode 100644 index 00000000000..b8f91fcb098 --- /dev/null +++ b/core/modules/content_translation/src/Hook/ContentTranslationThemeHooks.php @@ -0,0 +1,21 @@ +<?php + +namespace Drupal\content_translation\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for content_translation. + */ +class ContentTranslationThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig. + */ + #[Hook('preprocess_language_content_settings_table')] + public function preprocessLanguageContentSettingsTable(&$variables): void { + \Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin'); + _content_translation_preprocess_language_content_settings_table($variables); + } + +} diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php index 65e4ea51654..44929204bc6 100644 --- a/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php @@ -12,6 +12,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; +use Drupal\user\UserInterface; /** * Tests the Content Translation UI. @@ -353,6 +354,18 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase { $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); $this->assertEquals($values[$langcode]['uid'], $metadata->getAuthor()->id(), 'Translation author correctly kept.'); $this->assertEquals($values[$langcode]['created'], $metadata->getCreatedTime(), 'Translation date correctly kept.'); + + // Verify that long usernames can be saved as the translation author. + $user = $this->drupalCreateUser([], $this->randomMachineName(UserInterface::USERNAME_MAX_LENGTH)); + $edit = [ + // Format the username as it is entered in autocomplete fields. + 'content_translation[uid]' => $user->getAccountName() . ' (' . $user->id() . ')', + 'content_translation[created]' => $this->container->get('date.formatter')->format($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'), + ]; + $this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode)); + $reloaded_entity = $storage->load($this->entityId); + $metadata = $this->manager->getTranslationMetadata($reloaded_entity->getTranslation($langcode)); + $this->assertEquals($user->id(), $metadata->getAuthor()->id(), 'Translation author correctly set.'); } /** diff --git a/core/modules/content_translation/tests/src/FunctionalJavascript/ContentTranslationConfigUITest.php b/core/modules/content_translation/tests/src/FunctionalJavascript/ContentTranslationConfigUITest.php index 4f4b7da3709..420e32b61a0 100644 --- a/core/modules/content_translation/tests/src/FunctionalJavascript/ContentTranslationConfigUITest.php +++ b/core/modules/content_translation/tests/src/FunctionalJavascript/ContentTranslationConfigUITest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\content_translation\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests that the content translation configuration javascript does't fail. - * - * @group content_translation */ +#[Group('content_translation')] class ContentTranslationConfigUITest extends WebDriverTestBase { /** diff --git a/core/modules/content_translation/tests/src/FunctionalJavascript/ContentTranslationContextualLinksTest.php b/core/modules/content_translation/tests/src/FunctionalJavascript/ContentTranslationContextualLinksTest.php index 8e11084b1f6..943f24def3c 100644 --- a/core/modules/content_translation/tests/src/FunctionalJavascript/ContentTranslationContextualLinksTest.php +++ b/core/modules/content_translation/tests/src/FunctionalJavascript/ContentTranslationContextualLinksTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\content_translation\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\language\Entity\ConfigurableLanguage; +use PHPUnit\Framework\Attributes\Group; /** * Tests that contextual links are available for content translation. - * - * @group content_translation */ +#[Group('content_translation')] class ContentTranslationContextualLinksTest extends WebDriverTestBase { /** diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php index 69a8855637d..b880c7124e1 100644 --- a/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php +++ b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\contextual\FunctionalJavascript; use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\user\Entity\Role; +use PHPUnit\Framework\Attributes\Group; /** * Tests the UI for correct contextual links. - * - * @group contextual */ +#[Group('contextual')] class ContextualLinksTest extends WebDriverTestBase { use ContextualLinkClickTrait; @@ -62,7 +62,7 @@ class ContextualLinksTest extends WebDriverTestBase { $contextualLinks = $this->assertSession()->waitForElement('css', '.contextual button'); $this->assertNotEmpty($contextualLinks); - // Confirm touchevents detection is loaded with Contextual Links + // Confirm touchevents detection is loaded with Contextual Links. $this->assertSession()->elementExists('css', 'html.no-touchevents'); // Ensure visibility remains correct after cached paged load. diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/DuplicateContextualLinksTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/DuplicateContextualLinksTest.php index 8549a9c06ff..a3f6e1d7fbc 100644 --- a/core/modules/contextual/tests/src/FunctionalJavascript/DuplicateContextualLinksTest.php +++ b/core/modules/contextual/tests/src/FunctionalJavascript/DuplicateContextualLinksTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\contextual\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the UI for correct contextual links. - * - * @group contextual */ +#[Group('contextual')] class DuplicateContextualLinksTest extends WebDriverTestBase { /** diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php index 1d4fa243c49..e0a7391d7f8 100644 --- a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php +++ b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php @@ -5,13 +5,13 @@ declare(strict_types=1); namespace Drupal\Tests\contextual\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests edit mode. - * - * @group contextual - * @group #slow */ +#[Group('contextual')] +#[Group('#slow')] class EditModeTest extends WebDriverTestBase { /** diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php index 9055d44982b..4fc259a760a 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php @@ -7,6 +7,7 @@ use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -31,8 +32,8 @@ class DateTimeDefaultWidget extends DateTimeWidgetBase { /** * {@inheritdoc} */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $elementInfoManager, EntityStorageInterface $date_storage) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $this->dateStorage = $date_storage; } @@ -47,7 +48,8 @@ class DateTimeDefaultWidget extends DateTimeWidgetBase { $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], - $container->get('entity_type.manager')->getStorage('date_format') + $container->get('plugin.manager.element_info'), + $container->get('entity_type.manager')->getStorage('date_format'), ); } diff --git a/core/modules/datetime/tests/src/Functional/DateFilterTest.php b/core/modules/datetime/tests/src/Functional/DateFilterTest.php index 87896594408..74283c0da8e 100644 --- a/core/modules/datetime/tests/src/Functional/DateFilterTest.php +++ b/core/modules/datetime/tests/src/Functional/DateFilterTest.php @@ -88,7 +88,8 @@ class DateFilterTest extends ViewTestBase { $this->drupalGet('test_exposed_filter_datetime'); $this->assertSession()->statusCodeEquals(200); - // Ensure that invalid date format entries in the exposed filter are validated + // Ensure that invalid date format entries in the exposed filter are + // validated. $edit = ['edit-field-date-value-value' => 'lun 2018-04-27']; $this->submitForm($edit, 'Apply'); $this->assertSession()->pageTextContains('Invalid date format.'); diff --git a/core/modules/datetime/tests/src/Functional/DateTestBase.php b/core/modules/datetime/tests/src/Functional/DateTestBase.php index b7399930273..04c8f4d7b20 100644 --- a/core/modules/datetime/tests/src/Functional/DateTestBase.php +++ b/core/modules/datetime/tests/src/Functional/DateTestBase.php @@ -64,7 +64,7 @@ abstract class DateTestBase extends BrowserTestBase { protected static $timezones = [ // UTC-12, no DST. 'Pacific/Kwajalein', - // UTC-11, no DST + // UTC-11, no DST. 'Pacific/Midway', // UTC-7, no DST. 'America/Phoenix', @@ -72,7 +72,7 @@ abstract class DateTestBase extends BrowserTestBase { 'UTC', // UTC+5:30, no DST. 'Asia/Kolkata', - // UTC+12, no DST + // UTC+12, no DST. 'Pacific/Funafuti', // UTC+13, no DST. 'Pacific/Tongatapu', diff --git a/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php index d6dee40b55e..ccffb9e71c1 100644 --- a/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php +++ b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php @@ -426,7 +426,7 @@ class DateTimeFieldTest extends DateTestBase { $this->assertSession()->elementExists('xpath', '//fieldset[@aria-describedby="edit-' . $field_name . '-0--description"]'); $this->assertSession()->elementExists('xpath', '//div[@id="edit-' . $field_name . '-0--description"]'); - // Assert that Hour and Minute Elements do not appear on Date Only + // Assert that Hour and Minute Elements do not appear on Date Only. $this->assertSession()->elementNotExists('xpath', "//*[@id=\"edit-$field_name-0-value-hour\"]"); $this->assertSession()->elementNotExists('xpath', "//*[@id=\"edit-$field_name-0-value-minute\"]"); diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php index 04c6f4eaf1c..b2f90d662e9 100644 --- a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php +++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php @@ -7,6 +7,7 @@ use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -31,8 +32,8 @@ class DateRangeDefaultWidget extends DateRangeWidgetBase { /** * {@inheritdoc} */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $elementInfoManager, EntityStorageInterface $date_storage) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $this->dateStorage = $date_storage; } @@ -47,6 +48,7 @@ class DateRangeDefaultWidget extends DateRangeWidgetBase { $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], + $container->get('plugin.manager.element_info'), $container->get('entity_type.manager')->getStorage('date_format') ); } diff --git a/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php b/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php index 207fe6a74d7..355b2c97098 100644 --- a/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php +++ b/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php @@ -9,12 +9,12 @@ use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests Daterange field. - * - * @group datetime */ +#[Group('datetime')] class DateRangeFieldTest extends WebDriverTestBase { /** diff --git a/core/modules/dblog/config/schema/dblog.schema.yml b/core/modules/dblog/config/schema/dblog.schema.yml index cd59a5dd891..32df3f56152 100644 --- a/core/modules/dblog/config/schema/dblog.schema.yml +++ b/core/modules/dblog/config/schema/dblog.schema.yml @@ -8,7 +8,6 @@ dblog.settings: type: integer label: 'Database log messages to keep' constraints: - Range: - min: 0 + PositiveOrZero: ~ constraints: FullyValidatable: ~ diff --git a/core/modules/editor/tests/src/FunctionalJavascript/EditorAdminTest.php b/core/modules/editor/tests/src/FunctionalJavascript/EditorAdminTest.php index 375a75030a9..c1dfc0732c1 100644 --- a/core/modules/editor/tests/src/FunctionalJavascript/EditorAdminTest.php +++ b/core/modules/editor/tests/src/FunctionalJavascript/EditorAdminTest.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace Drupal\Tests\editor\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore sulaco - /** - * @group editor + * Tests Editor Admin. */ +#[Group('editor')] class EditorAdminTest extends WebDriverTestBase { /** diff --git a/core/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php b/core/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php index eb5ce5deb9f..75014bcc505 100644 --- a/core/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php +++ b/core/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php @@ -510,7 +510,7 @@ xss:ex/*XSS*//*/*/pression(alert("XSS"))\'>', '<META http-equiv="refresh" content="text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">', ]; - // META with additional URL parameter + // META with additional URL parameter. // @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#META $data[] = [ '<META HTTP-EQUIV="refresh" CONTENT="0; URL=http://;URL=javascript:alert(\'XSS\');">', diff --git a/core/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php b/core/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php index a8edc9623ff..dd58152a90f 100644 --- a/core/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php +++ b/core/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php @@ -95,7 +95,7 @@ class FieldTestHooks { */ #[Hook('entity_extra_field_info_alter')] public function entityExtraFieldInfoAlter(&$info): void { - // Remove all extra fields from the 'no_fields' content type; + // Remove all extra fields from the 'no_fields' content type. unset($info['node']['no_fields']); } diff --git a/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php b/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php index 36957502013..dd9a4167ba5 100644 --- a/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php +++ b/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php @@ -9,6 +9,9 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\WidgetInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Render\Element\Number; +use Drupal\Core\Render\Element\Textfield; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; /** @@ -18,17 +21,17 @@ class FieldThirdPartyTestHooks { use StringTranslationTrait; + public function __construct(protected ElementInfoManagerInterface $elementInfoManager) {} + /** * Implements hook_field_widget_third_party_settings_form(). */ #[Hook('field_widget_third_party_settings_form')] public function fieldWidgetThirdPartySettingsForm(WidgetInterface $plugin, FieldDefinitionInterface $field_definition, $form_mode, $form, FormStateInterface $form_state): array { - $element['field_test_widget_third_party_settings_form'] = [ - '#type' => 'textfield', - '#title' => $this->t('3rd party widget settings form'), - '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'field_test_widget_third_party_settings_form'), - ]; - return $element; + $textfield = $this->elementInfoManager->fromClass(Textfield::class); + $textfield->title = $this->t('3rd party widget settings form'); + $textfield->default_value = $plugin->getThirdPartySetting('field_third_party_test', 'field_test_widget_third_party_settings_form'); + return $textfield->toRenderable('field_test_widget_third_party_settings_form'); } /** @@ -36,12 +39,10 @@ class FieldThirdPartyTestHooks { */ #[Hook('field_widget_third_party_settings_form')] public function fieldWidgetThirdPartySettingsFormAdditionalImplementation(WidgetInterface $plugin, FieldDefinitionInterface $field_definition, $form_mode, $form, FormStateInterface $form_state): array { - $element['second_field_widget_third_party_settings_form'] = [ - '#type' => 'number', - '#title' => $this->t('Second 3rd party widget settings form'), - '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'second_field_widget_third_party_settings_form'), - ]; - return $element; + $number = $this->elementInfoManager->fromClass(Number::class); + $number->title = $this->t('Second 3rd party widget settings form'); + $number->default_value = $plugin->getThirdPartySetting('field_third_party_test', 'second_field_widget_third_party_settings_form'); + return $number->toRenderable('second_field_widget_third_party_settings_form'); } /** @@ -57,12 +58,10 @@ class FieldThirdPartyTestHooks { */ #[Hook('field_formatter_third_party_settings_form')] public function fieldFormatterThirdPartySettingsForm(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, $view_mode, $form, FormStateInterface $form_state): array { - $element['field_test_field_formatter_third_party_settings_form'] = [ - '#type' => 'textfield', - '#title' => $this->t('3rd party formatter settings form'), - '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'field_test_field_formatter_third_party_settings_form'), - ]; - return $element; + $textfield = $this->elementInfoManager->fromClass(Textfield::class); + $textfield->title = $this->t('3rd party formatter settings form'); + $textfield->default_value = $plugin->getThirdPartySetting('field_third_party_test', 'field_test_field_formatter_third_party_settings_form'); + return $textfield->toRenderable('field_test_field_formatter_third_party_settings_form'); } /** @@ -70,12 +69,10 @@ class FieldThirdPartyTestHooks { */ #[Hook('field_formatter_third_party_settings_form')] public function fieldFormatterThirdPartySettingsFormAdditionalImplementation(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, $view_mode, $form, FormStateInterface $form_state): array { - $element['second_field_formatter_third_party_settings_form'] = [ - '#type' => 'number', - '#title' => $this->t('Second 3rd party formatter settings form'), - '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'second_field_formatter_third_party_settings_form'), - ]; - return $element; + $number = $this->elementInfoManager->fromClass(Number::class); + $number->title = $this->t('Second 3rd party formatter settings form'); + $number->default_value = $plugin->getThirdPartySetting('field_third_party_test', 'second_field_formatter_third_party_settings_form'); + return $number->toRenderable('second_field_formatter_third_party_settings_form'); } /** diff --git a/core/modules/field/tests/src/Functional/FormTest.php b/core/modules/field/tests/src/Functional/FormTest.php index e5c8838f353..c76864f10fe 100644 --- a/core/modules/field/tests/src/Functional/FormTest.php +++ b/core/modules/field/tests/src/Functional/FormTest.php @@ -138,7 +138,7 @@ class FormTest extends FieldTestBase { $this->assertSession()->pageTextContains("{$this->field['label']} does not accept the value -1."); // @todo check that the correct field is flagged for error. - // Create an entity + // Create an entity. $value = mt_rand(1, 127); $edit = [ "{$field_name}[0][value]" => $value, @@ -233,7 +233,7 @@ class FormTest extends FieldTestBase { $this->submitForm($edit, 'Save'); $this->assertSession()->pageTextContains("{$this->field['label']} field is required."); - // Create an entity + // Create an entity. $value = mt_rand(1, 127); $edit = [ "{$field_name}[0][value]" => $value, @@ -316,7 +316,7 @@ class FormTest extends FieldTestBase { $pattern[$weight] = "<input [^>]*value=\"$value\" [^>]*"; } - // Press 'add more' button -> 4 widgets + // Press 'add more' button -> 4 widgets. $this->submitForm($edit, 'Add another item'); for ($delta = 0; $delta <= $delta_range; $delta++) { $this->assertSession()->fieldValueEquals("{$field_name}[$delta][value]", $values[$delta]); @@ -347,7 +347,7 @@ class FormTest extends FieldTestBase { // Submit: check that the entity is updated with correct values // Re-submit: check that the field can be emptied. - // Test with several multiple fields in a form + // Test with several multiple fields in a form. } /** diff --git a/core/modules/field/tests/src/Functional/NestedFormTest.php b/core/modules/field/tests/src/Functional/NestedFormTest.php index 717d5f84b8c..2a59cd661fb 100644 --- a/core/modules/field/tests/src/Functional/NestedFormTest.php +++ b/core/modules/field/tests/src/Functional/NestedFormTest.php @@ -83,7 +83,7 @@ class NestedFormTest extends FieldTestBase { /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */ $display_repository = \Drupal::service('entity_display.repository'); - // Add two fields on the 'entity_test' + // Add two fields on the 'entity_test'. FieldStorageConfig::create($this->fieldStorageSingle)->save(); FieldStorageConfig::create($this->fieldStorageUnlimited)->save(); $this->field['field_name'] = 'field_single'; diff --git a/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php b/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php index 16321397808..531647fac27 100644 --- a/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php +++ b/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php @@ -197,7 +197,7 @@ class NumberFieldTest extends BrowserTestBase { $this->assertSession()->fieldValueEquals("{$field_name}[0][value]", ''); $this->assertSession()->responseContains('placeholder="4"'); - // Submit a valid integer + // Submit a valid integer. $value = rand($minimum, $maximum); $edit = [ "{$field_name}[0][value]" => $value, @@ -207,7 +207,7 @@ class NumberFieldTest extends BrowserTestBase { $id = $match[1]; $this->assertSession()->pageTextContains('entity_test ' . $id . ' has been created.'); - // Try to set a value below the minimum value + // Try to set a value below the minimum value. $this->drupalGet('entity_test/add'); $edit = [ "{$field_name}[0][value]" => $minimum - 1, @@ -215,7 +215,7 @@ class NumberFieldTest extends BrowserTestBase { $this->submitForm($edit, 'Save'); $this->assertSession()->pageTextContains("{$field_name} must be higher than or equal to {$minimum}."); - // Try to set a decimal value + // Try to set a decimal value. $this->drupalGet('entity_test/add'); $edit = [ "{$field_name}[0][value]" => 1.5, @@ -223,7 +223,7 @@ class NumberFieldTest extends BrowserTestBase { $this->submitForm($edit, 'Save'); $this->assertSession()->pageTextContains("{$field_name} is not a valid number."); - // Try to set a value above the maximum value + // Try to set a value above the maximum value. $this->drupalGet('entity_test/add'); $edit = [ "{$field_name}[0][value]" => $maximum + 1, diff --git a/core/modules/field/tests/src/FunctionalJavascript/Boolean/BooleanFormatterSettingsTest.php b/core/modules/field/tests/src/FunctionalJavascript/Boolean/BooleanFormatterSettingsTest.php index d85495c8e84..46597555851 100644 --- a/core/modules/field/tests/src/FunctionalJavascript/Boolean/BooleanFormatterSettingsTest.php +++ b/core/modules/field/tests/src/FunctionalJavascript/Boolean/BooleanFormatterSettingsTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\field\FunctionalJavascript\Boolean; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the Boolean field formatter settings. - * - * @group field */ +#[Group('field')] class BooleanFormatterSettingsTest extends WebDriverTestBase { /** diff --git a/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php b/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php index 76907277eae..688392289ae 100644 --- a/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php +++ b/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php @@ -6,17 +6,17 @@ namespace Drupal\Tests\field\FunctionalJavascript\EntityReference; use Behat\Mink\Element\NodeElement; use Drupal\Core\Url; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; +use PHPUnit\Framework\Attributes\Group; /** * Tests for the administrative UI. - * - * @group entity_reference */ +#[Group('entity_reference')] class EntityReferenceAdminTest extends WebDriverTestBase { use FieldUiTestTrait; diff --git a/core/modules/field/tests/src/FunctionalJavascript/MultipleValueWidgetTest.php b/core/modules/field/tests/src/FunctionalJavascript/MultipleValueWidgetTest.php index 442cbf40f1b..4128d510e6b 100644 --- a/core/modules/field/tests/src/FunctionalJavascript/MultipleValueWidgetTest.php +++ b/core/modules/field/tests/src/FunctionalJavascript/MultipleValueWidgetTest.php @@ -9,12 +9,12 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests widget form for a multiple value field. - * - * @group field */ +#[Group('field')] class MultipleValueWidgetTest extends WebDriverTestBase { /** diff --git a/core/modules/field/tests/src/FunctionalJavascript/Number/NumberFieldTest.php b/core/modules/field/tests/src/FunctionalJavascript/Number/NumberFieldTest.php index 9a9f95420e1..85997fa3dac 100644 --- a/core/modules/field/tests/src/FunctionalJavascript/Number/NumberFieldTest.php +++ b/core/modules/field/tests/src/FunctionalJavascript/Number/NumberFieldTest.php @@ -8,12 +8,12 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\node\Entity\Node; +use PHPUnit\Framework\Attributes\Group; /** * Tests the numeric field widget. - * - * @group field */ +#[Group('field')] class NumberFieldTest extends WebDriverTestBase { /** diff --git a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php index 056bf84c01e..1b283ab918a 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php @@ -193,7 +193,7 @@ class EntityReferenceItemTest extends FieldKernelTestBase { $this->assertInstanceOf(\InvalidArgumentException::class, $e); } - // Delete terms so we have nothing to reference and try again + // Delete terms so we have nothing to reference and try again. $term->delete(); $term2->delete(); $entity = EntityTest::create(['name' => $this->randomMachineName()]); @@ -302,7 +302,7 @@ class EntityReferenceItemTest extends FieldKernelTestBase { $this->assertEquals($vocabulary2->id(), $entity->field_test_taxonomy_vocabulary->entity->id()); $this->assertEquals($vocabulary2->label(), $entity->field_test_taxonomy_vocabulary->entity->label()); - // Delete terms so we have nothing to reference and try again + // Delete terms so we have nothing to reference and try again. $this->vocabulary->delete(); $vocabulary2->delete(); $entity = EntityTest::create(['name' => $this->randomMachineName()]); diff --git a/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php b/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php index 52e4c6b4cfc..2123c912264 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php @@ -280,7 +280,7 @@ class EntityReferenceRelationshipTest extends ViewsKernelTestBase { $this->assertEquals($this->entities[$index]->id(), $row->_entity->id()); // Test the forward relationship. - // $this->assertEquals(1, $row->entity_test_entity_test_mul__field_data_test_id); + // $this->assertEquals(1, $row->entity_test_entity_test_mul__field_data_test_id);. // Test that the correct relationship entity is on the row. $this->assertEquals(1, $row->_relationship_entities['field_test_data_with_a_long_name']->id()); diff --git a/core/modules/field/tests/src/Kernel/FieldAttachOtherTest.php b/core/modules/field/tests/src/Kernel/FieldAttachOtherTest.php index 175789b83ef..a7a72a49be2 100644 --- a/core/modules/field/tests/src/Kernel/FieldAttachOtherTest.php +++ b/core/modules/field/tests/src/Kernel/FieldAttachOtherTest.php @@ -270,11 +270,11 @@ class FieldAttachOtherTest extends FieldKernelTestBase { $this->assertEquals($this->fieldTestData->field->getLabel(), $form[$this->fieldTestData->field_name]['widget']['#title'], "First field's form title is {$this->fieldTestData->field->getLabel()}"); $this->assertEquals($this->fieldTestData->field_2->getLabel(), $form[$this->fieldTestData->field_name_2]['widget']['#title'], "Second field's form title is {$this->fieldTestData->field_2->getLabel()}"); for ($delta = 0; $delta < $this->fieldTestData->field_storage->getCardinality(); $delta++) { - // field_test_widget uses 'textfield' + // field_test_widget uses 'textfield'. $this->assertEquals('textfield', $form[$this->fieldTestData->field_name]['widget'][$delta]['value']['#type'], "First field's form delta {$delta} widget is textfield"); } for ($delta = 0; $delta < $this->fieldTestData->field_storage_2->getCardinality(); $delta++) { - // field_test_widget uses 'textfield' + // field_test_widget uses 'textfield'. $this->assertEquals('textfield', $form[$this->fieldTestData->field_name_2]['widget'][$delta]['value']['#type'], "Second field's form delta {$delta} widget is textfield"); } @@ -292,7 +292,7 @@ class FieldAttachOtherTest extends FieldKernelTestBase { $this->assertFalse(isset($form[$this->fieldTestData->field_name]), 'The first field does not exist in the form'); $this->assertEquals($this->fieldTestData->field_2->getLabel(), $form[$this->fieldTestData->field_name_2]['widget']['#title'], "Second field's form title is {$this->fieldTestData->field_2->getLabel()}"); for ($delta = 0; $delta < $this->fieldTestData->field_storage_2->getCardinality(); $delta++) { - // field_test_widget uses 'textfield' + // field_test_widget uses 'textfield'. $this->assertEquals('textfield', $form[$this->fieldTestData->field_name_2]['widget'][$delta]['value']['#type'], "Second field's form delta {$delta} widget is textfield"); } } diff --git a/core/modules/field/tests/src/Kernel/FieldAttachStorageTest.php b/core/modules/field/tests/src/Kernel/FieldAttachStorageTest.php index 9d6c428cf57..e18f2fa6544 100644 --- a/core/modules/field/tests/src/Kernel/FieldAttachStorageTest.php +++ b/core/modules/field/tests/src/Kernel/FieldAttachStorageTest.php @@ -241,18 +241,18 @@ class FieldAttachStorageTest extends FieldKernelTestBase { ->create(['type' => $this->fieldTestData->field->getTargetBundle()]); $vids = []; - // Create revision 0 + // Create revision 0. $values = $this->_generateTestFieldValues($cardinality); $entity->{$this->fieldTestData->field_name} = $values; $entity->save(); $vids[] = $entity->getRevisionId(); - // Create revision 1 + // Create revision 1. $entity->setNewRevision(); $entity->save(); $vids[] = $entity->getRevisionId(); - // Create revision 2 + // Create revision 2. $entity->setNewRevision(); $entity->save(); $vids[] = $entity->getRevisionId(); @@ -260,7 +260,7 @@ class FieldAttachStorageTest extends FieldKernelTestBase { $controller = $this->container->get('entity_type.manager')->getStorage($entity->getEntityTypeId()); $controller->resetCache(); - // Confirm each revision loads + // Confirm each revision loads. foreach ($vids as $vid) { $revision = $controller->loadRevision($vid); $this->assertCount($cardinality, $revision->{$this->fieldTestData->field_name}, "The test entity revision $vid has $cardinality values."); @@ -275,12 +275,12 @@ class FieldAttachStorageTest extends FieldKernelTestBase { $this->assertCount($cardinality, $revision->{$this->fieldTestData->field_name}, "The test entity revision $vid has $cardinality values."); } - // Confirm the current revision still loads + // Confirm the current revision still loads. $controller->resetCache(); $current = $controller->load($entity->id()); $this->assertCount($cardinality, $current->{$this->fieldTestData->field_name}, "The test entity current revision has $cardinality values."); - // Delete all field data, confirm nothing loads + // Delete all field data, confirm nothing loads. $entity->delete(); $controller->resetCache(); foreach ([0, 1, 2] as $vid) { @@ -333,7 +333,7 @@ class FieldAttachStorageTest extends FieldKernelTestBase { $this->fieldTestData->field_definition['bundle'] = $new_bundle; FieldConfig::create($this->fieldTestData->field_definition)->save(); - // Create a second field for the test bundle + // Create a second field for the test bundle. $field_name = $this->randomMachineName() . '_field_name'; $field_storage = [ 'field_name' => $field_name, @@ -352,7 +352,7 @@ class FieldAttachStorageTest extends FieldKernelTestBase { ]; FieldConfig::create($field)->save(); - // Save an entity with data for both fields + // Save an entity with data for both fields. $entity = $this->container->get('entity_type.manager') ->getStorage($entity_type) ->create(['type' => $this->fieldTestData->field->getTargetBundle()]); @@ -361,7 +361,7 @@ class FieldAttachStorageTest extends FieldKernelTestBase { $entity->{$field_name} = $this->_generateTestFieldValues(1); $entity = $this->entitySaveReload($entity); - // Verify the fields are present on load + // Verify the fields are present on load. $this->assertCount(4, $entity->{$this->fieldTestData->field_name}, 'First field got loaded'); $this->assertCount(1, $entity->{$field_name}, 'Second field got loaded'); @@ -373,7 +373,7 @@ class FieldAttachStorageTest extends FieldKernelTestBase { ->delete(); EntityTestHelper::deleteBundle($this->fieldTestData->field->getTargetBundle(), $entity_type); - // Verify no data gets loaded + // Verify no data gets loaded. $controller = $this->container->get('entity_type.manager')->getStorage($entity->getEntityTypeId()); $controller->resetCache(); $entity = $controller->load($entity->id()); diff --git a/core/modules/field/tests/src/Kernel/FieldCrudTest.php b/core/modules/field/tests/src/Kernel/FieldCrudTest.php index bf1e0cf5c57..cf948bdd06c 100644 --- a/core/modules/field/tests/src/Kernel/FieldCrudTest.php +++ b/core/modules/field/tests/src/Kernel/FieldCrudTest.php @@ -65,7 +65,7 @@ class FieldCrudTest extends FieldKernelTestBase { // - a full fledged $field structure, check that all the values are there // - a minimal $field structure, check all default values are set // defer actual $field comparison to a helper function, used for the two cases above, - // and for testUpdateField + // and for testUpdateField. /** * Tests the creation of a field. diff --git a/core/modules/field/tests/src/Kernel/FieldImportDeleteUninstallTest.php b/core/modules/field/tests/src/Kernel/FieldImportDeleteUninstallTest.php index d0928dc7540..d2030032bd5 100644 --- a/core/modules/field/tests/src/Kernel/FieldImportDeleteUninstallTest.php +++ b/core/modules/field/tests/src/Kernel/FieldImportDeleteUninstallTest.php @@ -90,7 +90,7 @@ class FieldImportDeleteUninstallTest extends FieldKernelTestBase { unset($core_extension['module']['telephone']); $sync->write('core.extension', $core_extension); - // Stage the field deletion + // Stage the field deletion. $sync->delete('field.storage.entity_test.field_test'); $sync->delete('field.field.entity_test.entity_test.field_test'); diff --git a/core/modules/field/tests/src/Kernel/FieldStorageCrudTest.php b/core/modules/field/tests/src/Kernel/FieldStorageCrudTest.php index 849dd240212..01521d89946 100644 --- a/core/modules/field/tests/src/Kernel/FieldStorageCrudTest.php +++ b/core/modules/field/tests/src/Kernel/FieldStorageCrudTest.php @@ -27,7 +27,7 @@ class FieldStorageCrudTest extends FieldKernelTestBase { // @todo Test creation with // - a full fledged $field structure, check that all the values are there // - a minimal $field structure, check all default values are set - // defer actual $field comparison to a helper function, used for the two cases above + // defer actual $field comparison to a helper function, used for the two cases above. /** * Tests the creation of a field storage. @@ -375,13 +375,13 @@ class FieldStorageCrudTest extends FieldKernelTestBase { $field = FieldConfig::load('entity_test.' . $field_definition['bundle'] . '.' . $field_definition['field_name']); $this->assertFalse($field->isDeleted()); - // Save an entity with data for the field + // Save an entity with data for the field. $entity = EntityTest::create(); $values[0]['value'] = mt_rand(1, 127); $entity->{$field_storage->getName()}->value = $values[0]['value']; $entity = $this->entitySaveReload($entity); - // Verify the field is present on load + // Verify the field is present on load. $this->assertCount(1, $entity->{$field_storage->getName()}, "Data in previously deleted field saves and loads correctly"); foreach ($values as $delta => $value) { $this->assertEquals($values[$delta]['value'], $entity->{$field_storage->getName()}[$delta]->value, "Data in previously deleted field saves and loads correctly"); diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php index 81631820ce6..4a5f7dbe89b 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php @@ -132,7 +132,7 @@ class MigrateFieldTest extends MigrateDrupal7TestBase { $field = FieldStorageConfig::load('node.field_term_entityreference'); $this->assertEquals('taxonomy_term', $field->getSetting('target_type')); - // Make sure that datetime fields get the right datetime_type setting + // Make sure that datetime fields get the right datetime_type setting. $field = FieldStorageConfig::load('node.field_date'); $this->assertEquals('datetime', $field->getSetting('datetime_type')); $field = FieldStorageConfig::load('node.field_date_without_time'); diff --git a/core/modules/field/tests/src/Kernel/Timestamp/TimestampFormatterTest.php b/core/modules/field/tests/src/Kernel/Timestamp/TimestampFormatterTest.php index 01131d84857..9ccbd336958 100644 --- a/core/modules/field/tests/src/Kernel/Timestamp/TimestampFormatterTest.php +++ b/core/modules/field/tests/src/Kernel/Timestamp/TimestampFormatterTest.php @@ -148,7 +148,7 @@ class TimestampFormatterTest extends KernelTestBase { foreach (range(1, 7) as $granularity) { $request_time = \Drupal::requestStack()->getCurrentRequest()->server->get('REQUEST_TIME'); - // Test a timestamp in the past + // Test a timestamp in the past. $value = $request_time - 87654321; $interval = \Drupal::service('date.formatter')->formatTimeDiffSince($value, ['granularity' => $granularity]); $expected = $interval . ' ago'; @@ -164,7 +164,7 @@ class TimestampFormatterTest extends KernelTestBase { $this->renderEntityFields($entity, $this->display); $this->assertRaw($expected); - // Test a timestamp in the future + // Test a timestamp in the future. $value = $request_time + 87654321; $interval = \Drupal::service('date.formatter')->formatTimeDiffUntil($value, ['granularity' => $granularity]); $expected = $interval . ' hence'; diff --git a/core/modules/field/tests/src/Kernel/Views/HandlerFieldFieldTest.php b/core/modules/field/tests/src/Kernel/Views/HandlerFieldFieldTest.php index 3b91b5cc916..b40e39e27e1 100644 --- a/core/modules/field/tests/src/Kernel/Views/HandlerFieldFieldTest.php +++ b/core/modules/field/tests/src/Kernel/Views/HandlerFieldFieldTest.php @@ -199,7 +199,7 @@ class HandlerFieldFieldTest extends KernelTestBase { $view->style_plugin->getField(4, $this->fieldStorages[4]->getName()); $view->destroy(); - // Test delta limit + offset + // Test delta limit + offset. $this->prepareView($view); $view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE; $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3; diff --git a/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php index d6ef689ad5a..8330f2a2acf 100644 --- a/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php +++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\field_layout\FunctionalJavascript; use Drupal\entity_test\Entity\EntityTest; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests using field layout for entity displays. - * - * @group field_layout */ +#[Group('field_layout')] class FieldLayoutTest extends WebDriverTestBase { /** diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module index 8904dd925e5..809328d9abc 100644 --- a/core/modules/field_ui/field_ui.module +++ b/core/modules/field_ui/field_ui.module @@ -5,6 +5,7 @@ */ use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Theme\ThemePreprocess; use Drupal\field_ui\FieldUI; /** @@ -18,16 +19,7 @@ use Drupal\field_ui\FieldUI; * rendered as a table. */ function template_preprocess_field_ui_table(&$variables): void { - template_preprocess_table($variables); -} - -/** - * Implements hook_preprocess_HOOK(). - */ -function field_ui_preprocess_form_element__new_storage_type(&$variables): void { - // Add support for a variant string so radios in the add field form can be - // programmatically distinguished. - $variables['variant'] = $variables['element']['#variant'] ?? NULL; + \Drupal::service(ThemePreprocess::class)->preprocessTable($variables); } /** diff --git a/core/modules/field_ui/src/Hook/FieldUiThemeHooks.php b/core/modules/field_ui/src/Hook/FieldUiThemeHooks.php new file mode 100644 index 00000000000..9edd1d038ec --- /dev/null +++ b/core/modules/field_ui/src/Hook/FieldUiThemeHooks.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\field_ui\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for field_ui. + */ +class FieldUiThemeHooks { + + /** + * Implements hook_preprocess_HOOK(). + */ + #[Hook('preprocess_form_element__new_storage_type')] + public function preprocessFormElementNewStorageType(&$variables): void { + // Add support for a variant string so radios in the add field form can be + // programmatically distinguished. + $variables['variant'] = $variables['element']['#variant'] ?? NULL; + } + +} diff --git a/core/modules/field_ui/tests/src/Functional/ManageDisplayTest.php b/core/modules/field_ui/tests/src/Functional/ManageDisplayTest.php index e247bdf12b0..cc2512cbe05 100644 --- a/core/modules/field_ui/tests/src/Functional/ManageDisplayTest.php +++ b/core/modules/field_ui/tests/src/Functional/ManageDisplayTest.php @@ -407,7 +407,7 @@ class ManageDisplayTest extends BrowserTestBase { $options[] = $option->getValue(); } - // Loops trough all the option groups + // Loops trough all the option groups. foreach ($element->optgroup as $optgroup) { $options = array_merge($this->getAllOptionsList($optgroup), $options); } diff --git a/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTestBase.php b/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTestBase.php index 360e1f8ffa6..c8c99edf4f8 100644 --- a/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTestBase.php +++ b/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTestBase.php @@ -15,7 +15,7 @@ use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the Field UI "Manage fields" screen. */ -class ManageFieldsFunctionalTestBase extends BrowserTestBase { +abstract class ManageFieldsFunctionalTestBase extends BrowserTestBase { use EntityReferenceFieldCreationTrait; use FieldUiTestTrait; diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/DefaultValueWidgetTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/DefaultValueWidgetTest.php index ade6ff5e8b0..1a5c809b2cf 100644 --- a/core/modules/field_ui/tests/src/FunctionalJavascript/DefaultValueWidgetTest.php +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/DefaultValueWidgetTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\field_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests the default value widget in Field UI. - * - * @group field_ui */ +#[Group('field_ui')] class DefaultValueWidgetTest extends WebDriverTestBase { use TaxonomyTestTrait; diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/DisplayModeBundleSelectionTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/DisplayModeBundleSelectionTest.php index 7908418d3fb..dc170c5ae35 100644 --- a/core/modules/field_ui/tests/src/FunctionalJavascript/DisplayModeBundleSelectionTest.php +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/DisplayModeBundleSelectionTest.php @@ -6,12 +6,13 @@ namespace Drupal\Tests\field_ui\FunctionalJavascript; use Drupal\Core\Entity\Entity\EntityFormMode; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; /** * Tests the bundle selection for view & form display modes. - * - * @group field_ui */ +#[Group('field_ui')] class DisplayModeBundleSelectionTest extends WebDriverTestBase { /** @@ -66,9 +67,8 @@ class DisplayModeBundleSelectionTest extends WebDriverTestBase { * Display mode path. * @param string $custom_mode * Custom mode to test. - * - * @dataProvider providerBundleSelection */ + #[DataProvider('providerBundleSelection')] public function testBundleSelection($display_mode, $path, $custom_mode): void { $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php index fa6cee74261..640cb9e2842 100644 --- a/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\field_ui\FunctionalJavascript; use Drupal\entity_test\Entity\EntityTest; use Drupal\entity_test\EntityTestHelper; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the UI for entity displays. - * - * @group field_ui */ +#[Group('field_ui')] class EntityDisplayTest extends WebDriverTestBase { /** diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php index 4b6162abfc6..c5a13e49bd0 100644 --- a/core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php @@ -8,14 +8,13 @@ use Behat\Mink\Element\NodeElement; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore onewidgetfield - /** * Tests the Field UI "Manage display" and "Manage form display" screens. - * - * @group field_ui */ +#[Group('field_ui')] class ManageDisplayTest extends WebDriverTestBase { use FieldUiJSTestTrait; diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/ManageFieldsTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/ManageFieldsTest.php index 8ca2dbb9d83..ed8e1e4c2d7 100644 --- a/core/modules/field_ui/tests/src/FunctionalJavascript/ManageFieldsTest.php +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/ManageFieldsTest.php @@ -8,14 +8,13 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore horserad - /** * Tests the Field UI "Manage Fields" screens. - * - * @group field_ui */ +#[Group('field_ui')] class ManageFieldsTest extends WebDriverTestBase { use FieldUiJSTestTrait; diff --git a/core/modules/file/src/Hook/TokenHooks.php b/core/modules/file/src/Hook/TokenHooks.php index 39fbddfc646..8abaa6567fc 100644 --- a/core/modules/file/src/Hook/TokenHooks.php +++ b/core/modules/file/src/Hook/TokenHooks.php @@ -47,7 +47,7 @@ class TokenHooks { $replacements[$original] = $file->uuid(); break; - // Essential file data + // Essential file data. case 'name': $replacements[$original] = $file->getFilename(); break; diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php index a38e64ab53c..bb4268ff389 100644 --- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php +++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php @@ -29,16 +29,10 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; class FileWidget extends WidgetBase { /** - * The element info manager. - */ - protected ElementInfoManagerInterface $elementInfo; - - /** * {@inheritdoc} */ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); - $this->elementInfo = $element_info; + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $element_info); } /** @@ -238,7 +232,7 @@ class FileWidget extends WidgetBase { // Essentially we use the managed_file type, extended with some // enhancements. - $element_info = $this->elementInfo->getInfo('managed_file'); + $element_info = $this->elementInfoManager->getInfo('managed_file'); $element += [ '#type' => 'managed_file', '#upload_location' => $items[$delta]->getUploadLocation(), diff --git a/core/modules/file/tests/file_deprecated_test/file_deprecated_test.module b/core/modules/file/tests/file_deprecated_test/file_deprecated_test.module deleted file mode 100644 index 625f5ee9dbb..00000000000 --- a/core/modules/file/tests/file_deprecated_test/file_deprecated_test.module +++ /dev/null @@ -1,35 +0,0 @@ -<?php - -/** - * @file - * Support module for testing deprecated file features. - */ - -declare(strict_types=1); - -// cspell:ignore garply tarz - -/** - * Implements hook_file_mimetype_mapping_alter(). - * - * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Kept only for BC test coverage, see \Drupal\KernelTests\Core\File\MimeTypeLegacyTest. - * - * @see https://www.drupal.org/node/3494040 - */ -function file_deprecated_test_file_mimetype_mapping_alter(&$mapping): void { - // Add new mappings. - $mapping['mimetypes']['file_test_mimetype_1'] = 'made_up/file_test_1'; - $mapping['mimetypes']['file_test_mimetype_2'] = 'made_up/file_test_2'; - $mapping['mimetypes']['file_test_mimetype_3'] = 'made_up/doc'; - $mapping['mimetypes']['application-x-compress'] = 'application/x-compress'; - $mapping['mimetypes']['application-x-tarz'] = 'application/x-tarz'; - $mapping['mimetypes']['application-x-garply-waldo'] = 'application/x-garply-waldo'; - $mapping['extensions']['file_test_1'] = 'file_test_mimetype_1'; - $mapping['extensions']['file_test_2'] = 'file_test_mimetype_2'; - $mapping['extensions']['file_test_3'] = 'file_test_mimetype_2'; - $mapping['extensions']['z'] = 'application-x-compress'; - $mapping['extensions']['tar.z'] = 'application-x-tarz'; - $mapping['extensions']['garply.waldo'] = 'application-x-garply-waldo'; - // Override existing mapping. - $mapping['extensions']['doc'] = 'file_test_mimetype_3'; -} diff --git a/core/modules/file/tests/file_deprecated_test/src/Hook/FileDeprecatedTestThemeHooks.php b/core/modules/file/tests/file_deprecated_test/src/Hook/FileDeprecatedTestThemeHooks.php new file mode 100644 index 00000000000..a25027b687a --- /dev/null +++ b/core/modules/file/tests/file_deprecated_test/src/Hook/FileDeprecatedTestThemeHooks.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\file_deprecated_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for file_deprecated_test. + */ +class FileDeprecatedTestThemeHooks { + // cspell:ignore garply tarz + + /** + * Implements hook_file_mimetype_mapping_alter(). + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Kept only for BC test coverage, see \Drupal\KernelTests\Core\File\MimeTypeLegacyTest. + * + * @see https://www.drupal.org/node/3494040 + */ + #[Hook('file_mimetype_mapping_alter')] + public function fileMimetypeMappingAlter(&$mapping): void { + // Add new mappings. + $mapping['mimetypes']['file_test_mimetype_1'] = 'made_up/file_test_1'; + $mapping['mimetypes']['file_test_mimetype_2'] = 'made_up/file_test_2'; + $mapping['mimetypes']['file_test_mimetype_3'] = 'made_up/doc'; + $mapping['mimetypes']['application-x-compress'] = 'application/x-compress'; + $mapping['mimetypes']['application-x-tarz'] = 'application/x-tarz'; + $mapping['mimetypes']['application-x-garply-waldo'] = 'application/x-garply-waldo'; + $mapping['extensions']['file_test_1'] = 'file_test_mimetype_1'; + $mapping['extensions']['file_test_2'] = 'file_test_mimetype_2'; + $mapping['extensions']['file_test_3'] = 'file_test_mimetype_2'; + $mapping['extensions']['z'] = 'application-x-compress'; + $mapping['extensions']['tar.z'] = 'application-x-tarz'; + $mapping['extensions']['garply.waldo'] = 'application-x-garply-waldo'; + // Override existing mapping. + $mapping['extensions']['doc'] = 'file_test_mimetype_3'; + } + +} diff --git a/core/modules/file/tests/file_test/src/FileTestHelper.php b/core/modules/file/tests/file_test/src/FileTestHelper.php index 9a364110415..9ff5f908690 100644 --- a/core/modules/file/tests/file_test/src/FileTestHelper.php +++ b/core/modules/file/tests/file_test/src/FileTestHelper.php @@ -19,7 +19,7 @@ class FileTestHelper { * @see Drupal\file_test\FileTestHelper::reset() */ public static function reset(): void { - // Keep track of calls to these hooks + // Keep track of calls to these hooks. $results = [ 'load' => [], 'validate' => [], diff --git a/core/modules/file/tests/file_test/src/Form/FileRequiredTestForm.php b/core/modules/file/tests/file_test/src/Form/FileRequiredTestForm.php index 1491510fd64..4bdcf5455f9 100644 --- a/core/modules/file/tests/file_test/src/Form/FileRequiredTestForm.php +++ b/core/modules/file/tests/file_test/src/Form/FileRequiredTestForm.php @@ -14,14 +14,14 @@ class FileRequiredTestForm extends FileTestForm { /** * {@inheritdoc} */ - public function getFormId() { + public function getFormId(): string { return '_file_required_test_form'; } /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state) { + public function buildForm(array $form, FormStateInterface $form_state): array { $form = parent::buildForm($form, $form_state); $form['file_test_upload']['#required'] = TRUE; return $form; diff --git a/core/modules/file/tests/file_test/src/Form/FileTestForm.php b/core/modules/file/tests/file_test/src/Form/FileTestForm.php index d021a538f44..902c675cdcf 100644 --- a/core/modules/file/tests/file_test/src/Form/FileTestForm.php +++ b/core/modules/file/tests/file_test/src/Form/FileTestForm.php @@ -6,28 +6,28 @@ namespace Drupal\file_test\Form; use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; -use Drupal\Core\Form\FormInterface; +use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; /** * File test form class. */ -class FileTestForm implements FormInterface { +class FileTestForm extends FormBase { use FileTestFormTrait; use StringTranslationTrait; /** * {@inheritdoc} */ - public function getFormId() { + public function getFormId(): string { return '_file_test_form'; } /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state) { + public function buildForm(array $form, FormStateInterface $form_state): array { $form = $this->baseForm($form, $form_state); @@ -42,12 +42,7 @@ class FileTestForm implements FormInterface { /** * {@inheritdoc} */ - public function validateForm(array &$form, FormStateInterface $form_state) {} - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { + public function submitForm(array &$form, FormStateInterface $form_state): void { // Process the upload and perform validation. Note: we're using the // form value for the $replace parameter. if (!$form_state->isValueEmpty('file_subdir')) { diff --git a/core/modules/file/tests/src/FunctionalJavascript/AjaxFileManagedMultipleTest.php b/core/modules/file/tests/src/FunctionalJavascript/AjaxFileManagedMultipleTest.php index e7fb4e3e701..a0f9f727dfe 100644 --- a/core/modules/file/tests/src/FunctionalJavascript/AjaxFileManagedMultipleTest.php +++ b/core/modules/file/tests/src/FunctionalJavascript/AjaxFileManagedMultipleTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\file\FunctionalJavascript; use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests ajax upload to managed files. - * - * @group file */ +#[Group('file')] class AjaxFileManagedMultipleTest extends WebDriverTestBase { use TestFileCreationTrait { diff --git a/core/modules/file/tests/src/FunctionalJavascript/FileFieldValidateTest.php b/core/modules/file/tests/src/FunctionalJavascript/FileFieldValidateTest.php index 724b80db8e3..983e828effc 100644 --- a/core/modules/file/tests/src/FunctionalJavascript/FileFieldValidateTest.php +++ b/core/modules/file/tests/src/FunctionalJavascript/FileFieldValidateTest.php @@ -7,15 +7,15 @@ namespace Drupal\Tests\file\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\file\Functional\FileFieldCreationTrait; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests file field validation functions. * * Values validated include the file type, max file size, max size per node, * and whether the field is required. - * - * @group file */ +#[Group('file')] class FileFieldValidateTest extends WebDriverTestBase { use FileFieldCreationTrait; diff --git a/core/modules/file/tests/src/FunctionalJavascript/FileFieldWidgetClaroThemeTest.php b/core/modules/file/tests/src/FunctionalJavascript/FileFieldWidgetClaroThemeTest.php index f8c6e315d22..08640330942 100644 --- a/core/modules/file/tests/src/FunctionalJavascript/FileFieldWidgetClaroThemeTest.php +++ b/core/modules/file/tests/src/FunctionalJavascript/FileFieldWidgetClaroThemeTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\file\FunctionalJavascript; use Drupal\Core\Url; +use PHPUnit\Framework\Attributes\Group; /** * Tests the widget visibility settings for the Claro theme. @@ -13,9 +14,8 @@ use Drupal\Core\Url; * the changes added in _claro_preprocess_file_and_image_widget(). * * @see _claro_preprocess_file_and_image_widget() - * - * @group file */ +#[Group('file')] class FileFieldWidgetClaroThemeTest extends FileFieldWidgetTest { /** diff --git a/core/modules/file/tests/src/FunctionalJavascript/FileFieldWidgetTest.php b/core/modules/file/tests/src/FunctionalJavascript/FileFieldWidgetTest.php index ac5f8fc750e..a545cb9ddaf 100644 --- a/core/modules/file/tests/src/FunctionalJavascript/FileFieldWidgetTest.php +++ b/core/modules/file/tests/src/FunctionalJavascript/FileFieldWidgetTest.php @@ -9,12 +9,12 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; use Drupal\Tests\file\Functional\FileFieldCreationTrait; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests the file field widget, single and multi-valued, using AJAX upload. - * - * @group file */ +#[Group('file')] class FileFieldWidgetTest extends WebDriverTestBase { use FieldUiTestTrait; diff --git a/core/modules/file/tests/src/FunctionalJavascript/FileManagedFileElementTest.php b/core/modules/file/tests/src/FunctionalJavascript/FileManagedFileElementTest.php index 8924703c8fd..7a0119cd6fc 100644 --- a/core/modules/file/tests/src/FunctionalJavascript/FileManagedFileElementTest.php +++ b/core/modules/file/tests/src/FunctionalJavascript/FileManagedFileElementTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\file\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the 'managed_file' element type. - * - * @group file */ +#[Group('file')] class FileManagedFileElementTest extends WebDriverTestBase { /** diff --git a/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php b/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php index 41bbae0b789..a32014c8492 100644 --- a/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php +++ b/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php @@ -6,14 +6,14 @@ namespace Drupal\Tests\file\FunctionalJavascript; use Drupal\Component\Utility\Bytes; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; -use Drupal\Tests\TestFileCreationTrait; use Drupal\Tests\file\Functional\FileFieldCreationTrait; +use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests uploading a file that exceeds the maximum file size. - * - * @group file */ +#[Group('file')] class MaximumFileSizeExceededUploadTest extends WebDriverTestBase { use FileFieldCreationTrait; diff --git a/core/modules/file/tests/src/Kernel/DeleteTest.php b/core/modules/file/tests/src/Kernel/DeleteTest.php index 95139ef26f9..6afeae8cca6 100644 --- a/core/modules/file/tests/src/Kernel/DeleteTest.php +++ b/core/modules/file/tests/src/Kernel/DeleteTest.php @@ -74,7 +74,7 @@ class DeleteTest extends FileManagedUnitTestBase { ->execute(); \Drupal::service('cron')->run(); - // file_cron() loads + // file_cron() loads. $this->assertFileHooksCalled(['delete']); $this->assertFileDoesNotExist($file->getFileUri()); $this->assertNull(File::load($file->id()), 'File was removed from the database.'); diff --git a/core/modules/file/tests/src/Kernel/FileItemTest.php b/core/modules/file/tests/src/Kernel/FileItemTest.php index c01cf28a115..39c285bb428 100644 --- a/core/modules/file/tests/src/Kernel/FileItemTest.php +++ b/core/modules/file/tests/src/Kernel/FileItemTest.php @@ -89,7 +89,7 @@ class FileItemTest extends FieldKernelTestBase { $handler_id = $field_definition->getSetting('handler'); $this->assertEquals('default:file', $handler_id); - // Create a test entity with the + // Create a test entity with the test file field. $entity = EntityTest::create(); $entity->file_test->target_id = $this->file->id(); $entity->file_test->display = 1; diff --git a/core/modules/file/tests/src/Kernel/SpaceUsedTest.php b/core/modules/file/tests/src/Kernel/SpaceUsedTest.php index 0f3dedf8fb4..c2e7bbd9faf 100644 --- a/core/modules/file/tests/src/Kernel/SpaceUsedTest.php +++ b/core/modules/file/tests/src/Kernel/SpaceUsedTest.php @@ -68,7 +68,7 @@ class SpaceUsedTest extends FileManagedUnitTestBase { $this->assertEquals(300, $file->spaceUsed(3)); $this->assertEquals(370, $file->spaceUsed()); - // Test the status fields + // Test the status fields. $this->assertEquals(4, $file->spaceUsed(NULL, 0)); $this->assertEquals(370, $file->spaceUsed()); diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index 38da47a437e..4c5e3f7a0ac 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -514,7 +514,7 @@ function _filter_url($text, $filter) { $text = implode($chunks); } - // Revert to the original comment contents + // Revert to the original comment contents. _filter_url_escape_comments('', FALSE); $text = $text ? preg_replace_callback('`<!--(.*?)-->`', '_filter_url_escape_comments', $text) : $text; } @@ -630,14 +630,14 @@ function _filter_url_trim($text, $length = NULL) { * Based on: http://photomatt.net/scripts/autop */ function _filter_autop($text) { - // All block level tags + // All block level tags. $block = '(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|option|form|map|area|blockquote|address|math|input|p|h[1-6]|fieldset|legend|hr|article|aside|details|figcaption|figure|footer|header|hgroup|menu|nav|section|summary)'; // Split at opening and closing PRE, SCRIPT, STYLE, OBJECT, IFRAME tags // and comments. We don't apply any processing to the contents of these tags // to avoid messing up code. We look for matched pairs and allow basic // nesting. For example: - // "processed<pre>ignored<script>ignored</script>ignored</pre>processed" + // "processed<pre>ignored<script>ignored</script>ignored</pre>processed". $chunks = preg_split('@(<!--.*?-->|</?(?:pre|script|style|object|iframe|drupal-media|svg|!--)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); // Note: PHP ensures the array consists of alternating delimiters and literals // and begins and ends with a literal (inserting NULL as required). @@ -688,28 +688,28 @@ function _filter_autop($text) { } } - // Just to make things a little easier, pad the end + // Just to make things a little easier, pad the end. $chunk = preg_replace('|\n*$|', '', $chunk) . "\n\n"; $chunk = preg_replace('|<br />\s*<br />|', "\n\n", $chunk); - // Space things out a little + // Space things out a little. $chunk = preg_replace('!(<' . $block . '[^>]*>)!', "\n$1", $chunk); - // Space things out a little + // Space things out a little. $chunk = preg_replace('!(</' . $block . '>)!', "$1\n\n", $chunk); - // Take care of duplicates + // Take care of duplicates. $chunk = preg_replace("/\n\n+/", "\n\n", $chunk); $chunk = preg_replace('/^\n|\n\s*\n$/', '', $chunk); - // Make paragraphs, including one at the end + // Make paragraphs, including one at the end. $chunk = '<p>' . preg_replace('/\n\s*\n\n?(.)/', "</p>\n<p>$1", $chunk) . "</p>\n"; - // Problem with nested lists + // Problem with nested lists. $chunk = preg_replace("|<p>(<li.+?)</p>|", "$1", $chunk); $chunk = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $chunk); $chunk = str_replace('</blockquote></p>', '</p></blockquote>', $chunk); // Under certain strange conditions it could create a P of entirely - // whitespace + // whitespace. $chunk = preg_replace('|<p>\s*</p>\n?|', '', $chunk); $chunk = preg_replace('!<p>\s*(</?' . $block . '[^>]*>)!', "$1", $chunk); $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*</p>!', "$1", $chunk); - // Make line breaks + // Make line breaks. $chunk = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $chunk); $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*<br />!', "$1", $chunk); $chunk = preg_replace('!<br />(\s*</?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)!', '$1', $chunk); diff --git a/core/modules/filter/src/Element/TextFormat.php b/core/modules/filter/src/Element/TextFormat.php index 38ca133e3cd..6b1fc25c3ff 100644 --- a/core/modules/filter/src/Element/TextFormat.php +++ b/core/modules/filter/src/Element/TextFormat.php @@ -12,11 +12,15 @@ use Drupal\Core\Url; * Provides a text format render element. * * Properties: - * - #base_type: The form element #type to use for the 'value' element. + * + * @property $base_type + * The form element #type to use for the 'value' element. * 'textarea' by default. - * - #format: (optional) The text format ID to preselect. If omitted, the + * @property $format + * (optional) The text format ID to preselect. If omitted, the * default format for the current user will be used. - * - #allowed_formats: (optional) An array of text format IDs that are available + * @property $allowed_formats + * (optional) An array of text format IDs that are available * for this element. If omitted, all text formats that the current user has * access to will be allowed. * diff --git a/core/modules/filter/src/FilterFormatFormBase.php b/core/modules/filter/src/FilterFormatFormBase.php index 7e694188e4d..f78c195ed4f 100644 --- a/core/modules/filter/src/FilterFormatFormBase.php +++ b/core/modules/filter/src/FilterFormatFormBase.php @@ -86,7 +86,7 @@ abstract class FilterFormatFormBase extends EntityForm { // Filter order (tabledrag). $form['filters']['order'] = [ '#type' => 'table', - // For filter.admin.js + // For filter.admin.js. '#attributes' => ['id' => 'filter-order'], '#title' => $this->t('Filter processing order'), '#tabledrag' => [ diff --git a/core/modules/filter/src/Plugin/migrate/process/FilterID.php b/core/modules/filter/src/Plugin/migrate/process/FilterID.php index f88c1fa830b..4981a4b4264 100644 --- a/core/modules/filter/src/Plugin/migrate/process/FilterID.php +++ b/core/modules/filter/src/Plugin/migrate/process/FilterID.php @@ -114,438 +114,216 @@ class FilterID extends StaticMap implements ContainerFactoryPluginInterface { * @see \Drupal\filter\Plugin\FilterInterface::getType() */ protected static function getSourceFilterType($filter_id) { - switch ($filter_id) { - // Drupal 7 core filters. - // - https://git.drupalcode.org/project/drupal/blob/7.69/modules/filter/filter.module#L1229 - // - https://git.drupalcode.org/project/drupal/blob/7.69/modules/php/php.module#L139 - case 'filter_html': - return FilterInterface::TYPE_HTML_RESTRICTOR; - - case 'filter_url': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - - case 'filter_autop': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - - case 'filter_htmlcorrector': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - - case 'filter_html_escape': - return FilterInterface::TYPE_HTML_RESTRICTOR; - - case 'php_code': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - + // Drupal 7 core filters. + // - https://git.drupalcode.org/project/drupal/blob/7.69/modules/filter/filter.module#L1229 + // - https://git.drupalcode.org/project/drupal/blob/7.69/modules/php/php.module#L139 + return match ($filter_id) { + 'filter_html' => FilterInterface::TYPE_HTML_RESTRICTOR, + 'filter_url' => FilterInterface::TYPE_MARKUP_LANGUAGE, + 'filter_autop' => FilterInterface::TYPE_MARKUP_LANGUAGE, + 'filter_htmlcorrector' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, + 'filter_html_escape' => FilterInterface::TYPE_HTML_RESTRICTOR, + 'php_code' => FilterInterface::TYPE_MARKUP_LANGUAGE, // Drupal 7 contrib filters. // https://www.drupal.org/project/abbrfilter - case 'abbrfilter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'abbrfilter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/ace_editor - case 'ace_editor': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'ace_editor' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/adsense - case 'adsense': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'adsense' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/api - case 'api_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'api_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/api_tokens - case 'api_tokens': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'api_tokens' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/autofloat - case 'filter_autofloat': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_autofloat' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/bbcode - case 'bbcode': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - + 'bbcode' => FilterInterface::TYPE_MARKUP_LANGUAGE, // https://www.drupal.org/project/biblio - case 'biblio_filter_reference': - case 'biblio_filter_inline_reference': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'biblio_filter_reference', 'biblio_filter_inline_reference' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/caption - case 'caption': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'caption' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/caption_filter - case 'caption_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'caption_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/cincopa - case 'filter_cincopa': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_cincopa' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/ckeditor_blocks - case 'ckeditor_blocks': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'ckeditor_blocks' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/ckeditor_filter - case 'ckeditor_filter': - return FilterInterface::TYPE_HTML_RESTRICTOR; - + 'ckeditor_filter' => FilterInterface::TYPE_HTML_RESTRICTOR, // https://www.drupal.org/project/ckeditor_link - case 'ckeditor_link_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'ckeditor_link_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/ckeditor_swf - case 'ckeditor_swf_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'ckeditor_swf_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/codefilter - case 'codefilter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'codefilter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/collapse_text - case 'collapse_text_filter': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'collapse_text_filter' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/columns_filter - case 'columns_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'columns_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/commonmark - case 'commonmark': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - + 'commonmark' => FilterInterface::TYPE_MARKUP_LANGUAGE, // https://www.drupal.org/project/commons_hashtags - case 'filter_hashtags': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_hashtags' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/deepzoom - case 'deepzoom': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'deepzoom' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/editor - case 'editor_align': - case 'editor_caption': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'editor_align', 'editor_caption' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/elf - case 'elf': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'elf' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/emogrifier - case 'filter_emogrifier': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_emogrifier' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/emptyparagraphkiller - case 'emptyparagraphkiller': - return FilterInterface::TYPE_HTML_RESTRICTOR; - + 'emptyparagraphkiller' => FilterInterface::TYPE_HTML_RESTRICTOR, // https://www.drupal.org/project/entity_embed - case 'entity_embed': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - - case 'filter_align': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'entity_embed' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, + 'filter_align' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/ext_link_page - case 'ext_link_page': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'ext_link_page' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/filter_html_image_secure - case 'filter_html_image_secure': - return FilterInterface::TYPE_HTML_RESTRICTOR; - + 'filter_html_image_secure' => FilterInterface::TYPE_HTML_RESTRICTOR, // https://www.drupal.org/project/filter_transliteration - case 'filter_transliteration': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_transliteration' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/flickr - case 'flickr_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'flickr_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/float_filter - case 'float_filter': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'float_filter' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/footnotes - case 'filter_footnotes': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_footnotes' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/forena - case 'forena_report': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'forena_report' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/g2 - case 'filter_g2': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_g2' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/geo_filter - case 'geo_filter_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'geo_filter_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/google_analytics_counter - case 'filter_google_analytics_counter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_google_analytics_counter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/google_analytics_referrer - case 'filter_google_analytics_referrer': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_google_analytics_referrer' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/gotwo - case 'gotwo_link': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'gotwo_link' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/h5p - case 'h5p_content': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'h5p_content' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/highlightjs - case 'highlight_js': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'highlight_js' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/htmLawed - case 'htmLawed': - return FilterInterface::TYPE_HTML_RESTRICTOR; - + 'htmLawed' => FilterInterface::TYPE_HTML_RESTRICTOR, // https://www.drupal.org/project/htmlpurifier - case 'htmlpurifier_basic': - case 'htmlpurifier_advanced': - return FilterInterface::TYPE_HTML_RESTRICTOR; - + 'htmlpurifier_basic', 'htmlpurifier_advanced' => FilterInterface::TYPE_HTML_RESTRICTOR, // https://www.drupal.org/project/htmltidy - case 'htmltidy': - return FilterInterface::TYPE_HTML_RESTRICTOR; - + 'htmltidy' => FilterInterface::TYPE_HTML_RESTRICTOR, // https://www.drupal.org/project/icon - case 'icon_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'icon_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/iframe_filter - case 'iframe': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'iframe' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/image_resize_filter - case 'image_resize_filter': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'image_resize_filter' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/insert_view - case 'insert_view': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'insert_view' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/intlinks - case 'intlinks title': - case 'intlinks hide bad': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'intlinks title', 'intlinks hide bad' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/jquery_ui_filter - case 'accordion': - case 'dialog': - case 'tabs': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - + 'accordion', 'dialog', 'tabs' => FilterInterface::TYPE_MARKUP_LANGUAGE, // https://www.drupal.org/project/language_sections - case 'language_sections': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - + 'language_sections' => FilterInterface::TYPE_MARKUP_LANGUAGE, // https://www.drupal.org/project/lazy - case 'lazy_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'lazy_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/lazyloader_filter - case 'lazyloader_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'lazyloader_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/link_node - case 'filter_link_node': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_link_node' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/linktitle - case 'linktitle': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'linktitle' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/markdown - case 'filter_markdown': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - + 'filter_markdown' => FilterInterface::TYPE_MARKUP_LANGUAGE, // https://www.drupal.org/project/media_wysiwyg - case 'media_filter': - case 'media_filter_paragraph_fix': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'media_filter', 'media_filter_paragraph_fix' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/mentions - case 'filter_mentions': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_mentions' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/menu_filter - case 'menu_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'menu_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/mobile_codes - case 'mobile_codes': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'mobile_codes' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/multicolumn - case 'multicolumn': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'multicolumn' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/multilink - case 'multilink_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'multilink_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/mytube - case 'mytube': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'mytube' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/node_embed - case 'node_embed': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'node_embed' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/node_field_embed - case 'node_field_embed': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'node_field_embed' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/noindex_external_links - case 'external_links': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'external_links' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/noreferrer - case 'noreferrer': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'noreferrer' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/oembed - case 'oembed': - case 'oembed_legacy': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'oembed', 'oembed_legacy' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/office_html - case 'office_html_strip': - return FilterInterface::TYPE_HTML_RESTRICTOR; - - case 'office_html_convert': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'office_html_strip' => FilterInterface::TYPE_HTML_RESTRICTOR, + 'office_html_convert' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/openlayers_filters - case 'openlayers': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'openlayers' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/opengraph_filter - case 'opengraph_filter': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'opengraph_filter' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/pathologic - case 'pathologic': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'pathologic' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/popup - case 'popup_tags': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'popup_tags' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/prettify - case 'prettify': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'prettify' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/rel_to_abs - case 'rel_to_abs': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'rel_to_abs' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/rollover_filter - case 'rollover_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'rollover_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/sanitizable - case 'sanitizable': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'sanitizable' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/smart_paging - case 'smart_paging_filter': - case 'smart_paging_filter_autop': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'smart_paging_filter', 'smart_paging_filter_autop' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/spamspan - case 'spamspan': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'spamspan' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/scald - case 'mee_scald_widgets': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'mee_scald_widgets' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/script_filter - case 'script_filter': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'script_filter' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/shortcode - case 'shortcode': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - - case 'shortcode_text_corrector': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'shortcode' => FilterInterface::TYPE_MARKUP_LANGUAGE, + 'shortcode_text_corrector' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/smiley - case 'smiley': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'smiley' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/svg_embed - case 'filter_svg_embed': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_svg_embed' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/spoiler - case 'spoiler': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'spoiler' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/tableofcontents - case 'filter_toc': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_toc' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/tables - case 'filter_tables': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_tables' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/target_filter_url - case 'target_filter_url': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'target_filter_url' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/textile - case 'textile': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - + 'textile' => FilterInterface::TYPE_MARKUP_LANGUAGE, // https://www.drupal.org/project/theme_filter - case 'theme_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'theme_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/token_filter - case 'filter_tokens': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'filter_tokens' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/transliteration - case 'transliteration': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'transliteration' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/typogrify - case 'typogrify': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'typogrify' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/uuid_link - case 'uuid_link_filter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'uuid_link_filter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/wysiwyg - case 'wysiwyg': - case 'wysiwyg_template_cleanup': - return FilterInterface::TYPE_HTML_RESTRICTOR; - + 'wysiwyg', 'wysiwyg_template_cleanup' => FilterInterface::TYPE_HTML_RESTRICTOR, // https://www.drupal.org/project/word_link - case 'word_link': - return FilterInterface::TYPE_TRANSFORM_REVERSIBLE; - + 'word_link' => FilterInterface::TYPE_TRANSFORM_REVERSIBLE, // https://www.drupal.org/project/wordfilter - case 'wordfilter': - return FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE; - + 'wordfilter' => FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, // https://www.drupal.org/project/xbbcode - case 'xbbcode': - return FilterInterface::TYPE_MARKUP_LANGUAGE; - } - - return NULL; + 'xbbcode' => FilterInterface::TYPE_MARKUP_LANGUAGE, + default => NULL, + }; } } diff --git a/core/modules/help/help.module b/core/modules/help/help.module index 69ab53e1436..17cb448e221 100644 --- a/core/modules/help/help.module +++ b/core/modules/help/help.module @@ -5,15 +5,6 @@ */ /** - * Implements hook_preprocess_HOOK() for block templates. - */ -function help_preprocess_block(&$variables): void { - if ($variables['plugin_id'] == 'help_block') { - $variables['attributes']['role'] = 'complementary'; - } -} - -/** * Ensure that search is updated when extensions are installed or uninstalled. * * @param string[] $extensions diff --git a/core/modules/help/src/Hook/HelpThemeHooks.php b/core/modules/help/src/Hook/HelpThemeHooks.php new file mode 100644 index 00000000000..431c7d089d2 --- /dev/null +++ b/core/modules/help/src/Hook/HelpThemeHooks.php @@ -0,0 +1,25 @@ +<?php + +namespace Drupal\help\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for help. + */ +class HelpThemeHooks { + /** + * @file + */ + + /** + * Implements hook_preprocess_HOOK() for block templates. + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + if ($variables['plugin_id'] == 'help_block') { + $variables['attributes']['role'] = 'complementary'; + } + } + +} diff --git a/core/modules/image/src/Hook/ImageHooks.php b/core/modules/image/src/Hook/ImageHooks.php index 2d6af269220..910b93037bd 100644 --- a/core/modules/image/src/Hook/ImageHooks.php +++ b/core/modules/image/src/Hook/ImageHooks.php @@ -177,7 +177,7 @@ class ImageHooks { // Private file access for image style derivatives. if (str_starts_with($path, 'styles/')) { $args = explode('/', $path); - // Discard "styles", style name, and scheme from the path + // Discard "styles", style name, and scheme from the path. $args = array_slice($args, 3); // Then the remaining parts are the path to the image. $original_uri = StreamWrapperManager::getScheme($uri) . '://' . implode('/', $args); diff --git a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php index efeacd6ff16..4ceca734539 100644 --- a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php +++ b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php @@ -81,7 +81,7 @@ class ImageFieldDisplayTest extends ImageFieldTestBase { $this->submitForm([], "{$field_name}_settings_edit"); $this->assertSession()->linkByHrefNotExists(Url::fromRoute('entity.image_style.collection')->toString(), 'Link to image styles configuration is absent when permissions are insufficient'); - // Restore 'administer image styles' permission to testing admin user + // Restore 'administer image styles' permission to testing admin user. user_role_change_permissions(reset($admin_user_roles), ['administer image styles' => TRUE]); // Create a new node with an image attached. @@ -384,7 +384,7 @@ class ImageFieldDisplayTest extends ImageFieldTestBase { $this->submitForm([], "{$field_name}_settings_edit"); $this->assertSession()->linkByHrefNotExists(Url::fromRoute('entity.image_style.collection')->toString(), 'Link to image styles configuration is absent when permissions are insufficient'); - // Restore 'administer image styles' permission to testing admin user + // Restore 'administer image styles' permission to testing admin user. user_role_change_permissions(reset($admin_user_roles), ['administer image styles' => TRUE]); // Create a new node with an image attached. diff --git a/core/modules/image/tests/src/Functional/ImageFieldWidgetTest.php b/core/modules/image/tests/src/Functional/ImageFieldWidgetTest.php index dd2e6929a97..7dd717a5d61 100644 --- a/core/modules/image/tests/src/Functional/ImageFieldWidgetTest.php +++ b/core/modules/image/tests/src/Functional/ImageFieldWidgetTest.php @@ -22,7 +22,7 @@ class ImageFieldWidgetTest extends ImageFieldTestBase { * Tests file widget element. */ public function testWidgetElement(): void { - // Check for image widget in add/node/article page + // Check for image widget in add/node/article page. $field_name = $this->randomMachineName(); $min_resolution = 50; $max_resolution = 100; diff --git a/core/modules/image/tests/src/FunctionalJavascript/ImageAdminStylesTest.php b/core/modules/image/tests/src/FunctionalJavascript/ImageAdminStylesTest.php index 2da7f3edd9b..a417b1727ca 100644 --- a/core/modules/image/tests/src/FunctionalJavascript/ImageAdminStylesTest.php +++ b/core/modules/image/tests/src/FunctionalJavascript/ImageAdminStylesTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\image\FunctionalJavascript; use Drupal\image\Entity\ImageStyle; +use PHPUnit\Framework\Attributes\Group; /** * Tests creation, deletion, and editing of image styles and effects. - * - * @group image */ +#[Group('image')] class ImageAdminStylesTest extends ImageFieldTestBase { /** diff --git a/core/modules/image/tests/src/FunctionalJavascript/ImageFieldValidateTest.php b/core/modules/image/tests/src/FunctionalJavascript/ImageFieldValidateTest.php index 9c9839b15c6..4e4f0021e3e 100644 --- a/core/modules/image/tests/src/FunctionalJavascript/ImageFieldValidateTest.php +++ b/core/modules/image/tests/src/FunctionalJavascript/ImageFieldValidateTest.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace Drupal\Tests\image\FunctionalJavascript; -use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use PHPUnit\Framework\Attributes\Group; /** * Tests validation functions such as min/max dimensions. - * - * @group image */ +#[Group('image')] class ImageFieldValidateTest extends ImageFieldTestBase { /** diff --git a/core/modules/image/tests/src/FunctionalJavascript/ImageFieldWidgetMultipleTest.php b/core/modules/image/tests/src/FunctionalJavascript/ImageFieldWidgetMultipleTest.php index ac253b33b39..8c1a637e93b 100644 --- a/core/modules/image/tests/src/FunctionalJavascript/ImageFieldWidgetMultipleTest.php +++ b/core/modules/image/tests/src/FunctionalJavascript/ImageFieldWidgetMultipleTest.php @@ -9,12 +9,12 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\node\Entity\Node; use Drupal\Tests\image\Kernel\ImageFieldCreationTrait; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests the image field widget support multiple upload correctly. - * - * @group image */ +#[Group('image')] class ImageFieldWidgetMultipleTest extends WebDriverTestBase { use ImageFieldCreationTrait; diff --git a/core/modules/inline_form_errors/inline_form_errors.module b/core/modules/inline_form_errors/inline_form_errors.module index 5faac086641..0e4bf06f0a2 100644 --- a/core/modules/inline_form_errors/inline_form_errors.module +++ b/core/modules/inline_form_errors/inline_form_errors.module @@ -5,34 +5,6 @@ */ /** - * Implements hook_preprocess_HOOK() for form element templates. - */ -function inline_form_errors_preprocess_form_element(&$variables): void { - _inline_form_errors_set_errors($variables); -} - -/** - * Implements hook_preprocess_HOOK() for details element templates. - */ -function inline_form_errors_preprocess_details(&$variables): void { - _inline_form_errors_set_errors($variables); -} - -/** - * Implements hook_preprocess_HOOK() for fieldset element templates. - */ -function inline_form_errors_preprocess_fieldset(&$variables): void { - _inline_form_errors_set_errors($variables); -} - -/** - * Implements hook_preprocess_HOOK() for datetime form wrapper templates. - */ -function inline_form_errors_preprocess_datetime_wrapper(&$variables): void { - _inline_form_errors_set_errors($variables); -} - -/** * Populates form errors in the template. */ function _inline_form_errors_set_errors(&$variables): void { diff --git a/core/modules/inline_form_errors/src/Hook/InlineFormErrorsThemeHooks.php b/core/modules/inline_form_errors/src/Hook/InlineFormErrorsThemeHooks.php new file mode 100644 index 00000000000..ac6d6f16bb6 --- /dev/null +++ b/core/modules/inline_form_errors/src/Hook/InlineFormErrorsThemeHooks.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\inline_form_errors\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for inline_form_errors. + */ +class InlineFormErrorsThemeHooks { + /** + * @file + */ + + /** + * Implements hook_preprocess_HOOK() for form element templates. + */ + #[Hook('preprocess_form_element')] + public function preprocessFormElement(&$variables): void { + _inline_form_errors_set_errors($variables); + } + + /** + * Implements hook_preprocess_HOOK() for details element templates. + */ + #[Hook('preprocess_details')] + public function preprocessDetails(&$variables): void { + _inline_form_errors_set_errors($variables); + } + + /** + * Implements hook_preprocess_HOOK() for fieldset element templates. + */ + #[Hook('preprocess_fieldset')] + public function preprocessFieldset(&$variables): void { + _inline_form_errors_set_errors($variables); + } + + /** + * Implements hook_preprocess_HOOK() for datetime form wrapper templates. + */ + #[Hook('preprocess_datetime_wrapper')] + public function preprocessDatetimeWrapper(&$variables): void { + _inline_form_errors_set_errors($variables); + } + +} diff --git a/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditor5Test.php b/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditor5Test.php index 340423c71be..063356bc161 100644 --- a/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditor5Test.php +++ b/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditor5Test.php @@ -12,12 +12,12 @@ use Drupal\filter\Entity\FilterFormat; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\node\Entity\NodeType; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; /** * Tests the inline errors fragment link to a CKEditor5-enabled textarea. - * - * @group ckeditor5 */ +#[Group('ckeditor5')] class FormErrorHandlerCKEditor5Test extends WebDriverTestBase { /** diff --git a/core/modules/jsonapi/jsonapi.api.php b/core/modules/jsonapi/jsonapi.api.php index 5b2f2002d25..ca8c5ae993f 100644 --- a/core/modules/jsonapi/jsonapi.api.php +++ b/core/modules/jsonapi/jsonapi.api.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Access\AccessResult; +use Drupal\jsonapi\JsonApiFilter; /** * @defgroup jsonapi_architecture JSON:API Architecture @@ -263,10 +264,10 @@ use Drupal\Core\Access\AccessResult; * viewable. * - AccessResult::neutral() if the implementation has no opinion. * The supported subsets for which an access result may be returned are: - * - JSONAPI_FILTER_AMONG_ALL: all entities of the given type. - * - JSONAPI_FILTER_AMONG_PUBLISHED: all published entities of the given type. - * - JSONAPI_FILTER_AMONG_ENABLED: all enabled entities of the given type. - * - JSONAPI_FILTER_AMONG_OWN: all entities of the given type owned by the + * - JsonApiFilter::AMONG_ALL: all entities of the given type. + * - JsonApiFilter::AMONG_PUBLISHED: all published entities of the given type. + * - JsonApiFilter::AMONG_ENABLED: all enabled entities of the given type. + * - JsonApiFilter::AMONG_OWN: all entities of the given type owned by the * user for whom access is being checked. * See the documentation of the above constants for more information about * each subset. @@ -278,7 +279,7 @@ function hook_jsonapi_entity_filter_access(EntityTypeInterface $entity_type, Acc // by all entities of that type to users with that permission. if ($admin_permission = $entity_type->getAdminPermission()) { return ([ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission), ]); } } @@ -305,9 +306,9 @@ function hook_jsonapi_entity_filter_access(EntityTypeInterface $entity_type, Acc */ function hook_jsonapi_ENTITY_TYPE_filter_access(EntityTypeInterface $entity_type, AccountInterface $account): array { return ([ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer llamas'), - JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view all published llamas'), - JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermissions($account, ['view own published llamas', 'view own unpublished llamas'], 'AND'), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer llamas'), + JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view all published llamas'), + JsonApiFilter::AMONG_OWN => AccessResult::allowedIfHasPermissions($account, ['view own published llamas', 'view own unpublished llamas'], 'AND'), ]); } diff --git a/core/modules/jsonapi/jsonapi.module b/core/modules/jsonapi/jsonapi.module index 69414af650b..c512575305a 100644 --- a/core/modules/jsonapi/jsonapi.module +++ b/core/modules/jsonapi/jsonapi.module @@ -11,6 +11,10 @@ * regardless of whether they are published or enabled, and regardless of * their owner. * + * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use + * \Drupal\jsonapi\JsonApiFilter::AMONG_ALL instead. + * + * @see https://www.drupal.org/node/3495601 * @see hook_jsonapi_entity_filter_access() * @see hook_jsonapi_ENTITY_TYPE_filter_access() */ @@ -25,6 +29,10 @@ const JSONAPI_FILTER_AMONG_ALL = 'filter_among_all'; * This is used when an entity type has a "published" entity key and there's a * query condition for the value of that equaling 1. * + * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use + * \Drupal\jsonapi\JsonApiFilter::AMONG_PUBLISHED instead. + * + * @see https://www.drupal.org/node/3495601 * @see hook_jsonapi_entity_filter_access() * @see hook_jsonapi_ENTITY_TYPE_filter_access() */ @@ -42,6 +50,10 @@ const JSONAPI_FILTER_AMONG_PUBLISHED = 'filter_among_published'; * For the User entity type, which does not have a "status" entity key, the * "status" field is used. * + * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use + * \Drupal\jsonapi\JsonApiFilter::AMONG_ENABLED instead. + * + * @see https://www.drupal.org/node/3495601 * @see hook_jsonapi_entity_filter_access() * @see hook_jsonapi_ENTITY_TYPE_filter_access() */ @@ -64,6 +76,10 @@ const JSONAPI_FILTER_AMONG_ENABLED = 'filter_among_enabled'; * - The entity type has an "owner" entity key. * - There's a filter/query condition for the value equal to the user's ID. * + * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use + * \Drupal\jsonapi\JsonApiFilter::AMONG_OWN instead. + * + * @see https://www.drupal.org/node/3495601 * @see hook_jsonapi_entity_filter_access() * @see hook_jsonapi_ENTITY_TYPE_filter_access() */ diff --git a/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php b/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php index d59dca4ec03..2888fbbec77 100644 --- a/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php +++ b/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php @@ -12,6 +12,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\DataReferenceDefinitionInterface; +use Drupal\jsonapi\JsonApiFilter; use Drupal\jsonapi\Query\EntityCondition; use Drupal\jsonapi\Query\EntityConditionGroup; use Drupal\jsonapi\Query\Filter; @@ -323,12 +324,12 @@ class TemporaryQueryGuard { } /** - * Gets an access condition for the allowed JSONAPI_FILTER_AMONG_* subsets. + * Gets an access condition for the allowed JsonApiFilter::AMONG_* subsets. * - * If access is allowed for the JSONAPI_FILTER_AMONG_ALL subset, then no + * If access is allowed for the JsonApiFilter::AMONG_ALL subset, then no * conditions are returned. Otherwise, if access is allowed for - * JSONAPI_FILTER_AMONG_PUBLISHED, JSONAPI_FILTER_AMONG_ENABLED, or - * JSONAPI_FILTER_AMONG_OWN, then a condition group is returned for the union + * JsonApiFilter::AMONG_PUBLISHED, JsonApiFilter::AMONG_ENABLED, or + * JsonApiFilter::AMONG_OWN, then a condition group is returned for the union * of allowed subsets. If no subsets are allowed, then static::alwaysFalse() * is returned. * @@ -344,12 +345,12 @@ class TemporaryQueryGuard { * secure an entity query. */ protected static function getAccessConditionForKnownSubsets(EntityTypeInterface $entity_type, AccountInterface $account, CacheableMetadata $cacheability) { - // Get the combined access results for each JSONAPI_FILTER_AMONG_* subset. + // Get the combined access results for each JsonApiFilter::AMONG_* subset. $access_results = static::getAccessResultsFromEntityFilterHook($entity_type, $account); // No conditions are needed if access is allowed for all entities. - $cacheability->addCacheableDependency($access_results[JSONAPI_FILTER_AMONG_ALL]); - if ($access_results[JSONAPI_FILTER_AMONG_ALL]->isAllowed()) { + $cacheability->addCacheableDependency($access_results[JsonApiFilter::AMONG_ALL]); + if ($access_results[JsonApiFilter::AMONG_ALL]->isAllowed()) { return NULL; } @@ -363,7 +364,7 @@ class TemporaryQueryGuard { // The "published" subset. $published_field_name = $entity_type->getKey('published'); if ($published_field_name) { - $access_result = $access_results[JSONAPI_FILTER_AMONG_PUBLISHED]; + $access_result = $access_results[JsonApiFilter::AMONG_PUBLISHED]; $cacheability->addCacheableDependency($access_result); if ($access_result->isAllowed()) { $conditions[] = new EntityCondition($published_field_name, 1); @@ -375,7 +376,7 @@ class TemporaryQueryGuard { // @todo Remove ternary when the 'status' key is added to the User entity type. $status_field_name = $entity_type->id() === 'user' ? 'status' : $entity_type->getKey('status'); if ($status_field_name) { - $access_result = $access_results[JSONAPI_FILTER_AMONG_ENABLED]; + $access_result = $access_results[JsonApiFilter::AMONG_ENABLED]; $cacheability->addCacheableDependency($access_result); if ($access_result->isAllowed()) { $conditions[] = new EntityCondition($status_field_name, 1); @@ -387,7 +388,7 @@ class TemporaryQueryGuard { // @todo Remove ternary when the 'uid' key is added to the User entity type. $owner_field_name = $entity_type->id() === 'user' ? 'uid' : $entity_type->getKey('owner'); if ($owner_field_name) { - $access_result = $access_results[JSONAPI_FILTER_AMONG_OWN]; + $access_result = $access_results[JsonApiFilter::AMONG_OWN]; $cacheability->addCacheableDependency($access_result); if ($access_result->isAllowed()) { $cacheability->addCacheContexts(['user']); @@ -415,7 +416,7 @@ class TemporaryQueryGuard { } /** - * Gets the combined access result for each JSONAPI_FILTER_AMONG_* subset. + * Gets the combined access result for each JsonApiFilter::AMONG_* subset. * * This invokes hook_jsonapi_entity_filter_access() and * hook_jsonapi_ENTITY_TYPE_filter_access() and combines the results from all @@ -433,10 +434,10 @@ class TemporaryQueryGuard { protected static function getAccessResultsFromEntityFilterHook(EntityTypeInterface $entity_type, AccountInterface $account) { /** @var \Drupal\Core\Access\AccessResultInterface[] $combined_access_results */ $combined_access_results = [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::neutral(), - JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::neutral(), - JSONAPI_FILTER_AMONG_ENABLED => AccessResult::neutral(), - JSONAPI_FILTER_AMONG_OWN => AccessResult::neutral(), + JsonApiFilter::AMONG_ALL => AccessResult::neutral(), + JsonApiFilter::AMONG_PUBLISHED => AccessResult::neutral(), + JsonApiFilter::AMONG_ENABLED => AccessResult::neutral(), + JsonApiFilter::AMONG_OWN => AccessResult::neutral(), ]; // Invoke hook_jsonapi_entity_filter_access() and diff --git a/core/modules/jsonapi/src/Hook/JsonapiHooks.php b/core/modules/jsonapi/src/Hook/JsonapiHooks.php index 7db5b77b0e8..d5f6d2540fc 100644 --- a/core/modules/jsonapi/src/Hook/JsonapiHooks.php +++ b/core/modules/jsonapi/src/Hook/JsonapiHooks.php @@ -7,6 +7,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\jsonapi\JsonApiFilter; use Drupal\jsonapi\Routing\Routes; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; @@ -107,7 +108,7 @@ class JsonapiHooks { // AccessResult::forbidden() from its implementation of this hook. if ($admin_permission = $entity_type->getAdminPermission()) { return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission), ]; } return []; @@ -122,8 +123,8 @@ class JsonapiHooks { // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for // (isReusable()), so this does not have to. return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access block library'), - JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed(), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access block library'), + JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowed(), ]; } @@ -136,8 +137,8 @@ class JsonapiHooks { // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for // (access to the commented entity), so this does not have to. return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer comments'), - JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access comments'), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer comments'), + JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access comments'), ]; } @@ -148,7 +149,7 @@ class JsonapiHooks { public function jsonapiEntityTestFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array { // @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess() return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view test entity'), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view test entity'), ]; } @@ -161,7 +162,7 @@ class JsonapiHooks { // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for // (public OR owner), so this does not have to. return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access content'), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access content'), ]; } @@ -172,7 +173,7 @@ class JsonapiHooks { public function jsonapiMediaFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array { // @see \Drupal\media\MediaAccessControlHandler::checkAccess() return [ - JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view media'), + JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view media'), ]; } @@ -184,30 +185,30 @@ class JsonapiHooks { // @see \Drupal\node\NodeAccessControlHandler::access() if ($account->hasPermission('bypass node access')) { return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowed()->cachePerPermissions(), + JsonApiFilter::AMONG_ALL => AccessResult::allowed()->cachePerPermissions(), ]; } if (!$account->hasPermission('access content')) { $forbidden = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions(); return [ - JSONAPI_FILTER_AMONG_ALL => $forbidden, - JSONAPI_FILTER_AMONG_OWN => $forbidden, - JSONAPI_FILTER_AMONG_PUBLISHED => $forbidden, + JsonApiFilter::AMONG_ALL => $forbidden, + JsonApiFilter::AMONG_OWN => $forbidden, + JsonApiFilter::AMONG_PUBLISHED => $forbidden, // For legacy reasons, the Node entity type has a "status" key, so // forbid this subset as well, even though it has no semantic meaning. - JSONAPI_FILTER_AMONG_ENABLED => $forbidden, + JsonApiFilter::AMONG_ENABLED => $forbidden, ]; } return [ - // @see \Drupal\node\NodeAccessControlHandler::checkAccess() - JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own unpublished content'), - // @see \Drupal\node\NodeGrantDatabaseStorage::access() - // Note that: - // - This is just for the default grant. Other node access conditions - // are added via the 'node_access' query tag. - // - Permissions were checked earlier in this function, so we must - // vary the cache by them. - JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed()->cachePerPermissions(), + // @see \Drupal\node\NodeAccessControlHandler::checkAccess() + JsonApiFilter::AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own unpublished content'), + // @see \Drupal\node\NodeGrantDatabaseStorage::access() + // Note that: + // - This is just for the default grant. Other node access conditions + // are added via the 'node_access' query tag. + // - Permissions were checked earlier in this function, so we must + // vary the cache by them. + JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowed()->cachePerPermissions(), ]; } @@ -221,7 +222,7 @@ class JsonapiHooks { // "shortcut_set = $shortcut_set_storage->getDisplayedToUser($current_user)" // so this does not have to. return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')->orIf(AccessResult::allowedIfHasPermissions($account, [ + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')->orIf(AccessResult::allowedIfHasPermissions($account, [ 'access shortcuts', 'customize shortcut links', ])), @@ -235,8 +236,8 @@ class JsonapiHooks { public function jsonapiTaxonomyTermFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array { // @see \Drupal\taxonomy\TermAccessControlHandler::checkAccess() return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer taxonomy'), - JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access content'), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer taxonomy'), + JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access content'), ]; } @@ -249,8 +250,8 @@ class JsonapiHooks { // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for // (!isAnonymous()), so this does not have to. return [ - JSONAPI_FILTER_AMONG_OWN => AccessResult::allowed(), - JSONAPI_FILTER_AMONG_ENABLED => AccessResult::allowedIfHasPermission($account, 'access user profiles'), + JsonApiFilter::AMONG_OWN => AccessResult::allowed(), + JsonApiFilter::AMONG_ENABLED => AccessResult::allowedIfHasPermission($account, 'access user profiles'), ]; } @@ -261,8 +262,8 @@ class JsonapiHooks { public function jsonapiWorkspaceFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array { // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess() return [ - JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'), - JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own workspace'), + JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'), + JsonApiFilter::AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own workspace'), ]; } diff --git a/core/modules/jsonapi/src/JsonApiFilter.php b/core/modules/jsonapi/src/JsonApiFilter.php new file mode 100644 index 00000000000..c9ac90be7af --- /dev/null +++ b/core/modules/jsonapi/src/JsonApiFilter.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\jsonapi; + +/** + * JsonApi filter options. + */ +final class JsonApiFilter { + + /** + * Array key for denoting type-based filtering access. + * + * Array key for denoting access to filter among all entities of a given type, + * regardless of whether they are published or enabled, and regardless of + * their owner. + * + * @see hook_jsonapi_entity_filter_access() + * @see hook_jsonapi_ENTITY_TYPE_filter_access() + */ + const AMONG_ALL = 'filter_among_all'; + + /** + * Array key for denoting type-based published-only filtering access. + * + * Array key for denoting access to filter among all published entities of a + * given type, regardless of their owner. + * + * This is used when an entity type has a "published" entity key and there's a + * query condition for the value of that equaling 1. + * + * @see hook_jsonapi_entity_filter_access() + * @see hook_jsonapi_ENTITY_TYPE_filter_access() + */ + const AMONG_PUBLISHED = 'filter_among_published'; + + /** + * Array key for denoting type-based enabled-only filtering access. + * + * Array key for denoting access to filter among all enabled entities of a + * given type, regardless of their owner. + * + * This is used when an entity type has a "status" entity key and there's a + * query condition for the value of that equaling 1. + * + * For the User entity type, which does not have a "status" entity key, the + * "status" field is used. + * + * @see hook_jsonapi_entity_filter_access() + * @see hook_jsonapi_ENTITY_TYPE_filter_access() + */ + const AMONG_ENABLED = 'filter_among_enabled'; + + /** + * Array key for denoting type-based owned-only filtering access. + * + * Array key for denoting access to filter among all entities of a given type, + * regardless of whether they are published or enabled, so long as they are + * owned by the user for whom access is being checked. + * + * When filtering among User entities, this is used when access is being + * checked for an authenticated user and there's a query condition + * limiting the result set to just that user's entity object. + * + * When filtering among entities of another type, this is used when all of the + * following conditions are met: + * - Access is being checked for an authenticated user. + * - The entity type has an "owner" entity key. + * - There's a filter/query condition for the value equal to the user's ID. + * + * @see hook_jsonapi_entity_filter_access() + * @see hook_jsonapi_ENTITY_TYPE_filter_access() + */ + const AMONG_OWN = 'filter_among_own'; + +} diff --git a/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php b/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php index 6d4cc6ce55f..6564f2b3401 100644 --- a/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php +++ b/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php @@ -98,7 +98,7 @@ class ResourceObject implements CacheableDependencyInterface, ResourceIdentifier $this->links = $links->withContext($this); // If the specified language empty it falls back the same way as in the - // entity system + // entity system. // @see \Drupal\Core\Entity\EntityBase::language() $this->language = $language ?: new Language(['id' => LanguageInterface::LANGCODE_NOT_SPECIFIED]); } diff --git a/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php b/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php index 199b27d308f..12aa5eeec52 100644 --- a/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php +++ b/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php @@ -584,7 +584,7 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase { 'resource_meta_title' => $node->getTitle(), ]; $this->assertEquals($expectedMeta, $result['data']['meta']); - // Test if the cache tags bubbled up + // Test if the cache tags bubbled up. $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'jsonapi_test_meta_events.object_meta'); $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.roles'); @@ -602,11 +602,11 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase { } - // Test if the cache tags bubbled up + // Test if the cache tags bubbled up. $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'jsonapi_test_meta_events.object_meta'); $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.roles'); - // Now try the same requests with a superuser, see if we get other caches + // Now try the same requests with a superuser, see if we get other caches. $this->mink->resetSessions(); $this->drupalResetSession(); $this->drupalLogin($this->adminUser); @@ -674,7 +674,7 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase { // Test if the cache tags bubbled up. $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'jsonapi_test_meta_events.relationship_meta'); - // Test if relationship has correct metadata when loading a single resource + // Test if relationship has correct metadata when loading a single resource. $resource = Json::decode($this->drupalGet('jsonapi/node/article/' . $node->uuid())); if ($resource['data']['id'] === $node->uuid()) { $tagNames = $resource['data']['relationships']['field_tags']['meta']['relationship_meta_name']; @@ -687,7 +687,7 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase { } - // Test if the cache tags bubbled up + // Test if the cache tags bubbled up. $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'jsonapi_test_meta_events.relationship_meta'); } @@ -706,7 +706,7 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase { 'fields' => ['name'], ]); - // Test if relationship has correct metadata when loading a single resource + // Test if relationship has correct metadata when loading a single resource. $str = $this->drupalGet('jsonapi/node/article/' . $node->uuid() . '/relationships/field_tags'); $resource = Json::decode($str); diff --git a/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php b/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php index b1b0934b5da..6c69e60f404 100644 --- a/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php +++ b/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php @@ -6,14 +6,15 @@ namespace Drupal\Tests\jsonapi\FunctionalJavascript; use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\PerformanceTestBase; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; /** * Tests performance for JSON:API routes. - * - * @group Common - * @group #slow - * @requires extension apcu */ +#[Group('Common')] +#[Group('#slow')] +#[RequiresPhpExtension('apcu')] class JsonApiPerformanceTest extends PerformanceTestBase { /** diff --git a/core/modules/jsonapi/tests/src/Kernel/EventSubscriber/ResourceObjectNormalizerCacherTest.php b/core/modules/jsonapi/tests/src/Kernel/EventSubscriber/ResourceObjectNormalizerCacherTest.php index d9cda8df404..4f609de47a7 100644 --- a/core/modules/jsonapi/tests/src/Kernel/EventSubscriber/ResourceObjectNormalizerCacherTest.php +++ b/core/modules/jsonapi/tests/src/Kernel/EventSubscriber/ResourceObjectNormalizerCacherTest.php @@ -131,7 +131,7 @@ class ResourceObjectNormalizerCacherTest extends KernelTestBase { $this->installEntitySchema('entity_test_computed_field'); // Use EntityTestComputedField since ComputedTestCacheableStringItemList has - // a max age of 800 + // a max age of 800. $baseMaxAge = 800; $entity = EntityTestComputedField::create([]); $entity->save(); @@ -149,7 +149,7 @@ class ResourceObjectNormalizerCacherTest extends KernelTestBase { $event = new TerminateEvent($http_kernel->reveal(), $request->reveal(), $response->reveal()); $this->cacher->onTerminate($event); - // Change request time to 500 seconds later + // Change request time to 500 seconds later. $current_request = \Drupal::requestStack()->getCurrentRequest(); $current_request->server->set('REQUEST_TIME', $current_request->server->get('REQUEST_TIME') + 500); $resource_normalization = $this->serializer diff --git a/core/modules/language/language.module b/core/modules/language/language.module index fb9420037db..2d65dc70365 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -127,15 +127,6 @@ function language_negotiation_url_prefixes_update(): void { } /** - * Implements hook_preprocess_HOOK() for block templates. - */ -function language_preprocess_block(&$variables): void { - if ($variables['configuration']['provider'] == 'language') { - $variables['attributes']['role'] = 'navigation'; - } -} - -/** * Returns language mappings between browser and Drupal language codes. * * @return array diff --git a/core/modules/language/src/Hook/LanguageThemeHooks.php b/core/modules/language/src/Hook/LanguageThemeHooks.php new file mode 100644 index 00000000000..0629f7ff682 --- /dev/null +++ b/core/modules/language/src/Hook/LanguageThemeHooks.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\language\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for language. + */ +class LanguageThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for block templates. + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + if ($variables['configuration']['provider'] == 'language') { + $variables['attributes']['role'] = 'navigation'; + } + } + +} diff --git a/core/modules/language/tests/src/Functional/LanguageSwitchingTest.php b/core/modules/language/tests/src/Functional/LanguageSwitchingTest.php index 32ec3a2c6e5..44beb40d3eb 100644 --- a/core/modules/language/tests/src/Functional/LanguageSwitchingTest.php +++ b/core/modules/language/tests/src/Functional/LanguageSwitchingTest.php @@ -356,11 +356,11 @@ class LanguageSwitchingTest extends BrowserTestBase { /** @var \Drupal\Core\Routing\UrlGenerator $generator */ $generator = $this->container->get('url_generator'); - // Verify the English URL is correct + // Verify the English URL is correct. $english_url = $generator->generateFromRoute('entity.user.canonical', ['user' => 2], ['language' => $languages['en']]); $this->assertSession()->elementAttributeContains('xpath', '//div[@id="block-test-language-block"]/ul/li/a[@hreflang="en"]', 'href', $english_url); - // Verify the Italian URL is correct + // Verify the Italian URL is correct. $italian_url = $generator->generateFromRoute('entity.user.canonical', ['user' => 2], ['language' => $languages['it']]); $this->assertSession()->elementAttributeContains('xpath', '//div[@id="block-test-language-block"]/ul/li/a[@hreflang="it"]', 'href', $italian_url); } diff --git a/core/modules/language/tests/src/Kernel/Condition/LanguageConditionTest.php b/core/modules/language/tests/src/Kernel/Condition/LanguageConditionTest.php index 19a74112e61..6b1949a72aa 100644 --- a/core/modules/language/tests/src/Kernel/Condition/LanguageConditionTest.php +++ b/core/modules/language/tests/src/Kernel/Condition/LanguageConditionTest.php @@ -66,7 +66,7 @@ class LanguageConditionTest extends KernelTestBase { // Check for the proper summary. $this->assertEquals('The language is Italian.', $condition->summary()); - // Negate the condition + // Negate the condition. $condition->setConfig('negate', TRUE); $this->assertTrue($condition->execute(), 'Language condition passes as expected.'); // Check for the proper summary. @@ -89,7 +89,7 @@ class LanguageConditionTest extends KernelTestBase { // Check for the proper summary. $this->assertEquals('The language is Italian.', $condition->summary()); - // Negate the condition + // Negate the condition. $condition->setConfig('negate', TRUE); $this->assertFalse($condition->execute(), 'Language condition fails as expected.'); // Check for the proper summary. diff --git a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageContentSettingsTest.php b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageContentSettingsTest.php index 28da2b80249..da4bd0750c5 100644 --- a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageContentSettingsTest.php +++ b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageContentSettingsTest.php @@ -76,7 +76,7 @@ class MigrateLanguageContentSettingsTest extends MigrateDrupal7TestBase { $this->assertSame($config->get('default_langcode'), 'und'); // Assert that a content type without a 'language_content_type' variable is - // not translatable + // not translatable. $config = ContentLanguageSettings::loadByEntityTypeBundle('node', 'book'); $this->assertTrue($config->isDefaultConfiguration()); $this->assertFalse($config->isLanguageAlterable()); diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module deleted file mode 100644 index 37dc492bf78..00000000000 --- a/core/modules/layout_builder/layout_builder.module +++ /dev/null @@ -1,29 +0,0 @@ -<?php - -/** - * @file - */ - -use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; - -/** - * Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig. - */ -function layout_builder_preprocess_language_content_settings_table(&$variables): void { - foreach ($variables['build']['#rows'] as &$row) { - if (isset($row['#field_name']) && $row['#field_name'] === OverridesSectionStorage::FIELD_NAME) { - // Rebuild the label to include a warning about using translations with - // layouts. - $row['data'][1]['data']['field'] = [ - 'label' => $row['data'][1]['data']['field'], - 'description' => [ - '#type' => 'container', - '#markup' => t('<strong>Warning</strong>: Layout Builder does not support translating layouts. (<a href="https://www.drupal.org/docs/8/core/modules/layout-builder/layout-builder-and-content-translation">online documentation</a>)'), - '#attributes' => [ - 'class' => ['layout-builder-translation-warning'], - ], - ], - ]; - } - } -} diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index d673d24d1d3..2b128f93188 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -11,6 +11,7 @@ use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Render\Attribute\RenderElement; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\RenderElementBase; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Security\Attribute\TrustedCallback; use Drupal\Core\Url; use Drupal\layout_builder\Context\LayoutBuilderContextTrait; @@ -52,9 +53,11 @@ class LayoutBuilder extends RenderElementBase implements ContainerFactoryPluginI * The plugin implementation definition. * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher service. + * @param \Drupal\Core\Render\ElementInfoManagerInterface|null $elementInfoManager + * The element info manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, ?ElementInfoManagerInterface $elementInfoManager = NULL) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); $this->eventDispatcher = $event_dispatcher; } @@ -66,7 +69,8 @@ class LayoutBuilder extends RenderElementBase implements ContainerFactoryPluginI $configuration, $plugin_id, $plugin_definition, - $container->get('event_dispatcher') + $container->get('event_dispatcher'), + $container->get('plugin.manager.element_info') ); } diff --git a/core/modules/layout_builder/src/Hook/LayoutBuilderThemeHooks.php b/core/modules/layout_builder/src/Hook/LayoutBuilderThemeHooks.php new file mode 100644 index 00000000000..a4da9e469b9 --- /dev/null +++ b/core/modules/layout_builder/src/Hook/LayoutBuilderThemeHooks.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\layout_builder\Hook; + +use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Hook implementations for layout_builder. + */ +class LayoutBuilderThemeHooks { + use StringTranslationTrait; + + /** + * Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig. + */ + #[Hook('preprocess_language_content_settings_table')] + public function preprocessLanguageContentSettingsTable(&$variables): void { + foreach ($variables['build']['#rows'] as &$row) { + if (isset($row['#field_name']) && $row['#field_name'] === OverridesSectionStorage::FIELD_NAME) { + // Rebuild the label to include a warning about using translations with + // layouts. + $row['data'][1]['data']['field'] = [ + 'label' => $row['data'][1]['data']['field'], + 'description' => [ + '#type' => 'container', + '#markup' => $this->t('<strong>Warning</strong>: Layout Builder does not support translating layouts. (<a href="https://www.drupal.org/docs/8/core/modules/layout-builder/layout-builder-and-content-translation">online documentation</a>)'), + '#attributes' => [ + 'class' => [ + 'layout-builder-translation-warning', + ], + ], + ], + ]; + } + } + } + +} diff --git a/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/core/modules/layout_builder/src/InlineBlockEntityOperations.php index 16bda99a45c..7a832ad284c 100644 --- a/core/modules/layout_builder/src/InlineBlockEntityOperations.php +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php @@ -134,8 +134,6 @@ class InlineBlockEntityOperations implements ContainerInjectionInterface { * The parent entity. */ public function handleEntityDelete(EntityInterface $entity) { - // @todo In https://www.drupal.org/node/3008943 call - // \Drupal\layout_builder\LayoutEntityHelperTrait::isLayoutCompatibleEntity(). $this->usage->removeByLayoutEntity($entity); } diff --git a/core/modules/layout_builder/tests/modules/layout_builder_block_content_dependency_test/layout_builder_block_content_dependency_test.module b/core/modules/layout_builder/tests/modules/layout_builder_block_content_dependency_test/layout_builder_block_content_dependency_test.module deleted file mode 100644 index 93bb03de404..00000000000 --- a/core/modules/layout_builder/tests/modules/layout_builder_block_content_dependency_test/layout_builder_block_content_dependency_test.module +++ /dev/null @@ -1,19 +0,0 @@ -<?php - -/** - * @file - * Provides hook implementations for testing Layout Builder with Block Content. - */ - -declare(strict_types=1); - -/** - * Implements hook_modules_installed(). - */ -function layout_builder_block_content_dependency_test_modules_installed(array $modules, bool $is_syncing): void { - // @see Drupal\Tests\layout_builder\Kernel\LayoutBuilderBlockContentDependencyTest - if (in_array('layout_builder', $modules)) { - \Drupal::service('plugin.manager.block')->getDefinitions(); - \Drupal::service('module_installer')->install(['block_content']); - } -} diff --git a/core/modules/layout_builder/tests/modules/layout_builder_block_content_dependency_test/src/Hook/LayoutBuilderBlockContentDependencyTestThemeHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_block_content_dependency_test/src/Hook/LayoutBuilderBlockContentDependencyTestThemeHooks.php new file mode 100644 index 00000000000..1269020852c --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_block_content_dependency_test/src/Hook/LayoutBuilderBlockContentDependencyTestThemeHooks.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\layout_builder_block_content_dependency_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for layout_builder_block_content_dependency_test. + */ +class LayoutBuilderBlockContentDependencyTestThemeHooks { + + /** + * Implements hook_modules_installed(). + */ + #[Hook('modules_installed')] + public function modulesInstalled(array $modules, bool $is_syncing): void { + // @see Drupal\Tests\layout_builder\Kernel\LayoutBuilderBlockContentDependencyTest + if (in_array('layout_builder', $modules)) { + \Drupal::service('plugin.manager.block')->getDefinitions(); + \Drupal::service('module_installer')->install([ + 'block_content', + ]); + } + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTestBase.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTestBase.php index 94cb2455b5a..75dac6efac2 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTestBase.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTestBase.php @@ -10,7 +10,7 @@ use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the Layout Builder UI. */ -class LayoutBuilderTestBase extends BrowserTestBase { +abstract class LayoutBuilderTestBase extends BrowserTestBase { use FieldUiTestTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/AjaxBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/AjaxBlockTest.php index bf33b3d7c78..13cbab6fdfb 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/AjaxBlockTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/AjaxBlockTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\Tests\system\Traits\OffCanvasTestTrait; +use PHPUnit\Framework\Attributes\Group; /** * Ajax blocks tests. - * - * @group layout_builder */ +#[Group('layout_builder')] class AjaxBlockTest extends WebDriverTestBase { use OffCanvasTestTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFilterTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFilterTest.php index 92d95a84908..60ae0570638 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFilterTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFilterTest.php @@ -7,13 +7,14 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Behat\Mink\Element\NodeElement; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; /** * Tests the JavaScript functionality of the block add filter. - * - * @group layout_builder - * @group legacy */ +#[Group('layout_builder')] +#[IgnoreDeprecations] class BlockFilterTest extends WebDriverTestBase { /** @@ -115,7 +116,7 @@ class BlockFilterTest extends WebDriverTestBase { $visible_categories = $this->filterVisibleElements($categories); $this->assertCount(3, $visible_categories); - // Test blocks reappear after being filtered by repeating search for "a" + // Test blocks reappear after being filtered by repeating search for "a". $filter->setValue('a'); $this->assertAnnounceContains('All available blocks are listed.'); diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php index c07b69de1ef..555af66c86c 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests that messages appear in the off-canvas dialog with configuring blocks. - * - * @group layout_builder */ +#[Group('layout_builder')] class BlockFormMessagesTest extends WebDriverTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/ContentPreviewToggleTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/ContentPreviewToggleTest.php index 333ae628292..476ec728fa4 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/ContentPreviewToggleTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/ContentPreviewToggleTest.php @@ -8,14 +8,13 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; use Drupal\Tests\system\Traits\OffCanvasTestTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore blocknodebundle testbody - /** * Tests toggling of content preview. - * - * @group layout_builder */ +#[Group('layout_builder')] class ContentPreviewToggleTest extends WebDriverTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/ContextualLinksTest.php index efa1b1ba979..fb62f1bed6f 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/ContextualLinksTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/ContextualLinksTest.php @@ -7,14 +7,13 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore blocktest - /** * Test contextual links compatibility with the Layout Builder. - * - * @group layout_builder */ +#[Group('layout_builder')] class ContextualLinksTest extends WebDriverTestBase { use AssertPageCacheContextsAndTagsTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php index 75fdf40097c..37c733435cd 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php @@ -7,15 +7,18 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\layout_builder\Plugin\Block\FieldBlock; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; // cspell:ignore datefield - /** - * @coversDefaultClass \Drupal\layout_builder\Plugin\Block\FieldBlock - * - * @group field - * @group legacy + * Tests Drupal\layout_builder\Plugin\Block\FieldBlock. */ +#[CoversClass(FieldBlock::class)] +#[Group('field')] +#[IgnoreDeprecations] class FieldBlockTest extends WebDriverTestBase { /** diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php index 97808e73b69..66e6a3d6a6c 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php @@ -11,12 +11,12 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\file\Functional\FileFieldCreationTrait; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Test access to private files in block fields on the Layout Builder. - * - * @group layout_builder */ +#[Group('layout_builder')] class InlineBlockPrivateFilesTest extends InlineBlockTestBase { use FileFieldCreationTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php index 2dcbbf645e5..9097e244c18 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php @@ -6,13 +6,14 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\node\Entity\Node; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; /** * Tests that the inline block feature works correctly. - * - * @group layout_builder - * @group #slow */ +#[Group('layout_builder')] +#[Group('#slow')] class InlineBlockTest extends InlineBlockTestBase { /** @@ -113,9 +114,8 @@ class InlineBlockTest extends InlineBlockTestBase { /** * Tests adding a new entity block and then not saving the layout. - * - * @dataProvider layoutNoSaveProvider */ + #[DataProvider('layoutNoSaveProvider')] public function testNoLayoutSave($operation, $no_save_button_text, $confirm_button_text): void { $this->drupalLogin($this->drupalCreateUser([ 'access contextual links', diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php index 6931a733d96..bd46767863a 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Drupal\Tests\layout_builder\FunctionalJavascript; -use Drupal\block_content\Entity\BlockContentType; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\Tests\block_content\Traits\BlockContentCreationTrait; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; // cspell:ignore blockbasic @@ -15,6 +15,9 @@ use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; */ abstract class InlineBlockTestBase extends WebDriverTestBase { + use BlockContentCreationTrait { + createBlockContentType as baseCreateBlockContentType; + } use ContextualLinkClickTrait; /** @@ -214,13 +217,11 @@ abstract class InlineBlockTestBase extends WebDriverTestBase { * The block type label. */ protected function createBlockContentType($id, $label) { - $bundle = BlockContentType::create([ + $this->baseCreateBlockContentType([ 'id' => $id, 'label' => $label, 'revision' => 1, - ]); - $bundle->save(); - block_content_add_body_field($bundle->id()); + ], TRUE); } } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/ItemLayoutFieldBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/ItemLayoutFieldBlockTest.php index 4bb6313816f..217347b4692 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/ItemLayoutFieldBlockTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/ItemLayoutFieldBlockTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use PHPUnit\Framework\Attributes\Group; /** * Field blocks tests for the override layout. - * - * @group layout_builder */ +#[Group('layout_builder')] class ItemLayoutFieldBlockTest extends WebDriverTestBase { /** diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php index dc44888a8b2..003248030d5 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php @@ -11,15 +11,14 @@ use Drupal\FunctionalJavascriptTests\JSWebAssert; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; use Drupal\Tests\system\Traits\OffCanvasTestTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore blocknodebundle fieldbody - /** * Tests the Layout Builder disables interactions of rendered blocks. - * - * @group layout_builder - * @group #slow */ +#[Group('layout_builder')] +#[Group('#slow')] class LayoutBuilderDisableInteractionsTest extends WebDriverTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderNestedFormUiTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderNestedFormUiTest.php index 6cb5b136b87..f7d0f2a6dd6 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderNestedFormUiTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderNestedFormUiTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests placing blocks containing forms in theLayout Builder UI. - * - * @group layout_builder */ +#[Group('layout_builder')] class LayoutBuilderNestedFormUiTest extends WebDriverTestBase { /** diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderOptInTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderOptInTest.php index f5e106cfc9a..55b3de28f6f 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderOptInTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderOptInTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\layout_builder\Traits\EnableLayoutBuilderTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests the ability for opting in and out of Layout Builder. - * - * @group layout_builder */ +#[Group('layout_builder')] class LayoutBuilderOptInTest extends WebDriverTestBase { use EnableLayoutBuilderTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php index fb237f03a22..33fe433ad31 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php @@ -10,12 +10,12 @@ use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; use Drupal\Tests\system\Traits\OffCanvasTestTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests the Layout Builder UI. - * - * @group layout_builder */ +#[Group('layout_builder')] class LayoutBuilderTest extends WebDriverTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderToolbarTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderToolbarTest.php index 5e0155ae06d..9e7b73fad53 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderToolbarTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderToolbarTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Test Layout Builder integration with Toolbar. - * - * @group layout_builder */ +#[Group('layout_builder')] class LayoutBuilderToolbarTest extends WebDriverTestBase { /** diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php index a9205d9f0dc..f3217671822 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php @@ -7,14 +7,13 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\block_content\Entity\BlockContentType; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore blocknodebundle fieldbody fieldlayout - /** * Tests the Layout Builder UI. - * - * @group layout_builder */ +#[Group('layout_builder')] class LayoutBuilderUiTest extends WebDriverTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php index df748df0e08..238f1d4723c 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php @@ -8,14 +8,13 @@ use Behat\Mink\Element\NodeElement; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore blocknodebundle fieldbody fieldlinks - /** * Tests moving blocks via the form. - * - * @group layout_builder */ +#[Group('layout_builder')] class MoveBlockFormTest extends WebDriverTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/TestMultiWidthLayoutsTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/TestMultiWidthLayoutsTest.php index c7037f99391..7e225dcf73d 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/TestMultiWidthLayoutsTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/TestMultiWidthLayoutsTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\layout_builder\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use PHPUnit\Framework\Attributes\Group; /** * Test the multi-width layout plugins. - * - * @group layout_builder */ +#[Group('layout_builder')] class TestMultiWidthLayoutsTest extends WebDriverTestBase { /** diff --git a/core/modules/layout_builder/tests/src/Unit/Plugin/ConfigAction/Deriver/AddComponentDeriverTest.php b/core/modules/layout_builder/tests/src/Unit/Plugin/ConfigAction/Deriver/AddComponentDeriverTest.php new file mode 100644 index 00000000000..b1545a8f737 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Unit/Plugin/ConfigAction/Deriver/AddComponentDeriverTest.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\layout_builder\Unit\Plugin\ConfigAction\Deriver; + +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\layout_builder\Plugin\ConfigAction\Deriver\AddComponentDeriver; +use Drupal\layout_builder\SectionListInterface; +use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Tests\UnitTestCase; +use Prophecy\PhpUnit\ProphecyTrait; + +/** + * @coversDefaultClass \Drupal\layout_builder\Plugin\ConfigAction\Deriver\AddComponentDeriver + * @group layout_builder + */ +class AddComponentDeriverTest extends UnitTestCase { + + use ProphecyTrait; + + /** + * Tests derivative generation for entities implementing SectionListInterface. + * + * @covers ::getDerivativeDefinitions + */ + public function testGetDerivativeDefinitions(): void { + $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $deriver = new AddComponentDeriver($entityTypeManager->reveal()); + $entity_types = []; + + // Create a mock entity type that implements both required interfaces. + $valid_entity_type = $this->prophesize(EntityTypeInterface::class); + $valid_entity_type->entityClassImplements(ConfigEntityInterface::class)->willReturn(TRUE); + $valid_entity_type->entityClassImplements(SectionListInterface::class)->willReturn(TRUE); + $valid_entity_type->id()->willReturn('valid_type'); + $entity_types['valid_type'] = $valid_entity_type->reveal(); + + // Create a mock entity type that only implements ConfigEntityInterface. + $config_only_type = $this->prophesize(EntityTypeInterface::class); + $config_only_type->entityClassImplements(ConfigEntityInterface::class)->willReturn(TRUE); + $config_only_type->entityClassImplements(SectionListInterface::class)->willReturn(FALSE); + $entity_types['config_only'] = $config_only_type->reveal(); + + // Create a mock entity type that only implements SectionListInterface. + $section_only_type = $this->prophesize(EntityTypeInterface::class); + $section_only_type->entityClassImplements(ConfigEntityInterface::class)->willReturn(FALSE); + $section_only_type->entityClassImplements(SectionListInterface::class)->willReturn(TRUE); + $entity_types['section_only'] = $section_only_type->reveal(); + + $entityTypeManager->getDefinitions()->willReturn($entity_types); + + $base_plugin_definition = []; + $derivatives = $deriver->getDerivativeDefinitions($base_plugin_definition); + + $this->assertCount(1, $derivatives, 'Only one derivative should be generated.'); + $this->assertArrayHasKey('addComponentToLayout', $derivatives, 'The derivative should be keyed by addComponentToLayout.'); + $this->assertEquals(['valid_type'], $derivatives['addComponentToLayout']['entity_types'], 'Only the valid entity type should be included in the derivative definition.'); + } + +} diff --git a/core/modules/link/src/Plugin/migrate/process/FieldLink.php b/core/modules/link/src/Plugin/migrate/process/FieldLink.php index 7509b8685f4..15d53779319 100644 --- a/core/modules/link/src/Plugin/migrate/process/FieldLink.php +++ b/core/modules/link/src/Plugin/migrate/process/FieldLink.php @@ -102,7 +102,7 @@ class FieldLink extends ProcessPluginBase { $link_domains = '[a-z][a-z0-9-]{1,62}'; // Starting a parenthesis group with (?: means that it is grouped, but - // is not captured + // is not captured. $authentication = "(?:(?:(?:[\w\.\-\+!$&'\(\)*\+,;=" . $link_i_chars . "]|%[0-9a-f]{2})+(?::(?:[\w" . $link_i_chars . "\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})*)?)?@)"; $domain = '(?:(?:[a-z0-9' . $link_i_chars . ']([a-z0-9' . $link_i_chars . '\-_\[\]])*)(\.(([a-z0-9' . $link_i_chars . '\-_\[\]])+\.)*(' . $link_domains . '|[a-z]{2}))?)'; $ipv4 = '(?:[0-9]{1,3}(\.[0-9]{1,3}){3})'; diff --git a/core/modules/link/tests/src/Functional/LinkFieldTest.php b/core/modules/link/tests/src/Functional/LinkFieldTest.php index bc5ba831b80..54e4dfbc8a7 100644 --- a/core/modules/link/tests/src/Functional/LinkFieldTest.php +++ b/core/modules/link/tests/src/Functional/LinkFieldTest.php @@ -188,9 +188,9 @@ class LinkFieldTest extends BrowserTestBase { $validation_error_2 = 'Manually entered paths should start with one of the following characters: / ? #'; $validation_error_3 = "The path '@link_path' is inaccessible."; $invalid_external_entries = [ - // Invalid protocol + // Invalid protocol. 'invalid://not-a-valid-protocol' => $validation_error_1, - // Missing host name + // Missing host name. 'http://' => $validation_error_1, ]; $invalid_internal_entries = [ diff --git a/core/modules/link/tests/src/Functional/Views/LinkViewsTokensTest.php b/core/modules/link/tests/src/Functional/Views/LinkViewsTokensTest.php index 159865cc8ea..c73347cc333 100644 --- a/core/modules/link/tests/src/Functional/Views/LinkViewsTokensTest.php +++ b/core/modules/link/tests/src/Functional/Views/LinkViewsTokensTest.php @@ -86,16 +86,16 @@ class LinkViewsTokensTest extends ViewTestBase { $this->drupalGet('test_link_tokens'); foreach ($uris as $uri => $title) { - // Formatted link: {{ field_link }}<br /> + // Formatted link: "{{ field_link }}<br />". $this->assertSession()->responseContains("Formatted: <a href=\"$uri\" class=\"test-link-class\">$title</a>"); - // Raw uri: {{ field_link__uri }}<br /> + // Raw uri: "{{ field_link__uri }}<br />". $this->assertSession()->responseContains("Raw uri: $uri"); - // Raw title: {{ field_link__title }}<br /> + // Raw title: "{{ field_link__title }}<br />". $this->assertSession()->responseContains("Raw title: $title"); - // Raw options: {{ field_link__options }}<br /> + // Raw options: "{{ field_link__options }}<br />". // Options is an array and should return empty after token replace. $this->assertSession()->responseContains("Raw options: ."); } diff --git a/core/modules/link/tests/src/FunctionalJavascript/LinkFieldFormStatesTest.php b/core/modules/link/tests/src/FunctionalJavascript/LinkFieldFormStatesTest.php index be20978ea8f..2942c03c71b 100644 --- a/core/modules/link/tests/src/FunctionalJavascript/LinkFieldFormStatesTest.php +++ b/core/modules/link/tests/src/FunctionalJavascript/LinkFieldFormStatesTest.php @@ -5,13 +5,14 @@ declare(strict_types=1); namespace Drupal\Tests\link\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; /** * Tests link field form states functionality. - * - * @group link - * @group #slow */ +#[Group('link')] +#[Group('#slow')] class LinkFieldFormStatesTest extends WebDriverTestBase { /** @@ -40,8 +41,9 @@ class LinkFieldFormStatesTest extends WebDriverTestBase { } /** - * @dataProvider linkFieldFormStatesData - */ + * Tests link field form states. + */ + #[DataProvider('linkFieldFormStatesData')] public function testLinkFieldFormStates(string $uri, string $title): void { $this->drupalGet('entity_test/add'); $session = $this->assertSession(); diff --git a/core/modules/link/tests/src/Kernel/LinkFormatterTest.php b/core/modules/link/tests/src/Kernel/LinkFormatterTest.php new file mode 100644 index 00000000000..8ebda26d9c5 --- /dev/null +++ b/core/modules/link/tests/src/Kernel/LinkFormatterTest.php @@ -0,0 +1,132 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\link\Kernel; + +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; +use Drupal\user\Entity\Role; +use Drupal\user\RoleInterface; + +/** + * Tests the Field Formatter for the link field type. + * + * @group link + */ +class LinkFormatterTest extends EntityKernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + protected static $modules = ['link']; + + /** + * The entity type used in this test. + * + * @var string + */ + protected string $entityType = 'entity_test'; + + /** + * The bundle used in this test. + * + * @var string + */ + protected string $bundle = 'entity_test'; + + /** + * The name of the field used in this test. + * + * @var string + */ + protected string $fieldName = 'field_test'; + + /** + * The entity to be tested. + * + * @var \Drupal\Core\Entity\EntityInterface + */ + protected $entity; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Use Stark theme for testing markup output. + \Drupal::service('theme_installer')->install(['stark']); + $this->config('system.theme')->set('default', 'stark')->save(); + $this->installEntitySchema('entity_test'); + // Grant the 'view test entity' permission. + $this->installConfig(['user']); + Role::load(RoleInterface::ANONYMOUS_ID) + ->grantPermission('view test entity') + ->save(); + + FieldStorageConfig::create([ + 'field_name' => $this->fieldName, + 'type' => 'link', + 'entity_type' => $this->entityType, + 'cardinality' => 1, + ])->save(); + + FieldConfig::create([ + 'field_name' => $this->fieldName, + 'entity_type' => $this->entityType, + 'bundle' => $this->bundle, + 'label' => 'Field test', + ])->save(); + } + + /** + * Tests the link formatters. + * + * @param string $formatter + * The name of the link formatter to test. + * + * @dataProvider providerLinkFormatter + */ + public function testLinkFormatter(string $formatter): void { + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityType) + ->create([ + 'name' => $this->randomMachineName(), + $this->fieldName => [ + 'uri' => 'https://www.drupal.org/', + 'title' => 'Hello world', + 'options' => [ + 'attributes' => [ + 'class' => 'classy', + 'onmouseover' => 'alert(document.cookie)', + ], + ], + ], + ]); + $entity->save(); + + $build = $entity->get($this->fieldName)->view(['type' => $formatter]); + + $renderer = $this->container->get('renderer'); + $renderer->renderRoot($build[0]); + + $output = (string) $build[0]['#markup']; + $this->assertStringContainsString('<a href="https://www.drupal.org/" class="classy">', $output); + $this->assertStringNotContainsString('onmouseover=', $output); + } + + /** + * Data provider for ::testLinkFormatter. + */ + public static function providerLinkFormatter(): array { + return [ + 'default formatter' => ['link'], + 'separate link text and URL' => ['link_separate'], + ]; + } + +} diff --git a/core/modules/link/tests/src/Unit/AttributeXssTest.php b/core/modules/link/tests/src/Unit/AttributeXssTest.php new file mode 100644 index 00000000000..cbf01a40f60 --- /dev/null +++ b/core/modules/link/tests/src/Unit/AttributeXssTest.php @@ -0,0 +1,126 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\link\Unit; + +use Drupal\link\AttributeXss; +use Drupal\Tests\UnitTestCase; + +/** + * Tests AttributeXss. + * + * @group link + * @covers \Drupal\link\AttributeXss + */ +final class AttributeXssTest extends UnitTestCase { + + /** + * Covers ::sanitizeAttributes. + * + * @dataProvider providerSanitizeAttributes + */ + public function testSanitizeAttributes(array $attributes, array $expected): void { + self::assertSame($expected, AttributeXss::sanitizeAttributes($attributes)); + } + + /** + * Data provider for ::testSanitizeAttributes. + * + * @return \Generator + * Test cases. + */ + public static function providerSanitizeAttributes(): \Generator { + yield 'safe' => [ + ['class' => ['foo', 'bar'], 'data-biscuit' => TRUE], + ['class' => ['foo', 'bar'], 'data-biscuit' => TRUE], + ]; + + yield 'valueless' => [ + ['class' => ['foo', 'bar'], 'selected' => ''], + ['class' => ['foo', 'bar'], 'selected' => ''], + ]; + + yield 'empty names' => [ + ['class' => ['foo', 'bar'], '' => 'live', ' ' => TRUE], + ['class' => ['foo', 'bar']], + ]; + + yield 'only empty names' => [ + ['' => 'live', ' ' => TRUE], + [], + ]; + + yield 'valueless, mangled with a space' => [ + ['class' => ['foo', 'bar'], 'selected href' => 'http://example.com'], + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'], + ]; + + yield 'valueless, mangled with a space, blocked' => [ + ['class' => ['foo', 'bar'], 'selected onclick href' => 'http://example.com'], + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'], + ]; + + yield 'with encoding' => [ + ['class' => ['foo', 'bar'], 'data-how-good' => "It's the bee's knees"], + ['class' => ['foo', 'bar'], 'data-how-good' => "It's the bee's knees"], + ]; + + yield 'valueless, mangled with multiple spaces, blocked' => [ + ['class' => ['foo', 'bar'], 'selected onclick href' => 'http://example.com'], + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'], + ]; + + yield 'valueless, mangled with multiple spaces, blocked, mangled first' => [ + ['selected onclick href' => 'http://example.com', 'class' => ['foo', 'bar']], + ['selected' => 'selected', 'href' => 'http://example.com', 'class' => ['foo', 'bar']], + ]; + + yield 'valueless but with value' => [ + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'], + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'], + ]; + + yield 'valueless but with value, bad protocol' => [ + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'javascript:alert()'], + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'alert()'], + ]; + + yield 'valueless, mangled with a space and bad protocol' => [ + ['class' => ['foo', 'bar'], 'selected href' => 'javascript:alert()'], + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'alert()'], + ]; + + yield 'valueless, mangled with a space and bad protocol, repeated' => [ + ['class' => ['foo', 'bar'], 'selected href' => 'javascript:alert()', 'href' => 'http://example.com'], + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'alert()'], + ]; + + yield 'with a space' => [ + ['class' => ['foo', 'bar'], 'href' => \urlencode('some file.pdf')], + ['class' => ['foo', 'bar'], 'href' => 'some+file.pdf'], + ]; + + yield 'with an unencoded space' => [ + ['class' => ['foo', 'bar'], 'href' => 'some file.pdf'], + ['class' => ['foo', 'bar'], 'href' => 'some file.pdf'], + ]; + + yield 'xss onclick' => [ + ['class' => ['foo', 'bar'], 'onclick' => 'alert("whoop");'], + ['class' => ['foo', 'bar']], + ]; + + yield 'xss onclick, valueless, mangled with a space' => [ + ['class' => ['foo', 'bar'], 'selected onclick href' => 'http://example.com'], + ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'], + ]; + + yield 'xss protocol' => [ + ['class' => ['foo', 'bar'], 'src' => 'javascript:alert("whoop");'], + ['class' => ['foo', 'bar'], 'src' => 'alert("whoop");'], + ]; + + } + +} diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 0cb5e905856..dd3ad4eee75 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -468,7 +468,8 @@ function locale_translate_file_attach_properties($file, array $options = []) { } // Extract project, version and language code from the file name. Supported: - // {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po + // "{project}-{version}.{langcode}.po", "{prefix}.{langcode}.po" or + // "{langcode}.po". preg_match('! ( # project OR project and version OR empty (group 1) ([a-z_]+) # project name (group 2) diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index d9ae64af89a..6d43d44ce9f 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -56,7 +56,7 @@ function locale_translation_build_projects() { // to fall back to the latest stable release for that branch. if (isset($data['info']['version']) && strpos($data['info']['version'], '-dev')) { if (preg_match("/^(\d+\.x-\d+\.).*$/", $data['info']['version'], $matches)) { - // Example matches: 8.x-1.x-dev, 8.x-1.0-alpha1+5-dev => 8.x-1.x + // Example matches: "8.x-1.x-dev", "8.x-1.0-alpha1+5-dev => 8.x-1.x". $data['info']['version'] = $matches[1] . 'x'; } elseif (preg_match("/^(\d+\.\d+\.).*$/", $data['info']['version'], $matches)) { diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index d3377f3773e..87b2c223b2a 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -123,7 +123,7 @@ function locale_schema(): array { 'customized' => [ 'type' => 'int', 'not null' => TRUE, - // LOCALE_NOT_CUSTOMIZED + // LOCALE_NOT_CUSTOMIZED. 'default' => 0, 'description' => 'Boolean indicating whether the translation is custom to this site.', ], diff --git a/core/modules/locale/tests/src/Functional/LocalePluralFormatTest.php b/core/modules/locale/tests/src/Functional/LocalePluralFormatTest.php index 1bb919413b0..d4c42f730bd 100644 --- a/core/modules/locale/tests/src/Functional/LocalePluralFormatTest.php +++ b/core/modules/locale/tests/src/Functional/LocalePluralFormatTest.php @@ -221,7 +221,7 @@ class LocalePluralFormatTest extends BrowserTestBase { $this->drupalGet($path); $this->submitForm($edit, 'Save translations'); - // User interface input for translating seconds should not be duplicated + // User interface input for translating seconds should not be duplicated. $this->assertSession()->pageTextContainsOnce('@count seconds'); // Member for time should be translated. Change the created time to ensure diff --git a/core/modules/media/media.module b/core/modules/media/media.module index dbf031d16a9..69cb62eddc5 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -7,8 +7,6 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Hook\Attribute\ProceduralHookScanStop; use Drupal\Core\Render\Element; -use Drupal\Core\Render\Element\RenderElementBase; -use Drupal\Core\Template\Attribute; use Drupal\Core\Url; /** @@ -35,31 +33,6 @@ function template_preprocess_media(array &$variables): void { } /** - * Implements hook_preprocess_HOOK() for media reference widgets. - */ -function media_preprocess_media_reference_help(&$variables): void { - // Most of these attribute checks are copied from - // template_preprocess_fieldset(). Our template extends - // field-multiple-value-form.html.twig to provide our help text, but also - // groups the information within a semantic fieldset with a legend. So, we - // incorporate parity for both. - $element = $variables['element']; - Element::setAttributes($element, ['id']); - RenderElementBase::setAttributes($element); - $variables['attributes'] = $element['#attributes'] ?? []; - $variables['legend_attributes'] = new Attribute(); - $variables['header_attributes'] = new Attribute(); - $variables['description']['attributes'] = new Attribute(); - $variables['legend_span_attributes'] = new Attribute(); - - if (!empty($element['#media_help'])) { - foreach ($element['#media_help'] as $key => $text) { - $variables[substr($key, 1)] = $text; - } - } -} - -/** * Returns the appropriate URL to add media for the current user. * * @todo Remove in https://www.drupal.org/project/drupal/issues/2938116 diff --git a/core/modules/media/src/Hook/MediaThemeHooks.php b/core/modules/media/src/Hook/MediaThemeHooks.php new file mode 100644 index 00000000000..e6dc956377b --- /dev/null +++ b/core/modules/media/src/Hook/MediaThemeHooks.php @@ -0,0 +1,42 @@ +<?php + +namespace Drupal\media\Hook; + +use Drupal\Core\Template\Attribute; +use Drupal\Core\Render\Element\RenderElementBase; +use Drupal\Core\Render\Element; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for media. + */ +class MediaThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for media reference widgets. + */ + #[Hook('preprocess_media_reference_help')] + public function preprocessMediaReferenceHelp(&$variables): void { + // Most of these attribute checks are copied from + // template_preprocess_fieldset(). Our template extends + // field-multiple-value-form.html.twig to provide our help text, but also + // groups the information within a semantic fieldset with a legend. So, we + // incorporate parity for both. + $element = $variables['element']; + Element::setAttributes($element, [ + 'id', + ]); + RenderElementBase::setAttributes($element); + $variables['attributes'] = $element['#attributes'] ?? []; + $variables['legend_attributes'] = new Attribute(); + $variables['header_attributes'] = new Attribute(); + $variables['description']['attributes'] = new Attribute(); + $variables['legend_span_attributes'] = new Attribute(); + if (!empty($element['#media_help'])) { + foreach ($element['#media_help'] as $key => $text) { + $variables[substr($key, 1)] = $text; + } + } + } + +} diff --git a/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php index 5fa53b2b9e9..0230cc19b46 100644 --- a/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php +++ b/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php @@ -7,6 +7,8 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Widget; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\media\Entity\MediaType; use Drupal\media\Plugin\media\Source\OEmbedInterface; @@ -28,24 +30,35 @@ class OEmbedWidget extends StringTextfieldWidget { /** * {@inheritdoc} */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $element = parent::formElement($items, $delta, $element, $form, $form_state); + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface { + $widget = parent::singleElementObject($items, $delta, $widget, $form, $form_state); + $value = $widget->getChild('value'); + $value->description = $this->getValueDescription($items, $value->description); + return $widget; + } + /** + * Merges description and provider messages. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * FieldItemList containing the values to be edited. + * @param scalar|\Stringable|\Drupal\Core\Render\RenderableInterface|array $description + * The description on the form element. + * + * @return string|array + * The description on the value child. + */ + protected function getValueDescription(FieldItemListInterface $items, mixed $description): string|array { /** @var \Drupal\media\Plugin\media\Source\OEmbedInterface $source */ $source = $items->getEntity()->getSource(); $message = $this->t('You can link to media from the following services: @providers', ['@providers' => implode(', ', $source->getProviders())]); - - if (!empty($element['value']['#description'])) { - $element['value']['#description'] = [ + if ($description) { + return [ '#theme' => 'item_list', - '#items' => [$element['value']['#description'], $message], + '#items' => [$description, $message], ]; } - else { - $element['value']['#description'] = $message; - } - - return $element; + return $message; } /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php index b1fc5baaf7d..22fe95ece62 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php @@ -11,12 +11,12 @@ use Drupal\field\Entity\FieldStorageConfig; use Drupal\media\Entity\Media; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; +use PHPUnit\Framework\Attributes\Group; /** * Basic display tests for Media. - * - * @group media */ +#[Group('media')] class MediaDisplayTest extends MediaJavascriptTestBase { /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiAddTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiAddTest.php index 554fc35e37c..4825687abfa 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiAddTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiAddTest.php @@ -4,17 +4,24 @@ declare(strict_types=1); namespace Drupal\Tests\media\FunctionalJavascript; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; + /** - * @covers ::media_filter_format_edit_form_validate - * @group media - * @group #slow + * Tests Media Embed Filter Configuration Ui Add. + * + * @legacy-covers ::media_filter_format_edit_form_validate */ +#[Group('media')] +#[Group('#slow')] class MediaEmbedFilterConfigurationUiAddTest extends MediaEmbedFilterTestBase { /** - * @covers \Drupal\media\Hook\MediaHooks::formFilterFormatAddFormAlter - * @dataProvider providerTestValidations + * Tests validation when adding. + * + * @legacy-covers \Drupal\media\Hook\MediaHooks::formFilterFormatAddFormAlter */ + #[DataProvider('providerTestValidations')] public function testValidationWhenAdding($filter_html_status, $filter_align_status, $filter_caption_status, $filter_html_image_secure_status, $media_embed, $allowed_html, $expected_error_message): void { $this->drupalGet('admin/config/content/formats/add'); diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiEditTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiEditTest.php index bcc62c73621..ce006de76ec 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiEditTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiEditTest.php @@ -4,17 +4,24 @@ declare(strict_types=1); namespace Drupal\Tests\media\FunctionalJavascript; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; + /** - * @covers ::media_filter_format_edit_form_validate - * @group media - * @group #slow + * Tests Media Embed Filter Configuration Ui Edit. + * + * @legacy-covers ::media_filter_format_edit_form_validate */ +#[Group('media')] +#[Group('#slow')] class MediaEmbedFilterConfigurationUiEditTest extends MediaEmbedFilterTestBase { /** - * @covers \Drupal\media\Hook\MediaHooks::formFilterFormatEditFormAlter - * @dataProvider providerTestValidations + * Tests validation when editing. + * + * @legacy-covers \Drupal\media\Hook\MediaHooks::formFilterFormatEditFormAlter */ + #[DataProvider('providerTestValidations')] public function testValidationWhenEditing($filter_html_status, $filter_align_status, $filter_caption_status, $filter_html_image_secure_status, $media_embed, $allowed_html, $expected_error_message): void { $this->drupalGet('admin/config/content/formats/manage/media_embed_test'); diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterTestBase.php b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterTestBase.php index 57d5278ee1d..097ddb6b310 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterTestBase.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterTestBase.php @@ -9,7 +9,7 @@ use Drupal\filter\Entity\FilterFormat; /** * Base class for media embed filter configuration tests. */ -class MediaEmbedFilterTestBase extends MediaJavascriptTestBase { +abstract class MediaEmbedFilterTestBase extends MediaJavascriptTestBase { /** * {@inheritdoc} diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaReferenceFieldHelpTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaReferenceFieldHelpTest.php index 80d438685dd..383ea8e7a60 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaReferenceFieldHelpTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaReferenceFieldHelpTest.php @@ -4,11 +4,12 @@ declare(strict_types=1); namespace Drupal\Tests\media\FunctionalJavascript; +use PHPUnit\Framework\Attributes\Group; + /** * Tests related to media reference fields. - * - * @group media */ +#[Group('media')] class MediaReferenceFieldHelpTest extends MediaJavascriptTestBase { /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php index 24c071a501c..379ff5f1b35 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php @@ -8,12 +8,12 @@ use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\file\Entity\File; +use PHPUnit\Framework\Attributes\Group; /** * Tests the Audio and Video media sources. - * - * @group media */ +#[Group('media')] class MediaSourceAudioVideoTest extends MediaSourceTestBase { /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceFileTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceFileTest.php index dfb784ba073..6d658a38815 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceFileTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceFileTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\media\FunctionalJavascript; use Drupal\media\Entity\Media; use Drupal\media\Plugin\media\Source\File; +use PHPUnit\Framework\Attributes\Group; /** * Tests the file media source. - * - * @group media */ +#[Group('media')] class MediaSourceFileTest extends MediaSourceTestBase { /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php index a21aec35012..8e9a32cab5c 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php @@ -12,12 +12,12 @@ use Drupal\media\Entity\MediaType; use Drupal\media\Plugin\media\Source\Image; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; /** * Tests the image media source. - * - * @group media */ +#[Group('media')] class MediaSourceImageTest extends MediaSourceTestBase { /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedVideoTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedVideoTest.php index fdbf1151882..67ffe424eab 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedVideoTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedVideoTest.php @@ -4,23 +4,22 @@ declare(strict_types=1); namespace Drupal\Tests\media\FunctionalJavascript; -use Drupal\Core\Session\AccountInterface; use Drupal\Core\Database\Database; +use Drupal\Core\Session\AccountInterface; use Drupal\dblog\Controller\DbLogController; use Drupal\media\Entity\Media; use Drupal\media\Entity\MediaType; use Drupal\media_test_oembed\Controller\ResourceController; use Drupal\Tests\media\Traits\OEmbedTestTrait; use Drupal\user\Entity\Role; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\DependencyInjection\ContainerInterface; // cspell:ignore dailymotion Schipulcon - /** * Tests the oembed:video media source. - * - * @group media */ +#[Group('media')] class MediaSourceOEmbedVideoTest extends MediaSourceTestBase { /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php index 72fa18190b7..36585f409aa 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php @@ -11,14 +11,13 @@ use Drupal\field\Entity\FieldStorageConfig; use Drupal\media_test_oembed\Controller\ResourceController; use Drupal\node\Entity\Node; use Drupal\Tests\media\Traits\OEmbedTestTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore Drupalin Hustlin Schipulcon - /** * Basic tests for Media configuration in the standard profile. - * - * @group media */ +#[Group('media')] class MediaStandardProfileTest extends MediaJavascriptTestBase { use OEmbedTestTrait; diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php index 699d97e9a18..0a2fa8c4a3c 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php @@ -5,14 +5,13 @@ declare(strict_types=1); namespace Drupal\Tests\media\FunctionalJavascript; use Drupal\Component\Utility\Html; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore pastafazoul - /** * Tests the media type creation. - * - * @group media */ +#[Group('media')] class MediaTypeCreationTest extends MediaJavascriptTestBase { /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php index 75724223423..a648eaaa8de 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php @@ -8,12 +8,12 @@ use Drupal\field\FieldConfigInterface; use Drupal\media\Entity\Media; use Drupal\media\Entity\MediaType; use Drupal\media\MediaSourceInterface; +use PHPUnit\Framework\Attributes\Group; /** * Ensures that media UI works correctly. - * - * @group media */ +#[Group('media')] class MediaUiJavascriptTest extends MediaJavascriptTestBase { /** diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php index 1654f5879b2..da0b9fa9ef8 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php @@ -5,15 +5,15 @@ declare(strict_types=1); namespace Drupal\Tests\media\FunctionalJavascript; use Drupal\views\Views; +use PHPUnit\Framework\Attributes\Group; /** * Tests the media entity type integration into the wizard. * - * @group media - * * @see \Drupal\media\Plugin\views\wizard\Media * @see \Drupal\media\Plugin\views\wizard\MediaRevision */ +#[Group('media')] class MediaViewsWizardTest extends MediaJavascriptTestBase { /** diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module index 2bd0c676c36..dfee569978d 100644 --- a/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -8,7 +8,6 @@ use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; -use Drupal\Core\Template\Attribute; use Drupal\image\Entity\ImageStyle; use Drupal\image\Plugin\Field\FieldType\ImageItem; use Drupal\media\MediaTypeInterface; @@ -46,26 +45,6 @@ function template_preprocess_media_library_item(array &$variables): void { } /** - * Implements hook_preprocess_media(). - */ -function media_library_preprocess_media(&$variables): void { - if ($variables['view_mode'] === 'media_library') { - /** @var \Drupal\media\MediaInterface $media */ - $media = $variables['media']; - $variables['#cache']['contexts'][] = 'user.permissions'; - $rel = $media->access('edit') ? 'edit-form' : 'canonical'; - $variables['url'] = $media->toUrl($rel, [ - 'language' => $media->language(), - ]); - $variables += [ - 'preview_attributes' => new Attribute(), - 'metadata_attributes' => new Attribute(), - ]; - $variables['status'] = $media->isPublished(); - } -} - -/** * Implements hook_preprocess_views_view() for the 'media_library' view. */ function media_library_preprocess_views_view__media_library(array &$variables): void { @@ -73,19 +52,6 @@ function media_library_preprocess_views_view__media_library(array &$variables): } /** - * Implements hook_preprocess_views_view_fields(). - */ -function media_library_preprocess_views_view_fields(&$variables): void { - // Add classes to media rendered entity field so it can be targeted for - // JavaScript mouseover and click events. - if ($variables['view']->id() === 'media_library' && isset($variables['fields']['rendered_entity'])) { - if (isset($variables['fields']['rendered_entity']->wrapper_attributes)) { - $variables['fields']['rendered_entity']->wrapper_attributes->addClass('js-click-to-select-trigger'); - } - } -} - -/** * Form #after_build callback for media_library view's exposed filters form. */ function _media_library_views_form_media_library_after_build(array $form, FormStateInterface $form_state) { diff --git a/core/modules/media_library/src/Form/FileUploadForm.php b/core/modules/media_library/src/Form/FileUploadForm.php index e9e25f108fc..892f76dbc10 100644 --- a/core/modules/media_library/src/Form/FileUploadForm.php +++ b/core/modules/media_library/src/Form/FileUploadForm.php @@ -166,7 +166,7 @@ class FileUploadForm extends AddFormBase { '#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']), '#upload_validators' => $item->getUploadValidators(), // Set multiple to true only if available slots is not exactly one - // to ensure correct language (singular or plural) in UI + // to ensure correct language (singular or plural) in UI. '#multiple' => $slots != 1 ? TRUE : FALSE, // Do not limit the number uploaded. There is validation based on the // number selected in the media library that prevents overages. diff --git a/core/modules/media_library/src/Hook/MediaLibraryThemeHooks.php b/core/modules/media_library/src/Hook/MediaLibraryThemeHooks.php new file mode 100644 index 00000000000..68890191ef8 --- /dev/null +++ b/core/modules/media_library/src/Hook/MediaLibraryThemeHooks.php @@ -0,0 +1,48 @@ +<?php + +namespace Drupal\media_library\Hook; + +use Drupal\Core\Template\Attribute; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for media_library. + */ +class MediaLibraryThemeHooks { + + /** + * Implements hook_preprocess_media(). + */ + #[Hook('preprocess_media')] + public function preprocessMedia(&$variables): void { + if ($variables['view_mode'] === 'media_library') { + /** @var \Drupal\media\MediaInterface $media */ + $media = $variables['media']; + $variables['#cache']['contexts'][] = 'user.permissions'; + $rel = $media->access('edit') ? 'edit-form' : 'canonical'; + $variables['url'] = $media->toUrl($rel, [ + 'language' => $media->language(), + ]); + $variables += [ + 'preview_attributes' => new Attribute(), + 'metadata_attributes' => new Attribute(), + ]; + $variables['status'] = $media->isPublished(); + } + } + + /** + * Implements hook_preprocess_views_view_fields(). + */ + #[Hook('preprocess_views_view_fields')] + public function preprocessViewsViewFields(&$variables): void { + // Add classes to media rendered entity field so it can be targeted for + // JavaScript mouseover and click events. + if ($variables['view']->id() === 'media_library' && isset($variables['fields']['rendered_entity'])) { + if (isset($variables['fields']['rendered_entity']->wrapper_attributes)) { + $variables['fields']['rendered_entity']->wrapper_attributes->addClass('js-click-to-select-trigger'); + } + } + } + +} diff --git a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php index 3b36ee5e377..33ffe1a3992 100644 --- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php @@ -18,6 +18,7 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -78,6 +79,8 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface * The widget settings. * @param array $third_party_settings * Any third party settings. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfoManager + * The element info manager. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity type manager service. * @param \Drupal\Core\Session\AccountInterface $current_user @@ -85,8 +88,8 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, ModuleHandlerInterface $module_handler) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $elementInfoManager, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, ModuleHandlerInterface $module_handler) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $this->entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; $this->moduleHandler = $module_handler; @@ -102,6 +105,7 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], + $container->get('plugin.manager.element_info'), $container->get('entity_type.manager'), $container->get('current_user'), $container->get('module_handler') diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/ContentModerationTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/ContentModerationTest.php index 28f123a946c..0bdb75e7ec1 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/ContentModerationTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/ContentModerationTest.php @@ -12,14 +12,13 @@ use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait; use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore hoglet - /** * Tests media library integration with content moderation. - * - * @group media_library */ +#[Group('media_library')] class ContentModerationTest extends WebDriverTestBase { use ContentModerationTestTrait; diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/EmbeddedFormWidgetTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/EmbeddedFormWidgetTest.php index 1fb1b9a9352..7e51adf769c 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/EmbeddedFormWidgetTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/EmbeddedFormWidgetTest.php @@ -9,12 +9,13 @@ use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\media\Entity\Media; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; /** * Tests media widget nested inside another widget. - * - * @group media_library */ +#[Group('media_library')] class EmbeddedFormWidgetTest extends WebDriverTestBase { use TestFileCreationTrait; @@ -94,9 +95,8 @@ class EmbeddedFormWidgetTest extends WebDriverTestBase { /** * Tests media inside another widget that validates too enthusiastically. - * - * @dataProvider insertionReselectionProvider */ + #[DataProvider('insertionReselectionProvider')] public function testInsertionAndReselection($widget): void { $this->container ->get('entity_display.repository') diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php index ea476c9ee96..bb02c1eca7d 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php @@ -8,12 +8,12 @@ use Drupal\field\Entity\FieldConfig; use Drupal\FunctionalJavascriptTests\SortableTestTrait; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; /** * Tests the Media library entity reference widget. - * - * @group media_library */ +#[Group('media_library')] class EntityReferenceWidgetTest extends MediaLibraryTestBase { use SortableTestTrait; diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/FieldUiIntegrationTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/FieldUiIntegrationTest.php index 7bd1e52628e..1b1378c16aa 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/FieldUiIntegrationTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/FieldUiIntegrationTest.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace Drupal\Tests\media_library\FunctionalJavascript; -// cspell:ignore shatner +use PHPUnit\Framework\Attributes\Group; +// cspell:ignore shatner /** * Tests field UI integration for media library widget. - * - * @group media_library */ +#[Group('media_library')] class FieldUiIntegrationTest extends MediaLibraryTestBase { /** diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaOverviewTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaOverviewTest.php index a88502c6596..535edb25fb0 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaOverviewTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaOverviewTest.php @@ -4,11 +4,12 @@ declare(strict_types=1); namespace Drupal\Tests\media_library\FunctionalJavascript; +use PHPUnit\Framework\Attributes\Group; + /** * Tests the grid-style media overview page. - * - * @group media_library */ +#[Group('media_library')] class MediaOverviewTest extends MediaLibraryTestBase { /** diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/TranslationsTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/TranslationsTest.php index f3dbb33ad1b..0d389b472fe 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/TranslationsTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/TranslationsTest.php @@ -12,12 +12,12 @@ use Drupal\media\Entity\Media; use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait; use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests media library for translatable media. - * - * @group media_library */ +#[Group('media_library')] class TranslationsTest extends WebDriverTestBase { use EntityReferenceFieldCreationTrait; diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/ViewsUiIntegrationTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/ViewsUiIntegrationTest.php index 73df14ec6fc..69043cdd0e2 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/ViewsUiIntegrationTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/ViewsUiIntegrationTest.php @@ -4,11 +4,12 @@ declare(strict_types=1); namespace Drupal\Tests\media_library\FunctionalJavascript; +use PHPUnit\Framework\Attributes\Group; + /** * Tests Media Library's integration with Views UI. - * - * @group media_library */ +#[Group('media_library')] class ViewsUiIntegrationTest extends MediaLibraryTestBase { /** diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetAccessTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetAccessTest.php index 78f4d6acf00..8e2c4411af2 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetAccessTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetAccessTest.php @@ -9,12 +9,12 @@ use Drupal\media\Entity\Media; use Drupal\media_library\MediaLibraryState; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; /** * Tests the media library UI access. - * - * @group media_library */ +#[Group('media_library')] class WidgetAccessTest extends MediaLibraryTestBase { /** diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetAnonymousTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetAnonymousTest.php index d7efb311cda..56a7ddd9008 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetAnonymousTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetAnonymousTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\media_library\FunctionalJavascript; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; /** * Tests that the widget works as expected for anonymous users. - * - * @group media_library */ +#[Group('media_library')] class WidgetAnonymousTest extends MediaLibraryTestBase { /** diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOEmbedTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOEmbedTest.php index 1d53128b704..d6b141f8a8d 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOEmbedTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOEmbedTest.php @@ -7,14 +7,13 @@ namespace Drupal\Tests\media_library\FunctionalJavascript; use Drupal\media\Entity\Media; use Drupal\media_test_oembed\Controller\ResourceController; use Drupal\Tests\media\Traits\OEmbedTestTrait; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore Drupalin Hustlin Schipulcon - /** * Tests that oEmbed media can be added in the Media library's widget. - * - * @group media_library */ +#[Group('media_library')] class WidgetOEmbedTest extends MediaLibraryTestBase { use OEmbedTestTrait; diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOverflowTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOverflowTest.php index 8a13b0e070c..08ec7dd7f5f 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOverflowTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOverflowTest.php @@ -5,16 +5,17 @@ declare(strict_types=1); namespace Drupal\Tests\media_library\FunctionalJavascript; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; /** * Tests that uploads in the 'media_library_widget' works as expected. * - * @group media_library - * @group #slow - * * @todo This test will occasionally fail with SQLite until * https://www.drupal.org/node/3066447 is addressed. */ +#[Group('media_library')] +#[Group('#slow')] class WidgetOverflowTest extends MediaLibraryTestBase { use TestFileCreationTrait; @@ -95,9 +96,8 @@ class WidgetOverflowTest extends MediaLibraryTestBase { * The operation of the button to click. For example, if this is "insert", * the "Save and insert" button will be pressed. If NULL, the "Save" button * will be pressed. - * - * @dataProvider providerWidgetOverflow */ + #[DataProvider('providerWidgetOverflow')] public function testWidgetOverflow(?string $selected_operation): void { // If we want to press the "Save and insert" or "Save and select" buttons, // we need to enable the advanced UI. @@ -138,9 +138,8 @@ class WidgetOverflowTest extends MediaLibraryTestBase { * The operation of the button to click. For example, if this is "insert", * the "Save and insert" button will be pressed. If NULL, the "Save" button * will be pressed. - * - * @dataProvider providerWidgetOverflow */ + #[DataProvider('providerWidgetOverflow')] public function testUnlimitedCardinality(?string $selected_operation): void { if ($selected_operation) { $this->config('media_library.settings')->set('advanced_ui', TRUE)->save(); diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetUploadTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetUploadTest.php index 7e0b3af57f0..736da735412 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetUploadTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetUploadTest.php @@ -6,15 +6,15 @@ namespace Drupal\Tests\media_library\FunctionalJavascript; use Drupal\media\Entity\Media; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests that uploads in the 'media_library_widget' works as expected. * - * @group media_library - * * @todo This test will occasionally fail with SQLite until * https://www.drupal.org/node/3066447 is addressed. */ +#[Group('media_library')] class WidgetUploadTest extends MediaLibraryTestBase { use TestFileCreationTrait; @@ -208,7 +208,7 @@ class WidgetUploadTest extends MediaLibraryTestBase { // Assert we can now only upload one more media item. $this->openMediaLibraryForField('field_twin_media'); $this->switchToMediaType('Four'); - // We set the multiple to FALSE if only one file can be uploaded + // We set the multiple to FALSE if only one file can be uploaded. $this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple')); $assert_session->pageTextContains('One file only.'); $choose_files = $assert_session->elementExists('css', '.form-managed-file'); @@ -559,7 +559,7 @@ class WidgetUploadTest extends MediaLibraryTestBase { $this->openMediaLibraryForField('field_twin_media'); $this->switchToMediaType('Four'); - // We set the multiple to FALSE if only one file can be uploaded + // We set the multiple to FALSE if only one file can be uploaded. $this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple')); $assert_session->pageTextContains('One file only.'); $choose_files = $assert_session->elementExists('css', '.form-managed-file'); diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetViewsTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetViewsTest.php index 09933c865a7..868aedfed4f 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetViewsTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetViewsTest.php @@ -4,11 +4,12 @@ declare(strict_types=1); namespace Drupal\Tests\media_library\FunctionalJavascript; +use PHPUnit\Framework\Attributes\Group; + /** * Tests the views in the media library widget. - * - * @group media_library */ +#[Group('media_library')] class WidgetViewsTest extends MediaLibraryTestBase { /** diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetWithoutTypesTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetWithoutTypesTest.php index 87eaea2af68..1008beadcc7 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetWithoutTypesTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetWithoutTypesTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\media_library\FunctionalJavascript; use Drupal\Core\Url; use Drupal\field_ui\FieldUI; +use PHPUnit\Framework\Attributes\Group; /** * Tests the media library widget when no media types are available. - * - * @group media_library */ +#[Group('media_library')] class WidgetWithoutTypesTest extends MediaLibraryTestBase { /** diff --git a/core/modules/menu_link_content/tests/src/Functional/MenuLinkContentDeleteFormTest.php b/core/modules/menu_link_content/tests/src/Functional/MenuLinkContentDeleteFormTest.php index f9239e7f392..dd68b58273e 100644 --- a/core/modules/menu_link_content/tests/src/Functional/MenuLinkContentDeleteFormTest.php +++ b/core/modules/menu_link_content/tests/src/Functional/MenuLinkContentDeleteFormTest.php @@ -50,7 +50,7 @@ class MenuLinkContentDeleteFormTest extends BrowserTestBase { $this->drupalGet($menu_link->toUrl('delete-form')); $this->assertSession()->pageTextContains("Are you sure you want to delete the custom menu link {$menu_link->label()}?"); $this->assertSession()->linkExists('Cancel'); - // Make sure cancel link points to link edit + // Make sure cancel link points to link edit. $this->assertSession()->linkByHrefExists($menu_link->toUrl('edit-form')->toString()); \Drupal::service('module_installer')->install(['menu_ui']); diff --git a/core/modules/menu_link_content/tests/src/Kernel/MenuLinksTest.php b/core/modules/menu_link_content/tests/src/Kernel/MenuLinksTest.php index 34e0a0eee04..751b93dd92d 100644 --- a/core/modules/menu_link_content/tests/src/Kernel/MenuLinksTest.php +++ b/core/modules/menu_link_content/tests/src/Kernel/MenuLinksTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\Tests\menu_link_content\Kernel; +use Drupal\Core\Link; use Drupal\Core\Menu\MenuTreeParameters; use Drupal\entity_test\Entity\EntityTestExternal; use Drupal\KernelTests\KernelTestBase; @@ -477,4 +478,32 @@ class MenuLinksTest extends KernelTestBase { static::assertIsArray($build); } + /** + * Assert that attributes are filtered. + */ + public function testXssFiltering(): void { + $options = [ + 'menu_name' => 'menu-test', + 'bundle' => 'menu_link_content', + 'link' => [ + [ + 'uri' => 'https://www.drupal.org/', + 'options' => [ + 'attributes' => [ + 'class' => 'classy', + 'onmouseover' => 'alert(document.cookie)', + ], + ], + ], + ], + 'title' => 'Link test', + ]; + $link = MenuLinkContent::create($options); + $link->save(); + assert($link instanceof MenuLinkContent); + $output = Link::fromTextAndUrl($link->getTitle(), $link->getUrlObject())->toString()->getGeneratedLink(); + $this->assertStringContainsString('<a href="https://www.drupal.org/" class="classy">', $output); + $this->assertStringNotContainsString('onmouseover=', $output); + } + } diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module index cf543258433..63f31d60e9d 100644 --- a/core/modules/menu_ui/menu_ui.module +++ b/core/modules/menu_ui/menu_ui.module @@ -223,12 +223,3 @@ function menu_ui_form_node_type_form_builder($entity_type, NodeTypeInterface $ty $type->setThirdPartySetting('menu_ui', 'available_menus', array_values(array_filter($form_state->getValue('menu_options')))); $type->setThirdPartySetting('menu_ui', 'parent', $form_state->getValue('menu_parent')); } - -/** - * Implements hook_preprocess_HOOK() for block templates. - */ -function menu_ui_preprocess_block(&$variables): void { - if ($variables['configuration']['provider'] == 'menu_ui') { - $variables['attributes']['role'] = 'navigation'; - } -} diff --git a/core/modules/menu_ui/src/Hook/MenuUiHooks.php b/core/modules/menu_ui/src/Hook/MenuUiHooks.php index 09e7c99f898..f36dcea8206 100644 --- a/core/modules/menu_ui/src/Hook/MenuUiHooks.php +++ b/core/modules/menu_ui/src/Hook/MenuUiHooks.php @@ -293,6 +293,38 @@ class MenuUiHooks { } /** + * Implements hook_ENTITY_TYPE_delete(). + */ + #[Hook('menu_delete')] + public function menuDelete(EntityInterface $entity): void { + if (!$this->entityTypeManager->hasDefinition('node_type')) { + return; + } + + // Remove the menu from content type third party settings. + $menu_id = $entity->id(); + $parent_prefix = $menu_id . ':'; + $storage = $this->entityTypeManager->getStorage('node_type'); + foreach ($storage->loadMultiple() as $content_type) { + $third_party_settings = $original_third_party_settings = $content_type->getThirdPartySettings('menu_ui'); + if (isset($third_party_settings['available_menus']) && in_array($menu_id, $third_party_settings['available_menus'])) { + $key = array_search($menu_id, $third_party_settings['available_menus']); + if ($key !== FALSE) { + unset($third_party_settings['available_menus'][$key]); + } + $content_type->setThirdPartySetting('menu_ui', 'available_menus', $third_party_settings['available_menus']); + } + if (isset($third_party_settings['parent']) && substr($third_party_settings['parent'], 0, strlen($parent_prefix)) == $parent_prefix) { + $third_party_settings['parent'] = ''; + $content_type->setThirdPartySetting('menu_ui', 'parent', $third_party_settings['parent']); + } + if ($third_party_settings != $original_third_party_settings) { + $content_type->save(); + } + } + } + + /** * Implements hook_system_breadcrumb_alter(). */ #[Hook('system_breadcrumb_alter')] diff --git a/core/modules/menu_ui/src/Hook/MenuUiThemeHooks.php b/core/modules/menu_ui/src/Hook/MenuUiThemeHooks.php new file mode 100644 index 00000000000..e8f8d47aa1f --- /dev/null +++ b/core/modules/menu_ui/src/Hook/MenuUiThemeHooks.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\menu_ui\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for menu_ui. + */ +class MenuUiThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for block templates. + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + if ($variables['configuration']['provider'] == 'menu_ui') { + $variables['attributes']['role'] = 'navigation'; + } + } + +} diff --git a/core/modules/menu_ui/tests/src/FunctionalJavascript/MenuUiJavascriptTest.php b/core/modules/menu_ui/tests/src/FunctionalJavascript/MenuUiJavascriptTest.php index a174b7a33c1..eddb64ab927 100644 --- a/core/modules/menu_ui/tests/src/FunctionalJavascript/MenuUiJavascriptTest.php +++ b/core/modules/menu_ui/tests/src/FunctionalJavascript/MenuUiJavascriptTest.php @@ -9,12 +9,12 @@ use Drupal\system\Entity\Menu; use Drupal\system\MenuStorage; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; use Drupal\Tests\menu_ui\Traits\MenuUiTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests custom menu and menu links operations using the UI. - * - * @group menu_ui */ +#[Group('menu_ui')] class MenuUiJavascriptTest extends WebDriverTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/menu_ui/tests/src/Kernel/MenuDeleteTest.php b/core/modules/menu_ui/tests/src/Kernel/MenuDeleteTest.php new file mode 100644 index 00000000000..3bb81874387 --- /dev/null +++ b/core/modules/menu_ui/tests/src/Kernel/MenuDeleteTest.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\menu_ui\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\menu_ui\Hook\MenuUiHooks; +use Drupal\node\Entity\NodeType; +use Drupal\system\Entity\Menu; + +/** + * Tests the menu_delete hook. + * + * @group menu_ui + */ +class MenuDeleteTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['node', 'menu_ui', 'system']; + + /** + * @covers \Drupal\menu_ui\Hook\MenuUiHooks::menuDelete + * @dataProvider providerMenuDelete + */ + public function testMenuDelete($settings, $expected): void { + $menu = Menu::create([ + 'id' => 'mock', + 'label' => $this->randomMachineName(16), + 'description' => 'Description text', + ]); + $menu->save(); + $content_type = NodeType::create([ + 'status' => TRUE, + 'dependencies' => [ + 'module' => ['menu_ui'], + ], + 'third_party_settings' => [ + 'menu_ui' => $settings, + ], + 'name' => 'Test type', + 'type' => 'test_type', + ]); + $content_type->save(); + $this->assertEquals($settings['available_menus'], $content_type->getThirdPartySetting('menu_ui', 'available_menus')); + $this->assertEquals($settings['parent'], $content_type->getThirdPartySetting('menu_ui', 'parent')); + + $hooks = new MenuUiHooks(\Drupal::entityTypeManager()); + $hooks->menuDelete($menu); + + $content_type = NodeType::load('test_type'); + $this->assertEquals($expected['available_menus'], $content_type->getThirdPartySetting('menu_ui', 'available_menus')); + $this->assertEquals($expected['parent'], $content_type->getThirdPartySetting('menu_ui', 'parent')); + } + + /** + * Provides data for testMenuDelete(). + */ + public static function providerMenuDelete(): array { + return [ + [ + ['available_menus' => ['mock'], 'parent' => 'mock:'], + ['available_menus' => [], 'parent' => ''], + ], + [ + ['available_menus' => ['mock'], 'parent' => 'mock:menu_link_content:e0cd7689-016e-43e4-af8f-7ce82801ab95'], + ['available_menus' => [], 'parent' => ''], + ], + [ + ['available_menus' => ['main', 'mock'], 'parent' => 'mock:'], + ['available_menus' => ['main'], 'parent' => ''], + ], + [ + ['available_menus' => ['main'], 'parent' => 'main:'], + ['available_menus' => ['main'], 'parent' => 'main:'], + ], + [ + ['available_menus' => ['main'], 'parent' => 'main:menu_link_content:e0cd7689-016e-43e4-af8f-7ce82801ab95'], + ['available_menus' => ['main'], 'parent' => 'main:menu_link_content:e0cd7689-016e-43e4-af8f-7ce82801ab95'], + ], + ]; + } + +} diff --git a/core/modules/migrate/migrate.api.php b/core/modules/migrate/migrate.api.php index 1e58b0090ff..5d2af7db180 100644 --- a/core/modules/migrate/migrate.api.php +++ b/core/modules/migrate/migrate.api.php @@ -156,7 +156,7 @@ function hook_migrate_prepare_row(Row $row, MigrateSourceInterface $source, Migr if ($migration->id() == 'd6_filter_formats') { $value = $source->getDatabase()->query('SELECT [value] FROM {variable} WHERE [name] = :name', [':name' => 'my_module_filter_foo_' . $row->getSourceProperty('format')])->fetchField(); if ($value) { - $row->setSourceProperty('settings:my_module:foo', unserialize($value)); + $row->setSourceProperty('settings:my_module:foo', unserialize($value, ['allowed_classes' => FALSE])); } } } @@ -179,7 +179,7 @@ function hook_migrate_prepare_row(Row $row, MigrateSourceInterface $source, Migr function hook_migrate_MIGRATION_ID_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) { $value = $source->getDatabase()->query('SELECT [value] FROM {variable} WHERE [name] = :name', [':name' => 'my_module_filter_foo_' . $row->getSourceProperty('format')])->fetchField(); if ($value) { - $row->setSourceProperty('settings:my_module:foo', unserialize($value)); + $row->setSourceProperty('settings:my_module:foo', unserialize($value, ['allowed_classes' => FALSE])); } } diff --git a/core/modules/migrate/src/MigrateExecutable.php b/core/modules/migrate/src/MigrateExecutable.php index a087ae6875e..2cf3a322351 100644 --- a/core/modules/migrate/src/MigrateExecutable.php +++ b/core/modules/migrate/src/MigrateExecutable.php @@ -106,7 +106,7 @@ class MigrateExecutable implements MigrateExecutableInterface { $this->message = $message ?: new MigrateMessage(); $this->getIdMap()->setMessage($this->message); $this->eventDispatcher = $event_dispatcher; - // Record the memory limit in bytes + // Record the memory limit in bytes. $limit = trim(ini_get('memory_limit')); if ($limit == '-1') { $this->memoryLimit = PHP_INT_MAX; @@ -557,7 +557,7 @@ class MigrateExecutable implements MigrateExecutableInterface { $usage = $this->attemptMemoryReclaim(); $pct_memory = $usage / $this->memoryLimit; // Use a lower threshold - we don't want to be in a situation where we - // keep coming back here and trimming a tiny amount + // keep coming back here and trimming a tiny amount. if ($pct_memory > (0.90 * $threshold)) { $this->message->display( $this->t( diff --git a/core/modules/migrate/src/Plugin/Discovery/AnnotatedClassDiscoveryAutomatedProviders.php b/core/modules/migrate/src/Plugin/Discovery/AnnotatedClassDiscoveryAutomatedProviders.php index 9adf60b46ff..30cc28562e8 100644 --- a/core/modules/migrate/src/Plugin/Discovery/AnnotatedClassDiscoveryAutomatedProviders.php +++ b/core/modules/migrate/src/Plugin/Discovery/AnnotatedClassDiscoveryAutomatedProviders.php @@ -65,7 +65,7 @@ class AnnotatedClassDiscoveryAutomatedProviders extends AnnotatedClassDiscovery if (isset($cached['id'])) { // Explicitly unserialize this to create a new object // instance. - $definitions[$cached['id']] = unserialize($cached['content']); + $definitions[$cached['id']] = unserialize($cached['content'], ['allowed_classes' => FALSE]); } continue; } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/Config.php b/core/modules/migrate/src/Plugin/migrate/destination/Config.php index c77de935a52..2e6c26a1684 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/Config.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/Config.php @@ -234,7 +234,7 @@ class Config extends DestinationBase implements ContainerFactoryPluginInterface, return 'config_translation'; } // Get the module handling this configuration object from the config_name, - // which is of the form <module_name>.<configuration object name> + // which is of the form "<module_name>.<configuration object name>". return !empty($this->configuration['config_name']) ? explode('.', $this->configuration['config_name'], 2)[0] : NULL; } diff --git a/core/modules/migrate/src/Plugin/migrate/source/ConfigEntity.php b/core/modules/migrate/src/Plugin/migrate/source/ConfigEntity.php index dc70496282f..a5351c74862 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/ConfigEntity.php +++ b/core/modules/migrate/src/Plugin/migrate/source/ConfigEntity.php @@ -71,7 +71,8 @@ class ConfigEntity extends SqlBase { * {@inheritdoc} */ public function prepareRow(Row $row) { - $row->setSourceProperty('data', unserialize($row->getSourceProperty('data'))); + // @see \Drupal\Core\Config\DatabaseStorage::decode() + $row->setSourceProperty('data', unserialize($row->getSourceProperty('data'), ['allowed_classes' => FALSE])); return parent::prepareRow($row); } diff --git a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php index 40dea1c7a2b..0aefdd99de2 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php @@ -424,7 +424,7 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi } // If we are tracking changes, we also need to retrieve all rows to compare - // hashes + // hashes. if ($this->trackChanges) { return FALSE; } diff --git a/core/modules/migrate/src/Row.php b/core/modules/migrate/src/Row.php index 3d961902bcd..2b5e8b2fb4e 100644 --- a/core/modules/migrate/src/Row.php +++ b/core/modules/migrate/src/Row.php @@ -267,6 +267,32 @@ class Row { } /** + * Tests if a property is an empty destination. + * + * @param string $property + * The name of the property. + * + * @return bool + * TRUE if the property is an empty destination. + */ + public function hasEmptyDestinationProperty(string $property): bool { + return in_array($property, $this->emptyDestinationProperties); + } + + /** + * Removes an empty destination property. + * + * @param string $property + * The name of the empty destination property. + */ + public function removeEmptyDestinationProperty(string $property): void { + $this->emptyDestinationProperties = array_diff( + $this->emptyDestinationProperties, + [$property], + ); + } + + /** * Returns the whole destination array. * * @return array diff --git a/core/modules/migrate/tests/src/Functional/MigrateMessageFormTest.php b/core/modules/migrate/tests/src/Functional/MigrateMessageFormTest.php index cfbcdf5b4df..22f6c4fbc46 100644 --- a/core/modules/migrate/tests/src/Functional/MigrateMessageFormTest.php +++ b/core/modules/migrate/tests/src/Functional/MigrateMessageFormTest.php @@ -45,7 +45,7 @@ class MigrateMessageFormTest extends MigrateMessageTestBase { $this->assertEquals($expected_count, $count[$level], sprintf('Count for level %s failed', $level)); } - // Reset the filter + // Reset the filter. $this->submitForm([], 'Reset'); $messages = $this->getMessages(); $this->assertCount(4, $messages); diff --git a/core/modules/migrate/tests/src/Functional/MigrateMessageTestBase.php b/core/modules/migrate/tests/src/Functional/MigrateMessageTestBase.php index 6885ba378e9..84dddc3c182 100644 --- a/core/modules/migrate/tests/src/Functional/MigrateMessageTestBase.php +++ b/core/modules/migrate/tests/src/Functional/MigrateMessageTestBase.php @@ -12,10 +12,8 @@ use Drupal\migrate\Plugin\MigrationInterface; /** * Provides base class for testing migrate messages. - * - * @group migrate */ -class MigrateMessageTestBase extends BrowserTestBase { +abstract class MigrateMessageTestBase extends BrowserTestBase { /** * {@inheritdoc} diff --git a/core/modules/migrate/tests/src/Kernel/RowTest.php b/core/modules/migrate/tests/src/Kernel/RowTest.php new file mode 100644 index 00000000000..1b4adf181c8 --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/RowTest.php @@ -0,0 +1,152 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\migrate\Kernel; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate\Event\MigratePreRowSaveEvent; +use Drupal\migrate\Event\MigrateEvents; +use Drupal\migrate\MigrateExecutable; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Tests the Row class. + * + * @group migrate + */ +class RowTest extends KernelTestBase { + + /** + * The event dispatcher. + */ + protected EventDispatcherInterface $eventDispatcher; + + /** + * The entity type manager. + */ + protected EntityTypeManagerInterface $entityTypeManager; + + /** + * The migration manager. + */ + protected MigrationPluginManagerInterface $migrationManager; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'entity_test', + 'field', + 'migrate', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('entity_test'); + + $this->eventDispatcher = \Drupal::service('event_dispatcher'); + $this->entityTypeManager = \Drupal::service('entity_type.manager'); + $this->migrationManager = \Drupal::service('plugin.manager.migration'); + + // Create two fields that will be set during migration. + $fields = ['field1', 'field2']; + foreach ($fields as $field) { + $this->entityTypeManager->getStorage('field_storage_config')->create([ + 'entity_type' => 'entity_test', + 'field_name' => $field, + 'type' => 'string', + ])->save(); + $this->entityTypeManager->getStorage('field_config')->create([ + 'entity_type' => 'entity_test', + 'field_name' => $field, + 'bundle' => 'entity_test', + ])->save(); + } + } + + /** + * Tests the destination properties of the Row class. + */ + public function testRowDestinations(): void { + $storage = $this->entityTypeManager->getStorage('entity_test'); + + // Execute a migration that creates an entity with two fields. + $data_rows = [ + ['id' => 1, 'field1' => 'f1value', 'field2' => 'f2value'], + ]; + $ids = ['id' => ['type' => 'integer']]; + $definition = [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => $data_rows, + 'ids' => $ids, + ], + 'process' => [ + 'id' => 'id', + 'field1' => 'field1', + 'field2' => 'field2', + ], + 'destination' => ['plugin' => 'entity:entity_test'], + ]; + $this->executeMigrationImport($definition); + $entity = $storage->load(1); + $this->assertEquals('f1value', $entity->get('field1')->getValue()[0]['value']); + $this->assertEquals('f2value', $entity->get('field2')->getValue()[0]['value']); + + // Execute a second migration that attempts to remove both field values. + // The event listener prevents the removal of the second field. + $data_rows = [ + ['id' => 1, 'field1' => NULL, 'field2' => NULL], + ]; + $definition['source']['data_rows'] = $data_rows; + $this->eventDispatcher->addListener(MigrateEvents::PRE_ROW_SAVE, [$this, 'preventFieldRemoval']); + $this->executeMigrationImport($definition); + + // The first field is now empty but the second field is still set. + $entity = $storage->load(1); + $this->assertTrue($entity->get('field1')->isEmpty()); + $this->assertEquals('f2value', $entity->get('field2')->getValue()[0]['value']); + } + + /** + * The pre-row-save event handler for the second migration. + * + * Checks row destinations and prevents the removal of the second field. + * + * @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event + * The migration event. + * @param string $name + * The event name. + */ + public function preventFieldRemoval(MigratePreRowSaveEvent $event, string $name): void { + $row = $event->getRow(); + + // Both fields are empty and their existing values will be removed. + $this->assertFalse($row->hasDestinationProperty('field1')); + $this->assertFalse($row->hasDestinationProperty('field2')); + $this->assertTrue($row->hasEmptyDestinationProperty('field1')); + $this->assertTrue($row->hasEmptyDestinationProperty('field2')); + + // Prevent removal of field 2. + $row->removeEmptyDestinationProperty('field2'); + } + + /** + * Executes a migration import for the given migration definition. + * + * @param array $definition + * The migration definition. + */ + protected function executeMigrationImport(array $definition): void { + $migration = $this->migrationManager->createStubMigration($definition); + (new MigrateExecutable($migration))->import(); + } + +} diff --git a/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php b/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php index e344e3e23e8..77e4cf64e64 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php @@ -426,7 +426,7 @@ class MigrateSourceTest extends MigrateTestCase { $migration = $this->getMigration(); $source = new StubSourceGeneratorPlugin([], '', [], $migration); - // Test the default value of the skipCount Value; + // Test the default value of the skipCount Value. $this->assertTrue($source->getSkipCount()); $this->assertTrue($source->getCacheCounts()); $this->assertTrue($source->getTrackChanges()); diff --git a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php index 1cabbebdde6..aefd45d0438 100644 --- a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php +++ b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php @@ -47,7 +47,7 @@ class EntityContentBaseTest extends EntityTestBase { // Syncing should be set once. $entity->setSyncing(Argument::exact(TRUE)) ->shouldBeCalledTimes(1); - // Set an id for the entity + // Set an id for the entity. $entity->id() ->willReturn(5); $destination->setEntity($entity->reveal()); diff --git a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityTestBase.php b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityTestBase.php index ba9ab78cff6..ed223601abb 100644 --- a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityTestBase.php +++ b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityTestBase.php @@ -14,7 +14,7 @@ use Drupal\Tests\UnitTestCase; /** * Base test class for entity migration destination functionality. */ -class EntityTestBase extends UnitTestCase { +abstract class EntityTestBase extends UnitTestCase { /** * The migration entity. diff --git a/core/modules/migrate/tests/src/Unit/RowTest.php b/core/modules/migrate/tests/src/Unit/RowTest.php index 69fce6f7860..7db1a51db43 100644 --- a/core/modules/migrate/tests/src/Unit/RowTest.php +++ b/core/modules/migrate/tests/src/Unit/RowTest.php @@ -292,6 +292,43 @@ class RowTest extends UnitTestCase { } /** + * Tests checking for and removing destination properties that may be empty. + * + * @covers ::hasEmptyDestinationProperty + * @covers ::removeEmptyDestinationProperty + */ + public function testDestinationOrEmptyProperty(): void { + $row = new Row($this->testValues, $this->testSourceIds); + + // Set a destination. + $row->setDestinationProperty('nid', 2); + $this->assertTrue($row->hasDestinationProperty('nid')); + $this->assertFalse($row->hasEmptyDestinationProperty('nid')); + + // Set an empty destination. + $row->setEmptyDestinationProperty('a_property_with_no_value'); + $this->assertTrue($row->hasEmptyDestinationProperty('a_property_with_no_value')); + $this->assertFalse($row->hasDestinationProperty('a_property_with_no_value')); + + // Removing an empty destination that is not actually empty has no effect. + $row->removeEmptyDestinationProperty('nid'); + $this->assertTrue($row->hasDestinationProperty('nid')); + $this->assertFalse($row->hasEmptyDestinationProperty('nid')); + + // Removing a destination that is actually empty has no effect. + $row->removeDestinationProperty('a_property_with_no_value'); + $this->assertTrue($row->hasEmptyDestinationProperty('a_property_with_no_value')); + + // Remove the empty destination. + $row->removeEmptyDestinationProperty('a_property_with_no_value'); + $this->assertFalse($row->hasEmptyDestinationProperty('a_property_with_no_value')); + + // Removing a destination that does not exist does not throw an error. + $this->assertFalse($row->hasEmptyDestinationProperty('not_a_property')); + $row->removeEmptyDestinationProperty('not_a_property'); + } + + /** * Tests setting/getting multiple destination IDs. */ public function testMultipleDestination(): void { diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php index 64ac2031a3f..06d9bbccc97 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php @@ -173,7 +173,7 @@ abstract class DrupalSqlBase extends SqlBase implements DependentPluginInterface catch (\Exception) { $result = FALSE; } - return $result !== FALSE ? unserialize($result) : $default; + return $result !== FALSE ? unserialize($result, ['allowed_classes' => ['stdClass']]) : $default; } /** diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/Variable.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/Variable.php index 0b83a099afb..f99cd69e898 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/source/Variable.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/Variable.php @@ -129,7 +129,10 @@ class Variable extends DrupalSqlBase { // Create an ID field so we can record migration in the map table. // Arbitrarily, use the first variable name. $values['id'] = reset($this->variables); - return $values + array_map('unserialize', $this->prepareQuery()->execute()->fetchAllKeyed()); + return $values + array_map( + fn($data) => unserialize($data, ['allowed_classes' => FALSE]), + $this->prepareQuery()->execute()->fetchAllKeyed(), + ); } /** diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/VariableMultiRow.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/VariableMultiRow.php index ee9b6268ebb..e2bcbddc8d3 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/source/VariableMultiRow.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/VariableMultiRow.php @@ -66,7 +66,7 @@ class VariableMultiRow extends DrupalSqlBase { */ public function prepareRow(Row $row) { if ($value = $row->getSourceProperty('value')) { - $row->setSourceProperty('value', unserialize($value)); + $row->setSourceProperty('value', unserialize($value, ['allowed_classes' => FALSE])); } return parent::prepareRow($row); } diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/VariableTranslation.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/VariableTranslation.php index 11323f4bf7a..f9e6ef61d75 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/VariableTranslation.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/VariableTranslation.php @@ -78,7 +78,7 @@ class VariableTranslation extends DrupalSqlBase { foreach ($result as $i18n_variable) { foreach ($values as $key => $value) { if ($values[$key]['language'] === $i18n_variable->language) { - $values[$key][$i18n_variable->name] = unserialize($i18n_variable->value); + $values[$key][$i18n_variable->name] = unserialize($i18n_variable->value, ['allowed_classes' => FALSE]); break; } } diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/VariableTranslation.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/VariableTranslation.php index 56121db822a..82d638fa3d8 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/VariableTranslation.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/VariableTranslation.php @@ -78,7 +78,7 @@ class VariableTranslation extends DrupalSqlBase { foreach ($values as $key => $value) { if ($values[$key]['language'] === $variable_store['realm_key']) { if ($variable_store['serialized']) { - $values[$key][$variable_store['name']] = unserialize($variable_store['value']); + $values[$key][$variable_store['name']] = unserialize($variable_store['value'], ['allowed_classes' => FALSE]); break; } else { diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d8/Config.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d8/Config.php index b1d409f4017..1bb5b4f51bf 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/source/d8/Config.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d8/Config.php @@ -88,7 +88,8 @@ class Config extends DrupalSqlBase { * {@inheritdoc} */ public function prepareRow(Row $row) { - $row->setSourceProperty('data', unserialize($row->getSourceProperty('data'))); + // @see \Drupal\Core\Config\DatabaseStorage::decode() + $row->setSourceProperty('data', unserialize($row->getSourceProperty('data'), ['allowed_classes' => FALSE])); return parent::prepareRow($row); } diff --git a/core/modules/migrate_drupal_ui/src/Form/ReviewForm.php b/core/modules/migrate_drupal_ui/src/Form/ReviewForm.php index a28dada3e9c..c6e76787f31 100644 --- a/core/modules/migrate_drupal_ui/src/Form/ReviewForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/ReviewForm.php @@ -286,7 +286,7 @@ class ReviewForm extends MigrateUpgradeFormBase { foreach ($migration_state as $source_machine_name => $destination_modules) { $data = NULL; if (isset($this->systemData['module'][$source_machine_name]['info'])) { - $data = unserialize($this->systemData['module'][$source_machine_name]['info']); + $data = unserialize($this->systemData['module'][$source_machine_name]['info'], ['allowed_classes' => FALSE]); } $source_module_name = $data['name'] ?? $source_machine_name; // Get the names of all the destination modules. diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php index 759f59b50a8..a6633fed5e9 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php @@ -269,7 +269,7 @@ abstract class MigrateUpgradeTestBase extends BrowserTestBase { // Convert $source_id into a keyless array so that // \Drupal\migrate\Plugin\migrate\id_map\Sql::getSourceHash() works as // expected. - $source_id_values = array_values(unserialize($source_id)); + $source_id_values = array_values(unserialize($source_id, ['allowed_classes' => FALSE])); $row = $id_map->getRowBySource($source_id_values); $destination = serialize($id_map->currentDestination()); $message = "Migration of $source_id to $destination as part of the {$migration->id()} migration. The source row status is " . $row['source_row_status']; diff --git a/core/modules/migrate_drupal_ui/tests/src/FunctionalJavascript/SettingsTest.php b/core/modules/migrate_drupal_ui/tests/src/FunctionalJavascript/SettingsTest.php index f85771b52b6..db9f32cb6a6 100644 --- a/core/modules/migrate_drupal_ui/tests/src/FunctionalJavascript/SettingsTest.php +++ b/core/modules/migrate_drupal_ui/tests/src/FunctionalJavascript/SettingsTest.php @@ -5,14 +5,14 @@ declare(strict_types=1); namespace Drupal\Tests\migrate_drupal_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore drupalmysqldriverdatabasemysql - /** * Tests migrate upgrade credential form with settings in settings.php. - * - * @group migrate_drupal_ui */ +#[Group('migrate_drupal_ui')] class SettingsTest extends WebDriverTestBase { /** @@ -55,9 +55,8 @@ class SettingsTest extends WebDriverTestBase { * * @throws \Behat\Mink\Exception\ElementNotFoundException * @throws \Behat\Mink\Exception\ExpectationException - * - * @dataProvider providerTestCredentialForm */ + #[DataProvider('providerTestCredentialForm')] public function testCredentialForm($source_connection, $version, array $manual, array $databases, $expected_source_connection): void { // Write settings. $migrate_file_public_path = '/var/www/drupal7/sites/default/files'; diff --git a/core/modules/migrate_drupal_ui/tests/src/Kernel/MigrationLabelExistTest.php b/core/modules/migrate_drupal_ui/tests/src/Kernel/MigrationLabelExistTest.php index 70741c025e9..257d9292a5d 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Kernel/MigrationLabelExistTest.php +++ b/core/modules/migrate_drupal_ui/tests/src/Kernel/MigrationLabelExistTest.php @@ -29,7 +29,7 @@ class MigrationLabelExistTest extends MigrateDrupalTestBase { /** @var \Drupal\migrate\Plugin\MigrationPluginManager $plugin_manager */ $plugin_manager = $this->container->get('plugin.manager.migration'); - // Get all the migrations + // Get all the migrations. $migrations = $plugin_manager->createInstances(array_keys($plugin_manager->getDefinitions())); /** @var \Drupal\migrate\Plugin\Migration $migration */ foreach ($migrations as $migration) { diff --git a/core/modules/mysqli/mysqli.install b/core/modules/mysqli/mysqli.install deleted file mode 100644 index 7f1147d63ad..00000000000 --- a/core/modules/mysqli/mysqli.install +++ /dev/null @@ -1,78 +0,0 @@ -<?php - -/** - * @file - * Install, update and uninstall functions for the mysqli module. - */ - -use Drupal\Core\Database\Database; -use Drupal\Core\Extension\Requirement\RequirementSeverity; -use Drupal\Core\Render\Markup; - -/** - * Implements hook_requirements(). - */ -function mysqli_requirements($phase): array { - $requirements = []; - - if ($phase === 'runtime') { - // Test with MySql databases. - if (Database::isActiveConnection()) { - $connection = Database::getConnection(); - // Only show requirements when MySQLi is the default database connection. - if (!($connection->driver() === 'mysqli' && $connection->getProvider() === 'mysqli')) { - return []; - } - - $query = $connection->isMariaDb() ? 'SELECT @@SESSION.tx_isolation' : 'SELECT @@SESSION.transaction_isolation'; - - $isolation_level = $connection->query($query)->fetchField(); - - $tables_missing_primary_key = []; - $tables = $connection->schema()->findTables('%'); - foreach ($tables as $table) { - $primary_key_column = Database::getConnection()->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name'); - if (empty($primary_key_column)) { - $tables_missing_primary_key[] = $table; - } - } - - $description = []; - if ($isolation_level == 'READ-COMMITTED') { - if (empty($tables_missing_primary_key)) { - $severity_level = RequirementSeverity::OK; - } - else { - $severity_level = RequirementSeverity::Error; - } - } - else { - if ($isolation_level == 'REPEATABLE-READ') { - $severity_level = RequirementSeverity::Warning; - } - else { - $severity_level = RequirementSeverity::Error; - $description[] = t('This is not supported by Drupal.'); - } - $description[] = t('The recommended level for Drupal is "READ COMMITTED".'); - } - - if (!empty($tables_missing_primary_key)) { - $description[] = t('For this to work correctly, all tables must have a primary key. The following table(s) do not have a primary key: @tables.', ['@tables' => implode(', ', $tables_missing_primary_key)]); - } - - $description[] = t('See the <a href=":performance_doc">setting MySQL transaction isolation level</a> page for more information.', [ - ':performance_doc' => 'https://www.drupal.org/docs/system-requirements/setting-the-mysql-transaction-isolation-level', - ]); - - $requirements['mysql_transaction_level'] = [ - 'title' => t('Transaction isolation level'), - 'severity' => $severity_level, - 'value' => $isolation_level, - 'description' => Markup::create(implode(' ', $description)), - ]; - } - } - - return $requirements; -} diff --git a/core/modules/mysqli/src/Hook/MysqliHooks.php b/core/modules/mysqli/src/Hook/MysqliHooks.php index 5fae187d16c..340b17373a1 100644 --- a/core/modules/mysqli/src/Hook/MysqliHooks.php +++ b/core/modules/mysqli/src/Hook/MysqliHooks.php @@ -2,7 +2,10 @@ namespace Drupal\mysqli\Hook; +use Drupal\Core\Database\Database; +use Drupal\Core\Extension\Requirement\RequirementSeverity; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Render\Markup; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -29,4 +32,71 @@ class MysqliHooks { return NULL; } + /** + * Implements hook_runtime_requirements(). + */ + #[Hook('runtime_requirements')] + public function runtimeRequirements(): array { + $requirements = []; + + // Test with MySql databases. + if (Database::isActiveConnection()) { + $connection = Database::getConnection(); + // Only show requirements when MySQLi is the default database connection. + if (!($connection->driver() === 'mysqli' && $connection->getProvider() === 'mysqli')) { + return []; + } + + $query = $connection->isMariaDb() ? 'SELECT @@SESSION.tx_isolation' : 'SELECT @@SESSION.transaction_isolation'; + + $isolation_level = $connection->query($query)->fetchField(); + + $tables_missing_primary_key = []; + $tables = $connection->schema()->findTables('%'); + foreach ($tables as $table) { + $primary_key_column = Database::getConnection()->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name'); + if (empty($primary_key_column)) { + $tables_missing_primary_key[] = $table; + } + } + + $description = []; + if ($isolation_level == 'READ-COMMITTED') { + if (empty($tables_missing_primary_key)) { + $severity_level = RequirementSeverity::OK; + } + else { + $severity_level = RequirementSeverity::Error; + } + } + else { + if ($isolation_level == 'REPEATABLE-READ') { + $severity_level = RequirementSeverity::Warning; + } + else { + $severity_level = RequirementSeverity::Error; + $description[] = $this->t('This is not supported by Drupal.'); + } + $description[] = $this->t('The recommended level for Drupal is "READ COMMITTED".'); + } + + if (!empty($tables_missing_primary_key)) { + $description[] = $this->t('For this to work correctly, all tables must have a primary key. The following table(s) do not have a primary key: @tables.', ['@tables' => implode(', ', $tables_missing_primary_key)]); + } + + $description[] = $this->t('See the <a href=":performance_doc">setting MySQL transaction isolation level</a> page for more information.', [ + ':performance_doc' => 'https://www.drupal.org/docs/system-requirements/setting-the-mysql-transaction-isolation-level', + ]); + + $requirements['mysql_transaction_level'] = [ + 'title' => $this->t('Transaction isolation level'), + 'severity' => $severity_level, + 'value' => $isolation_level, + 'description' => Markup::create(implode(' ', $description)), + ]; + } + + return $requirements; + } + } diff --git a/core/modules/navigation/config/schema/navigation.schema.yml b/core/modules/navigation/config/schema/navigation.schema.yml index ee31849202a..b4a2eca5dbd 100644 --- a/core/modules/navigation/config/schema/navigation.schema.yml +++ b/core/modules/navigation/config/schema/navigation.schema.yml @@ -32,22 +32,19 @@ navigation.settings: label: 'Maximum file sizes (bytes)' constraints: NotNull: [ ] - Range: - min: 0 + PositiveOrZero: ~ height: type: integer label: 'Logo expected height' constraints: NotNull: [ ] - Range: - min: 0 + PositiveOrZero: ~ width: type: integer label: 'Logo expected width' constraints: NotNull: [ ] - Range: - min: 0 + PositiveOrZero: ~ constraints: ValidKeys: '<infer>' constraints: diff --git a/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestHooks.php b/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestHooks.php index b61a694e80f..0bdede27721 100644 --- a/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestHooks.php +++ b/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestHooks.php @@ -97,7 +97,7 @@ class NavigationTestHooks { unset($tree[$key]); } - // Updates title for items in menu2 + // Updates title for items in menu2. if ($menu_name == 'menu2') { $item->link->updateLink(['title' => 'New Link Title'], FALSE); } diff --git a/core/modules/navigation/tests/src/Functional/NavigationLinkBlockTest.php b/core/modules/navigation/tests/src/Functional/NavigationLinkBlockTest.php index 0f385c5f1ca..f40d63935ea 100644 --- a/core/modules/navigation/tests/src/Functional/NavigationLinkBlockTest.php +++ b/core/modules/navigation/tests/src/Functional/NavigationLinkBlockTest.php @@ -162,7 +162,7 @@ class NavigationLinkBlockTest extends PageCacheTagsTestBase { $this->assertSession()->pageTextNotContains($help_link_title); // Enable Help module and grant permissions to admin user. - // Admin user should be capable to access to all the links + // Admin user should be capable to access to all the links. \Drupal::service('module_installer')->install(['help']); $this->adminUser->addRole($this->drupalCreateRole(['access help pages']))->save(); diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/NavigationBlockUiTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/NavigationBlockUiTest.php index 87d100d40aa..3af1d714f8d 100644 --- a/core/modules/navigation/tests/src/FunctionalJavascript/NavigationBlockUiTest.php +++ b/core/modules/navigation/tests/src/FunctionalJavascript/NavigationBlockUiTest.php @@ -11,12 +11,12 @@ use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; use Drupal\Tests\layout_builder\FunctionalJavascript\LayoutBuilderSortTrait; use Drupal\Tests\system\Traits\OffCanvasTestTrait; use Drupal\user\UserInterface; +use PHPUnit\Framework\Attributes\Group; /** * Tests that the navigation block UI exists and stores data correctly. - * - * @group navigation */ +#[Group('navigation')] class NavigationBlockUiTest extends WebDriverTestBase { use BlockCreationTrait; @@ -133,7 +133,7 @@ class NavigationBlockUiTest extends WebDriverTestBase { $this->getSession()->getPage()->pressButton('Enable edit mode'); $this->assertSession()->assertWaitOnAjaxRequest(); - // Add section should not be present + // Add section should not be present. $this->assertSession()->linkNotExists('Add section'); // Configure section should not be present. $this->assertSession()->linkNotExists('Configure Section 1'); diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/NavigationUserBlockTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/NavigationUserBlockTest.php index dbbce1ea0c7..e619686a806 100644 --- a/core/modules/navigation/tests/src/FunctionalJavascript/NavigationUserBlockTest.php +++ b/core/modules/navigation/tests/src/FunctionalJavascript/NavigationUserBlockTest.php @@ -7,14 +7,13 @@ namespace Drupal\Tests\navigation\FunctionalJavascript; use Behat\Mink\Element\Element; use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; // cspell:ignore navigationuser linksuserwrapper - /** * Tests for \Drupal\navigation\Plugin\NavigationBlock\NavigationUserBlock. - * - * @group navigation */ +#[Group('navigation')] class NavigationUserBlockTest extends WebDriverTestBase { /** diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php index 5bf9d2477f0..528c874649f 100644 --- a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php +++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Drupal\Tests\navigation\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\PerformanceTestBase; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; /** * Tests performance with the navigation toolbar enabled. @@ -13,11 +15,10 @@ use Drupal\FunctionalJavascriptTests\PerformanceTestBase; * * @todo move this coverage to StandardPerformanceTest when Navigation is * enabled by default. - * - * @group Common - * @group #slow - * @requires extension apcu */ +#[Group('Common')] +#[Group('#slow')] +#[RequiresPhpExtension('apcu')] class PerformanceTest extends PerformanceTestBase { /** @@ -93,7 +94,7 @@ class PerformanceTest extends PerformanceTestBase { 'ScriptCount' => 3, 'ScriptBytes' => 167569, 'StylesheetCount' => 2, - 'StylesheetBytes' => 46000, + 'StylesheetBytes' => 45450, ]; $this->assertMetrics($expected, $performance_data); diff --git a/core/modules/node/node.module b/core/modules/node/node.module index f14d843faa1..c99569a1c67 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -36,8 +36,12 @@ use Drupal\node\NodeTypeInterface; * @return array|false * A renderable array containing a list of linked node titles fetched from * $result, or FALSE if there are no rows in $result. + * + * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. There is no replacement. + * @see https://www.drupal.org/node/3531959 */ function node_title_list(StatementInterface $result, $title = NULL) { + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3531959', E_USER_DEPRECATED); $items = []; $num_rows = FALSE; $nids = []; @@ -105,10 +109,11 @@ function node_type_get_names() { * @return string|false * The node type label or FALSE if the node type is not found. * - * @todo Add this as generic helper method for config entities representing - * entity bundles. + * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use $node->getBundleEntity()->label() instead. + * @see https://www.drupal.org/node/3533301 */ function node_get_type_label(NodeInterface $node) { + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use $node->getBundleEntity()->label(). See https://www.drupal.org/node/3533301', E_USER_DEPRECATED); $type = NodeType::load($node->bundle()); return $type ? $type->label() : FALSE; } @@ -121,8 +126,12 @@ function node_get_type_label(NodeInterface $node) { * * @return string * The node type description. + * + * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Use $node_type->getDescription() instead. + * @see https://www.drupal.org/node/3531945 */ function node_type_get_description(NodeTypeInterface $node_type) { + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Use $node_type->getDescription() instead. See https://www.drupal.org/node/3531945', E_USER_DEPRECATED); return $node_type->getDescription(); } @@ -325,10 +334,11 @@ function template_preprocess_node(&$variables): void { // $variables['content'] is more flexible and consistent. $submitted_configurable = $node->getFieldDefinition('created')->isDisplayConfigurable('view') || $node->getFieldDefinition('uid')->isDisplayConfigurable('view'); if (!$skip_custom_preprocessing || !$submitted_configurable) { - $variables['date'] = \Drupal::service('renderer')->render($variables['elements']['created']); - unset($variables['elements']['created']); - $variables['author_name'] = \Drupal::service('renderer')->render($variables['elements']['uid']); - unset($variables['elements']['uid']); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $variables['date'] = !empty($variables['elements']['created']) ? $renderer->render($variables['elements']['created']) : ''; + $variables['author_name'] = !empty($variables['elements']['uid']) ? $renderer->render($variables['elements']['uid']) : ''; + unset($variables['elements']['created'], $variables['elements']['uid']); } if (isset($variables['elements']['title']) && (!$skip_custom_preprocessing || !$node->getFieldDefinition('title')->isDisplayConfigurable('view'))) { @@ -549,7 +559,7 @@ function node_access_rebuild($batch_mode = FALSE): void { } } else { - // Try to allocate enough time to rebuild node grants + // Try to allocate enough time to rebuild node grants. Environment::setTimeLimit(240); // Rebuild newest nodes first so that recent content becomes available diff --git a/core/modules/node/src/Form/NodeForm.php b/core/modules/node/src/Form/NodeForm.php index d739aa7de8f..5498c94c497 100644 --- a/core/modules/node/src/Form/NodeForm.php +++ b/core/modules/node/src/Form/NodeForm.php @@ -10,6 +10,7 @@ use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TempStore\PrivateTempStoreFactory; +use Drupal\Core\Utility\Error; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -124,7 +125,7 @@ class NodeForm extends ContentEntityForm { if ($this->operation == 'edit') { $form['#title'] = $this->t('<em>Edit @type</em> @title', [ - '@type' => node_get_type_label($node), + '@type' => $node->getBundleEntity()->label(), '@title' => $node->label(), ]); } @@ -163,7 +164,7 @@ class NodeForm extends ContentEntityForm { $form['meta']['author'] = [ '#type' => 'item', '#title' => $this->t('Author'), - '#markup' => $node->getOwner()?->getAccountName(), + '#markup' => $node->getOwner()?->getDisplayName(), '#wrapper_attributes' => ['class' => ['entity-meta__author']], ]; @@ -278,21 +279,22 @@ class NodeForm extends ContentEntityForm { public function save(array $form, FormStateInterface $form_state) { $node = $this->entity; $insert = $node->isNew(); - $node->save(); - $node_link = $node->toLink($this->t('View'))->toString(); - $context = ['@type' => $node->getType(), '%title' => $node->label(), 'link' => $node_link]; - $t_args = ['@type' => node_get_type_label($node), '%title' => $node->access('view') ? $node->toLink()->toString() : $node->label()]; - - if ($insert) { - $this->logger('content')->info('@type: added %title.', $context); - $this->messenger()->addStatus($this->t('@type %title has been created.', $t_args)); - } - else { - $this->logger('content')->info('@type: updated %title.', $context); - $this->messenger()->addStatus($this->t('@type %title has been updated.', $t_args)); - } - if ($node->id()) { + try { + $node->save(); + $node_link = $node->toLink($this->t('View'))->toString(); + $context = ['@type' => $node->getType(), '%title' => $node->label(), 'link' => $node_link]; + $t_args = ['@type' => $node->getBundleEntity()->label(), '%title' => $node->access('view') ? $node->toLink()->toString() : $node->label()]; + + if ($insert) { + $this->logger('content')->info('@type: added %title.', $context); + $this->messenger()->addStatus($this->t('@type %title has been created.', $t_args)); + } + else { + $this->logger('content')->info('@type: updated %title.', $context); + $this->messenger()->addStatus($this->t('@type %title has been updated.', $t_args)); + } + $form_state->setValue('nid', $node->id()); $form_state->set('nid', $node->id()); if ($node->access('view')) { @@ -310,10 +312,15 @@ class NodeForm extends ContentEntityForm { $store = $this->tempStoreFactory->get('node_preview'); $store->delete($node->uuid()); } - else { + catch (\Exception $e) { // In the unlikely case something went wrong on save, the node will be - // rebuilt and node form redisplayed the same way as in preview. - $this->messenger()->addError($this->t('The post could not be saved.')); + // rebuilt and node form redisplayed. + $this->messenger()->addError($this->t('The content could not be saved. Contact the site administrator if the problem persists.')); + // It's likely that this exception is an EntityStorageException in which + // case we won't have the actual backtrace available. Attempt to get the + // previous exception if available to include the backtrace. + $e = $e->getPrevious() ?: $e; + \Drupal::logger('node')->error('%type saving node form: @message in %function (line %line of %file) @backtrace_string.', Error::decodeException($e)); $form_state->setRebuild(); } } diff --git a/core/modules/node/src/Form/NodeRevisionRevertForm.php b/core/modules/node/src/Form/NodeRevisionRevertForm.php index ffb58fb71fb..a1e04e2942d 100644 --- a/core/modules/node/src/Form/NodeRevisionRevertForm.php +++ b/core/modules/node/src/Form/NodeRevisionRevertForm.php @@ -136,7 +136,7 @@ class NodeRevisionRevertForm extends ConfirmFormBase { $this->logger('content')->info('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]); $this->messenger() ->addStatus($this->t('@type %title has been reverted to the revision from %revision-date.', [ - '@type' => node_get_type_label($this->revision), + '@type' => $this->revision->getBundleEntity()->label(), '%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp), ])); diff --git a/core/modules/node/src/Hook/NodeHooks1.php b/core/modules/node/src/Hook/NodeHooks1.php index d2dbf545c3c..e103717fb61 100644 --- a/core/modules/node/src/Hook/NodeHooks1.php +++ b/core/modules/node/src/Hook/NodeHooks1.php @@ -219,7 +219,7 @@ class NodeHooks1 { $ranking = [ 'relevance' => [ 'title' => $this->t('Keyword relevance'), - // Average relevance values hover around 0.15 + // Average relevance values hover around 0.15. 'score' => 'i.relevance', ], 'sticky' => [ diff --git a/core/modules/node/src/Hook/NodeTokensHooks.php b/core/modules/node/src/Hook/NodeTokensHooks.php index 3d7f0b0adf4..a7a90347313 100644 --- a/core/modules/node/src/Hook/NodeTokensHooks.php +++ b/core/modules/node/src/Hook/NodeTokensHooks.php @@ -109,7 +109,7 @@ class NodeTokensHooks { break; case 'type-name': - $type_name = node_get_type_label($node); + $type_name = $node->getBundleEntity()->label(); $replacements[$original] = $type_name; break; diff --git a/core/modules/node/src/Hook/NodeViewsHooks.php b/core/modules/node/src/Hook/NodeViewsHooks.php index 477784c153d..8729f547c17 100644 --- a/core/modules/node/src/Hook/NodeViewsHooks.php +++ b/core/modules/node/src/Hook/NodeViewsHooks.php @@ -26,7 +26,7 @@ class NodeViewsHooks { if ($view->storage->get('base_table') == 'node') { foreach ($view->displayHandlers as $display) { if (!$display->isDefaulted('access') || !$display->isDefaulted('filters')) { - // Check for no access control + // Check for no access control. $access = $display->getOption('access'); if (empty($access['type']) || $access['type'] == 'none') { $anonymous_role = Role::load(RoleInterface::ANONYMOUS_ID); diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php index eea6cc10012..fbaab21a211 100644 --- a/core/modules/node/src/NodeGrantDatabaseStorage.php +++ b/core/modules/node/src/NodeGrantDatabaseStorage.php @@ -112,6 +112,13 @@ class NodeGrantDatabaseStorage implements NodeGrantDatabaseStorageInterface { if (count($grants) > 0) { $query->condition($grants); } + if ($query->execute()->fetchField()) { + $access_result = AccessResult::allowed(); + } + else { + $access_result = AccessResult::neutral(); + } + $access_result->addCacheContexts(['user.node_grants:' . $operation]); // Only the 'view' node grant can currently be cached; the others currently // don't have any cacheability metadata. Hopefully, we can add that in the @@ -119,20 +126,10 @@ class NodeGrantDatabaseStorage implements NodeGrantDatabaseStorageInterface { // cases. For now, this must remain marked as uncacheable, even when it is // theoretically cacheable, because we don't have the necessary metadata to // know it for a fact. - $set_cacheability = function (AccessResult $access_result) use ($operation) { - $access_result->addCacheContexts(['user.node_grants:' . $operation]); - if ($operation !== 'view') { - $access_result->setCacheMaxAge(0); - } - return $access_result; - }; - - if ($query->execute()->fetchField()) { - return $set_cacheability(AccessResult::allowed()); - } - else { - return $set_cacheability(AccessResult::neutral()); + if ($operation !== 'view') { + $access_result->setCacheMaxAge(0); } + return $access_result; } /** diff --git a/core/modules/node/src/NodeGrantDatabaseStorageInterface.php b/core/modules/node/src/NodeGrantDatabaseStorageInterface.php index 5e81e1d04d0..d343a2f350b 100644 --- a/core/modules/node/src/NodeGrantDatabaseStorageInterface.php +++ b/core/modules/node/src/NodeGrantDatabaseStorageInterface.php @@ -42,8 +42,8 @@ interface NodeGrantDatabaseStorageInterface { * @param string $base_table * The base table of the query. * - * @return int - * Status of the access check. + * @return void + * No return value. */ public function alterQuery($query, array $tables, $operation, AccountInterface $account, $base_table); diff --git a/core/modules/node/src/NodeListBuilder.php b/core/modules/node/src/NodeListBuilder.php index 0fa80dee411..2b356eb2b34 100644 --- a/core/modules/node/src/NodeListBuilder.php +++ b/core/modules/node/src/NodeListBuilder.php @@ -95,7 +95,7 @@ class NodeListBuilder extends EntityListBuilder { '#title' => $entity->label(), '#url' => $entity->toUrl(), ]; - $row['type'] = node_get_type_label($entity); + $row['type'] = $entity->getBundleEntity()->label(); $row['author']['data'] = [ '#theme' => 'username', '#account' => $entity->getOwner(), diff --git a/core/modules/node/src/NodeTranslationHandler.php b/core/modules/node/src/NodeTranslationHandler.php index 6c802440f37..88a3680a572 100644 --- a/core/modules/node/src/NodeTranslationHandler.php +++ b/core/modules/node/src/NodeTranslationHandler.php @@ -50,8 +50,7 @@ class NodeTranslationHandler extends ContentTranslationHandler { * {@inheritdoc} */ protected function entityFormTitle(EntityInterface $entity) { - $type_name = node_get_type_label($entity); - return $this->t('<em>Edit @type</em> @title', ['@type' => $type_name, '@title' => $entity->label()]); + return $this->t('<em>Edit @type</em> @title', ['@type' => $entity->getBundleEntity()->label(), '@title' => $entity->label()]); } /** diff --git a/core/modules/node/src/Plugin/views/wizard/Node.php b/core/modules/node/src/Plugin/views/wizard/Node.php index d66fa956f9a..bef4ddc8da5 100644 --- a/core/modules/node/src/Plugin/views/wizard/Node.php +++ b/core/modules/node/src/Plugin/views/wizard/Node.php @@ -92,7 +92,7 @@ class Node extends WizardPluginBase { * {@inheritdoc} */ public function getAvailableSorts() { - // You can't execute functions in properties, so override the method + // You can't execute functions in properties, so override the method. return [ 'node_field_data-title:ASC' => $this->t('Title'), ]; diff --git a/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php b/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php index 201e781d196..11753f8ca2f 100644 --- a/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php +++ b/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php @@ -130,7 +130,7 @@ class NodeTestHooks { #[Hook('node_presave')] public function nodePresave(NodeInterface $node): void { if ($node->getTitle() == 'testing_node_presave') { - // Sun, 19 Nov 1978 05:00:00 GMT + // Sun, 19 Nov 1978 05:00:00 GMT. $node->setCreatedTime(280299600); // Drupal 1.0 release. $node->changed = 979534800; diff --git a/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php b/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php index 2e584f3b826..9eba7e9614d 100644 --- a/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php +++ b/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php @@ -128,7 +128,7 @@ class NodeAccessBaseTableTest extends NodeTestBase { $num_simple_users = 2; $simple_users = []; - // Nodes keyed by uid and nid: $nodes[$uid][$nid] = $is_private; + // Nodes keyed by uid and nid: "$nodes[$uid][$nid] = $is_private". $this->nodesByUser = []; // Titles keyed by nid. $titles = []; diff --git a/core/modules/node/tests/src/Functional/NodeAccessGrantsCacheContextTest.php b/core/modules/node/tests/src/Functional/NodeAccessGrantsCacheContextTest.php index 27385e9666b..e67b27d4c3f 100644 --- a/core/modules/node/tests/src/Functional/NodeAccessGrantsCacheContextTest.php +++ b/core/modules/node/tests/src/Functional/NodeAccessGrantsCacheContextTest.php @@ -171,7 +171,7 @@ class NodeAccessGrantsCacheContextTest extends NodeTestBase { 3 => 'view.all', ]); - // Uninstall the node_access_test module + // Uninstall the node_access_test module. $this->container->get('module_installer')->uninstall(['node_access_test']); drupal_static_reset('node_access_view_all_nodes'); $this->assertUserCacheContext([ diff --git a/core/modules/node/tests/src/Functional/NodeCreationTest.php b/core/modules/node/tests/src/Functional/NodeCreationTest.php index 6930bb86f96..7e99e3ba2ec 100644 --- a/core/modules/node/tests/src/Functional/NodeCreationTest.php +++ b/core/modules/node/tests/src/Functional/NodeCreationTest.php @@ -311,6 +311,21 @@ class NodeCreationTest extends NodeTestBase { } /** + * Tests exception handling when saving a node through the form. + */ + public function testNodeCreateExceptionHandling(): void { + $this->drupalGet('node/add/page'); + + $this->submitForm([ + 'title[0][value]' => 'testing_transaction_exception', + 'body[0][value]' => $this->randomMachineName(16), + ], 'Save'); + + $this->assertSession()->pageTextNotContains('The website encountered an unexpected error.'); + $this->assertSession()->pageTextContains('The content could not be saved. Contact the site administrator if the problem persists.'); + } + + /** * Gets the watchdog IDs of the records with the rollback exception message. * * @return int[] diff --git a/core/modules/node/tests/src/Functional/NodeEditFormTest.php b/core/modules/node/tests/src/Functional/NodeEditFormTest.php index dc47998c909..f661ae18ebb 100644 --- a/core/modules/node/tests/src/Functional/NodeEditFormTest.php +++ b/core/modules/node/tests/src/Functional/NodeEditFormTest.php @@ -251,10 +251,15 @@ class NodeEditFormTest extends NodeTestBase { $edit['body[0][value]'] = $this->randomMachineName(16); $this->submitForm($edit, 'Save'); + // Enable user_hooks_test to test the users display name is visible on the + // edit form. + \Drupal::service('module_installer')->install(['user_hooks_test']); + \Drupal::keyValue('user_hooks_test')->set('user_format_name_alter', TRUE); $node = $this->drupalGetNodeByTitle($edit['title[0][value]']); - $this->drupalGet("node/" . $node->id() . "/edit"); + $this->drupalGet($node->toUrl('edit-form')); $this->assertSession()->pageTextContains('Published'); $this->assertSession()->pageTextContains($this->container->get('date.formatter')->format($node->getChangedTime(), 'short')); + $this->assertSession()->responseContains('<em>' . $this->adminUser->id() . '</em>'); } /** diff --git a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php index ac1e8664bad..42c4df88d3a 100644 --- a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php +++ b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php @@ -479,7 +479,7 @@ class NodeTranslationUITest extends ContentTranslationUITestBase { ->getStorage($this->entityTypeId); $entity = $storage->load($this->entityId); $languages = $this->container->get('language_manager')->getLanguages(); - $type_name = node_get_type_label($entity); + $type_name = $entity->getBundleEntity()->label(); foreach ($this->langcodes as $langcode) { // We only want to test the title for non-english translations. diff --git a/core/modules/node/tests/src/Functional/NodeTypeTest.php b/core/modules/node/tests/src/Functional/NodeTypeTest.php index 4169d88f492..d26e4cef58d 100644 --- a/core/modules/node/tests/src/Functional/NodeTypeTest.php +++ b/core/modules/node/tests/src/Functional/NodeTypeTest.php @@ -147,7 +147,7 @@ class NodeTypeTest extends NodeTestBase { $assert->pageTextContains('Foo'); $assert->pageTextContains('Body'); - // Change the name through the API + // Change the name through the API. /** @var \Drupal\node\NodeTypeInterface $node_type */ $node_type = NodeType::load('page'); $node_type->set('name', 'NewBar'); @@ -277,7 +277,7 @@ class NodeTypeTest extends NodeTestBase { $this->drupalGet('admin/structure/types/manage/page/delete'); $this->submitForm([], 'Delete'); - // Navigate to content type administration screen + // Navigate to content type administration screen. $this->drupalGet('admin/structure/types'); $this->assertSession()->pageTextContains("No content types available. Add content type."); $this->assertSession()->linkExists("Add content type"); diff --git a/core/modules/node/tests/src/Functional/Views/NodeFieldTokensTest.php b/core/modules/node/tests/src/Functional/Views/NodeFieldTokensTest.php index e7f060fe3d4..59c33a921e7 100644 --- a/core/modules/node/tests/src/Functional/Views/NodeFieldTokensTest.php +++ b/core/modules/node/tests/src/Functional/Views/NodeFieldTokensTest.php @@ -53,16 +53,16 @@ class NodeFieldTokensTest extends NodeTestBase { $this->drupalGet('test_node_tokens'); - // Body: {{ body }}<br /> + // Body: "{{ body }}<br />". $this->assertSession()->responseContains("Body: <p>$body</p>"); - // Raw value: {{ body__value }}<br /> + // Raw value: "{{ body__value }}<br />". $this->assertSession()->responseContains("Raw value: $body"); - // Raw summary: {{ body__summary }}<br /> + // Raw summary: "{{ body__summary }}<br />". $this->assertSession()->responseContains("Raw summary: $summary"); - // Raw format: {{ body__format }}<br /> + // Raw format: "{{ body__format }}<br />". $this->assertSession()->responseContains("Raw format: plain_text"); } diff --git a/core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php b/core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php index 84f4a376af9..6333930b886 100644 --- a/core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php +++ b/core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\node\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests that outlines of node meta values are displayed in summaries and tabs. - * - * @group node */ +#[Group('node')] class CollapsedSummariesTest extends WebDriverTestBase { /** diff --git a/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php index 4c499c01a86..876d9606776 100644 --- a/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php +++ b/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\node\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\node\Entity\Node; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; +use PHPUnit\Framework\Attributes\Group; /** * Create a node with revisions and test contextual links. - * - * @group node */ +#[Group('node')] class ContextualLinksTest extends WebDriverTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php b/core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php index 5eb912c577c..22a33d0c4b8 100644 --- a/core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php +++ b/core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\node\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\views\Views; +use PHPUnit\Framework\Attributes\Group; /** * Tests JavaScript functionality specific to delete operations. - * - * @group node */ +#[Group('node')] class NodeDeleteConfirmTest extends WebDriverTestBase { /** diff --git a/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php b/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php index 33a8795a649..e1dbf426cec 100644 --- a/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php +++ b/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\node\FunctionalJavascript; use Drupal\filter\Entity\FilterFormat; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript prevention of navigation away from node previews. - * - * @group node */ +#[Group('node')] class NodePreviewLinkTest extends WebDriverTestBase { /** diff --git a/core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php b/core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php index 99ba0722d00..309c14c37b3 100644 --- a/core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php +++ b/core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\node\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript updating of summaries on content type form. - * - * @group node */ +#[Group('node')] class SettingSummariesContentTypeTest extends WebDriverTestBase { /** diff --git a/core/modules/node/tests/src/Kernel/NodeFieldAccessTest.php b/core/modules/node/tests/src/Kernel/NodeFieldAccessTest.php index cfad6986f1e..b69ca50f1be 100644 --- a/core/modules/node/tests/src/Kernel/NodeFieldAccessTest.php +++ b/core/modules/node/tests/src/Kernel/NodeFieldAccessTest.php @@ -82,7 +82,7 @@ class NodeFieldAccessTest extends EntityKernelTestBase { // An unprivileged user. $page_unrelated_user = $this->createUser(['access content']); - // List of all users + // List of all users. $test_users = [ $content_admin_user, $page_creator_user, diff --git a/core/modules/options/tests/src/Functional/OptionsWidgetsTest.php b/core/modules/options/tests/src/Functional/OptionsWidgetsTest.php index 99543ffe90c..56923d7f9c2 100644 --- a/core/modules/options/tests/src/Functional/OptionsWidgetsTest.php +++ b/core/modules/options/tests/src/Functional/OptionsWidgetsTest.php @@ -347,7 +347,7 @@ class OptionsWidgetsTest extends FieldTestBase { $this->card1->setSetting('allowed_values_function', '\Drupal\options_test\OptionsAllowedValues::simpleValues'); $this->card1->save(); - // Display form: with no field data, nothing is selected + // Display form: with no field data, nothing is selected. $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit'); $this->assertFalse($this->assertSession()->optionExists('card_1', 0)->isSelected()); $this->assertFalse($this->assertSession()->optionExists('card_1', 1)->isSelected()); diff --git a/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUIAllowedValuesTest.php b/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUIAllowedValuesTest.php index 28dc50ef60f..174cdd0760f 100644 --- a/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUIAllowedValuesTest.php +++ b/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUIAllowedValuesTest.php @@ -8,13 +8,14 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; /** * Tests the Options field allowed values UI functionality. - * - * @group options - * @group #slow */ +#[Group('options')] +#[Group('#slow')] class OptionsFieldUIAllowedValuesTest extends WebDriverTestBase { use FieldUiJSTestTrait; @@ -84,9 +85,8 @@ class OptionsFieldUIAllowedValuesTest extends WebDriverTestBase { /** * Tests option types allowed values. - * - * @dataProvider providerTestOptionsAllowedValues */ + #[DataProvider('providerTestOptionsAllowedValues')] public function testOptionsAllowedValues($option_type, $options, $is_string_option, string $add_row_method): void { $assert = $this->assertSession(); $this->fieldName = 'field_options_text'; diff --git a/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUITest.php b/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUITest.php index 25dcc1d2ebc..9d7a5c9f547 100644 --- a/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUITest.php +++ b/core/modules/options/tests/src/FunctionalJavascript/OptionsFieldUITest.php @@ -8,13 +8,13 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests the Options field UI functionality. - * - * @group options - * @group #slow */ +#[Group('options')] +#[Group('#slow')] class OptionsFieldUITest extends WebDriverTestBase { use FieldUiJSTestTrait; diff --git a/core/modules/package_manager/config/schema/package_manager.schema.yml b/core/modules/package_manager/config/schema/package_manager.schema.yml index e048fd67a73..8b304d8352d 100644 --- a/core/modules/package_manager/config/schema/package_manager.schema.yml +++ b/core/modules/package_manager/config/schema/package_manager.schema.yml @@ -11,11 +11,21 @@ package_manager.settings: label: 'Package Manager settings' mapping: executables: - type: sequence + type: mapping label: 'Absolute paths to required executables, or NULL to rely on PATH' - sequence: - type: string - label: 'Absolute path to executable, or NULL' + mapping: + composer: + type: string + label: 'Absolute path to Composer executable, or NULL to auto-detect' + nullable: true + constraints: + IsExecutable: [] + rsync: + type: string + label: 'Absolute path to rsync executable, or NULL to auto-detect' + nullable: true + constraints: + IsExecutable: [] additional_trusted_composer_plugins: type: sequence label: 'Additional trusted composer plugins' diff --git a/core/modules/package_manager/package_manager.info.yml b/core/modules/package_manager/package_manager.info.yml index 20cd7bca565..8131f7db3ac 100644 --- a/core/modules/package_manager/package_manager.info.yml +++ b/core/modules/package_manager/package_manager.info.yml @@ -7,3 +7,4 @@ lifecycle: experimental dependencies: - drupal:update hidden: true +configure: package_manager.settings diff --git a/core/modules/package_manager/package_manager.links.menu.yml b/core/modules/package_manager/package_manager.links.menu.yml new file mode 100644 index 00000000000..44fdbfb730b --- /dev/null +++ b/core/modules/package_manager/package_manager.links.menu.yml @@ -0,0 +1,5 @@ +package_manager.settings: + title: 'Package Manager settings' + description: "Configure Package Manager and the locations of its required executables." + parent: system.admin_config_system + route_name: package_manager.settings diff --git a/core/modules/package_manager/package_manager.routing.yml b/core/modules/package_manager/package_manager.routing.yml new file mode 100644 index 00000000000..c61029bfa9c --- /dev/null +++ b/core/modules/package_manager/package_manager.routing.yml @@ -0,0 +1,7 @@ +package_manager.settings: + path: '/admin/config/system/package-manager' + defaults: + _title: 'Package Manager settings' + _form: '\Drupal\package_manager\Form\SettingsForm' + requirements: + _permission: 'administer software updates' diff --git a/core/modules/package_manager/package_manager.services.yml b/core/modules/package_manager/package_manager.services.yml index 059be554270..ead88254823 100644 --- a/core/modules/package_manager/package_manager.services.yml +++ b/core/modules/package_manager/package_manager.services.yml @@ -14,9 +14,6 @@ services: Drupal\package_manager\ExecutableFinder: public: false decorates: 'PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface' - Drupal\package_manager\ProcessFactory: - public: false - decorates: 'PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface' Drupal\package_manager\TranslatableStringFactory: public: false decorates: 'PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface' @@ -175,7 +172,7 @@ services: PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface: class: PhpTuf\ComposerStager\Internal\Process\Factory\ProcessFactory PhpTuf\ComposerStager\API\Process\Service\ComposerProcessRunnerInterface: - class: PhpTuf\ComposerStager\Internal\Process\Service\ComposerProcessRunner + class: Drupal\package_manager\ComposerRunner PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface: class: PhpTuf\ComposerStager\Internal\Process\Service\OutputCallback PhpTuf\ComposerStager\API\Process\Service\ProcessInterface: diff --git a/core/modules/package_manager/src/ComposerRunner.php b/core/modules/package_manager/src/ComposerRunner.php new file mode 100644 index 00000000000..dbc029f6aa3 --- /dev/null +++ b/core/modules/package_manager/src/ComposerRunner.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\package_manager; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\File\FileSystemInterface; +use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; +use PhpTuf\ComposerStager\API\Path\Value\PathInterface; +use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; +use PhpTuf\ComposerStager\API\Process\Service\ComposerProcessRunnerInterface; +use PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface; +use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface; +use Symfony\Component\Process\PhpExecutableFinder; + +// cspell:ignore BINDIR + +/** + * Runs Composer through the current PHP interpreter. + * + * @internal + * This is an internal part of Package Manager and may be changed or removed + * at any time without warning. External code should not interact with this + * class. + */ +final class ComposerRunner implements ComposerProcessRunnerInterface { + + public function __construct( + private readonly ExecutableFinderInterface $executableFinder, + private readonly ProcessFactoryInterface $processFactory, + private readonly FileSystemInterface $fileSystem, + private readonly ConfigFactoryInterface $configFactory, + ) {} + + /** + * {@inheritdoc} + */ + public function run(array $command, ?PathInterface $cwd = NULL, array $env = [], ?OutputCallbackInterface $callback = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT): void { + // Run Composer through the PHP interpreter so we don't have to rely on + // PHP being in the PATH. + array_unshift($command, (new PhpExecutableFinder())->find(), $this->executableFinder->find('composer')); + + $home = $this->fileSystem->getTempDirectory(); + $home .= '/package_manager_composer_home-'; + $home .= $this->configFactory->get('system.site')->get('uuid'); + $this->fileSystem->prepareDirectory($home, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + + $process = $this->processFactory->create($command, $cwd, $env + ['COMPOSER_HOME' => $home]); + $process->setTimeout($timeout); + $process->mustRun($callback); + } + +} diff --git a/core/modules/package_manager/src/ExecutableFinder.php b/core/modules/package_manager/src/ExecutableFinder.php index d8a2f1c21d5..a26de302187 100644 --- a/core/modules/package_manager/src/ExecutableFinder.php +++ b/core/modules/package_manager/src/ExecutableFinder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\package_manager; +use Composer\InstalledVersions; use Drupal\Core\Config\ConfigFactoryInterface; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; @@ -17,10 +18,19 @@ use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; */ final class ExecutableFinder implements ExecutableFinderInterface { + /** + * The path where Composer is installed in the project, or FALSE if it's not. + */ + private string|false|null $composerPath = NULL; + public function __construct( private readonly ExecutableFinderInterface $decorated, private readonly ConfigFactoryInterface $configFactory, - ) {} + ) { + $this->composerPath = InstalledVersions::isInstalled('composer/composer') + ? InstalledVersions::getInstallPath('composer/composer') . '/bin/composer' + : FALSE; + } /** * {@inheritdoc} @@ -29,7 +39,15 @@ final class ExecutableFinder implements ExecutableFinderInterface { $executables = $this->configFactory->get('package_manager.settings') ->get('executables'); - return $executables[$name] ?? $this->decorated->find($name); + if (isset($executables[$name])) { + return $executables[$name]; + } + + // If we're looking for Composer, use the project's local copy if available. + if ($name === 'composer' && $this->composerPath && file_exists($this->composerPath)) { + return $this->composerPath; + } + return $this->decorated->find($name); } } diff --git a/core/modules/package_manager/src/Form/SettingsForm.php b/core/modules/package_manager/src/Form/SettingsForm.php new file mode 100644 index 00000000000..5193801bd2b --- /dev/null +++ b/core/modules/package_manager/src/Form/SettingsForm.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\package_manager\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\ConfigTarget; +use Drupal\Core\Form\FormStateInterface; + +/** + * Configures Package Manager settings. + */ +final class SettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'package_manager_settings_form'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return ['package_manager.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['executables'] = [ + '#type' => 'details', + '#title' => $this->t('Executable paths'), + '#open' => TRUE, + '#description' => $this->t('Configure the paths to required executables.'), + ]; + $trim = fn (string $value): string => trim($value); + + $form['executables']['composer'] = [ + '#type' => 'textfield', + '#title' => $this->t('Composer executable path'), + '#config_target' => new ConfigTarget( + 'package_manager.settings', + 'executables.composer', + toConfig: $trim, + ), + '#description' => $this->t('The full path to the <code>composer</code> executable (e.g., <code>/usr/local/bin/composer</code>).'), + '#required' => TRUE, + ]; + + $form['executables']['rsync'] = [ + '#type' => 'textfield', + '#title' => $this->t('rsync executable path'), + '#config_target' => new ConfigTarget( + 'package_manager.settings', + 'executables.rsync', + toConfig: $trim, + ), + '#description' => $this->t('The full path to the <code>rsync</code> executable (e.g., <code>/usr/bin/rsync</code>).'), + '#required' => TRUE, + ]; + + return parent::buildForm($form, $form_state); + } + +} diff --git a/core/modules/package_manager/src/Hook/PackageManagerHooks.php b/core/modules/package_manager/src/Hook/PackageManagerHooks.php index 2f07923b8cb..7a206af32bc 100644 --- a/core/modules/package_manager/src/Hook/PackageManagerHooks.php +++ b/core/modules/package_manager/src/Hook/PackageManagerHooks.php @@ -3,6 +3,7 @@ namespace Drupal\package_manager\Hook; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Url; use Drupal\package_manager\ComposerInspector; use Drupal\Core\Hook\Attribute\Hook; @@ -20,6 +21,8 @@ class PackageManagerHooks { public function help($route_name) : ?string { switch ($route_name) { case 'help.page.package_manager': + $settings_form = Url::fromRoute('package_manager.settings')->toString(); + $output = '<h3 id="package-manager-about">' . $this->t('About') . '</h3>'; $output .= '<p>' . $this->t('Package Manager is a framework for updating Drupal core and installing contributed modules and themes via Composer. It has no user interface, but it provides an API for creating a temporary copy of the current site, making changes to the copy, and then syncing those changes back into the live site.') . '</p>'; $output .= '<p>' . $this->t('Package Manager dispatches events before and after various operations, and external code can integrate with it by subscribing to those events. For more information, see <code>package_manager.api.php</code>.') . '</p>'; @@ -54,8 +57,7 @@ class PackageManagerHooks { $output .= ' <p>' . $this->t('Ask your system administrator to remove <code>proc_open()</code> from the <a href=":url">disable_functions</a> setting in <code>php.ini</code>.', [':url' => 'https://www.php.net/manual/en/ini.core.php#ini.disable-functions']) . '</p>'; $output .= ' </li>'; $output .= ' <li>' . $this->t('What if it says the <code>composer</code> executable cannot be found?'); - $output .= ' <p>' . $this->t("If the <code>composer</code> executable's path cannot be automatically determined, it can be explicitly set by adding the following line to <code>settings.php</code>:") . '</p>'; - $output .= " <pre><code>\$config['package_manager.settings']['executables']['composer'] = '/full/path/to/composer.phar';</code></pre>"; + $output .= ' <p>' . $this->t("If the <code>composer</code> executable's path cannot be automatically determined, you can set it in the <a href=\":settings_form\">settings form</a>.", [':settings_form' => $settings_form]) . '</p>'; $output .= ' </li>'; $output .= ' <li>' . $this->t('What if it says the detected version of Composer is not supported?'); $output .= ' <p>' . $this->t('The version of the <code>composer</code> executable must satisfy <code>@version</code>. See the <a href=":url">the Composer documentation</a> for more information, or use this command to update Composer:', [ @@ -69,8 +71,7 @@ class PackageManagerHooks { $output .= ' </li>'; $output .= '</ul>'; $output .= '<h4 id="package-manager-faq-rsync">' . $this->t('Using rsync') . '</h4>'; - $output .= '<p>' . $this->t('Package Manager must be able to run <code>rsync</code> to copy files between the live site and the stage directory. Package Manager will try to detect the path to <code>rsync</code>, but if it cannot be detected, you can set it explicitly by adding the following line to <code>settings.php</code>:') . '</p>'; - $output .= "<pre><code>\$config['package_manager.settings']['executables']['rsync'] = '/full/path/to/rsync';</code></pre>"; + $output .= '<p>' . $this->t('Package Manager must be able to run <code>rsync</code> to copy files between the live site and the stage directory. Package Manager will try to detect the path to <code>rsync</code>, but if it cannot be detected, you can set it in the <a href=":settings_form">settings form</a>.', [':settings_form' => $settings_form]) . '</p>'; $output .= '<h4 id="package-manager-tuf-info">' . $this->t('Enabling PHP-TUF protection') . '</h4>'; $output .= '<p>' . $this->t('Package Manager requires <a href=":php-tuf">PHP-TUF</a>, which implements <a href=":tuf">The Update Framework</a> as a way to help secure Composer package downloads via the <a href=":php-tuf-plugin">PHP-TUF Composer integration plugin</a>. This plugin must be installed and configured properly in order to use Package Manager.', [ ':php-tuf' => 'https://github.com/php-tuf/php-tuf', diff --git a/core/modules/package_manager/src/Hook/PackageManagerRequirementsHooks.php b/core/modules/package_manager/src/Hook/PackageManagerRequirementsHooks.php index 52cc10bc4e9..b734df3d13f 100644 --- a/core/modules/package_manager/src/Hook/PackageManagerRequirementsHooks.php +++ b/core/modules/package_manager/src/Hook/PackageManagerRequirementsHooks.php @@ -6,6 +6,7 @@ use Drupal\Core\Extension\Requirement\RequirementSeverity; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Site\Settings; +use Drupal\Core\Url; use Drupal\package_manager\ComposerInspector; use Drupal\package_manager\Exception\FailureMarkerExistsException; use Drupal\package_manager\FailureMarker; @@ -53,8 +54,9 @@ class PackageManagerRequirementsHooks { $requirements['package_manager_composer'] = [ 'title' => $title, - 'description' => $this->t('Composer was not found. The error message was: @message', [ + 'description' => $this->t('Composer was not found. The error message was: @message. The path to Composer can be configured in <a href=":settings-form">the settings form</a>.', [ '@message' => $message, + ':settings-form' => Url::fromRoute('package_manager.settings')->toString(), ]), 'severity' => RequirementSeverity::Error, ]; diff --git a/core/modules/package_manager/src/PathExcluder/SiteConfigurationExcluder.php b/core/modules/package_manager/src/PathExcluder/SiteConfigurationExcluder.php index 9408e874c97..f8f23d0b86a 100644 --- a/core/modules/package_manager/src/PathExcluder/SiteConfigurationExcluder.php +++ b/core/modules/package_manager/src/PathExcluder/SiteConfigurationExcluder.php @@ -43,7 +43,7 @@ class SiteConfigurationExcluder implements EventSubscriberInterface { // Exclude site-specific settings files, which are always in the web root. // By default, Drupal core will always try to write-protect these files. - // @see system_requirements() + // @see \Drupal\system\Hook\SystemRequirementsHooks $settings_files = [ 'settings.php', 'settings.local.php', diff --git a/core/modules/package_manager/src/Plugin/Validation/Constraint/IsExecutableConstraint.php b/core/modules/package_manager/src/Plugin/Validation/Constraint/IsExecutableConstraint.php new file mode 100644 index 00000000000..1bbe29903ec --- /dev/null +++ b/core/modules/package_manager/src/Plugin/Validation/Constraint/IsExecutableConstraint.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\package_manager\Plugin\Validation\Constraint; + +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Validation\Attribute\Constraint; +use Symfony\Component\Validator\Constraint as SymfonyConstraint; + +/** + * Validates that a value is the path of an executable file. + */ +#[Constraint( + id: 'IsExecutable', + label: new TranslatableMarkup('Is executable', [], ['context' => 'Validation']) +)] +final class IsExecutableConstraint extends SymfonyConstraint { + + /** + * The error message shown when the path is not executable. + * + * @var string + */ + public string $message = '"@path" is not an executable file.'; + +} diff --git a/core/modules/package_manager/src/Plugin/Validation/Constraint/IsExecutableConstraintValidator.php b/core/modules/package_manager/src/Plugin/Validation/Constraint/IsExecutableConstraintValidator.php new file mode 100644 index 00000000000..92ca387b1e3 --- /dev/null +++ b/core/modules/package_manager/src/Plugin/Validation/Constraint/IsExecutableConstraintValidator.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\package_manager\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +/** + * Validates the IsExecutable constraint. + */ +final class IsExecutableConstraintValidator extends ConstraintValidator { + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint): void { + assert($constraint instanceof IsExecutableConstraint); + + if ($value === NULL || is_executable($value)) { + return; + } + $this->context->addViolation($constraint->message, ['@path' => $value]); + } + +} diff --git a/core/modules/package_manager/src/ProcessFactory.php b/core/modules/package_manager/src/ProcessFactory.php deleted file mode 100644 index 7e92977e7ff..00000000000 --- a/core/modules/package_manager/src/ProcessFactory.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\package_manager; - -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\File\FileSystemInterface; -use PhpTuf\ComposerStager\API\Path\Value\PathInterface; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; -use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface; - -// cspell:ignore BINDIR - -/** - * Defines a process factory which sets the COMPOSER_HOME environment variable. - * - * @internal - * This is an internal part of Package Manager and may be changed or removed - * at any time without warning. External code should not interact with this - * class. - */ -final class ProcessFactory implements ProcessFactoryInterface { - - public function __construct( - private readonly FileSystemInterface $fileSystem, - private readonly ConfigFactoryInterface $configFactory, - private readonly ProcessFactoryInterface $decorated, - ) {} - - /** - * {@inheritdoc} - */ - public function create(array $command, ?PathInterface $cwd = NULL, array $env = []): ProcessInterface { - $process = $this->decorated->create($command, $cwd, $env); - - $env = $process->getEnv(); - if ($command && $this->isComposerCommand($command)) { - $env['COMPOSER_HOME'] = $this->getComposerHomePath(); - } - // Ensure that the current PHP installation is the first place that will be - // searched when looking for the PHP interpreter. - $env['PATH'] = static::getPhpDirectory() . ':' . getenv('PATH'); - $process->setEnv($env); - return $process; - } - - /** - * Returns the directory which contains the PHP interpreter. - * - * @return string - * The path of the directory containing the PHP interpreter. If the server - * is running in a command-line interface, the directory portion of - * PHP_BINARY is returned; otherwise, the compile-time PHP_BINDIR is. - * - * @see php_sapi_name() - * @see https://www.php.net/manual/en/reserved.constants.php - */ - private static function getPhpDirectory(): string { - if (PHP_SAPI === 'cli' || PHP_SAPI === 'cli-server') { - return dirname(PHP_BINARY); - } - return PHP_BINDIR; - } - - /** - * Returns the path to use as the COMPOSER_HOME environment variable. - * - * @return string - * The path which should be used as COMPOSER_HOME. - */ - private function getComposerHomePath(): string { - $home_path = $this->fileSystem->getTempDirectory(); - $home_path .= '/package_manager_composer_home-'; - $home_path .= $this->configFactory->get('system.site')->get('uuid'); - $this->fileSystem->prepareDirectory($home_path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); - - return $home_path; - } - - /** - * Determines if a command is running Composer. - * - * @param string[] $command - * The command parts. - * - * @return bool - * TRUE if the command is running Composer, FALSE otherwise. - */ - private function isComposerCommand(array $command): bool { - $executable = $command[0]; - $executable_parts = explode('/', $executable); - $file = array_pop($executable_parts); - return str_starts_with($file, 'composer'); - } - -} diff --git a/core/modules/package_manager/tests/modules/package_manager_test_validation/package_manager_test_validation.services.yml b/core/modules/package_manager/tests/modules/package_manager_test_validation/package_manager_test_validation.services.yml index bd41327d315..785fde50831 100644 --- a/core/modules/package_manager/tests/modules/package_manager_test_validation/package_manager_test_validation.services.yml +++ b/core/modules/package_manager/tests/modules/package_manager_test_validation/package_manager_test_validation.services.yml @@ -10,3 +10,8 @@ services: autowire: true tags: - { name: event_subscriber } + Drupal\package_manager_test_validation\TestExecutableFinder: + public: false + arguments: + - '@.inner' + decorates: 'PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface' diff --git a/core/modules/package_manager/tests/modules/package_manager_test_validation/src/TestExecutableFinder.php b/core/modules/package_manager/tests/modules/package_manager_test_validation/src/TestExecutableFinder.php new file mode 100644 index 00000000000..05df109d2e4 --- /dev/null +++ b/core/modules/package_manager/tests/modules/package_manager_test_validation/src/TestExecutableFinder.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\package_manager_test_validation; + +use PhpTuf\ComposerStager\API\Exception\LogicException; +use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; +use PhpTuf\ComposerStager\API\Translation\Service\TranslatorInterface; +use PhpTuf\ComposerStager\API\Translation\Value\TranslatableInterface; + +/** + * A test-only executable finder that can be rigged to throw an exception. + */ +final class TestExecutableFinder implements ExecutableFinderInterface { + + public function __construct( + private readonly ExecutableFinderInterface $decorated, + ) {} + + /** + * Throws an exception when finding a specific executable, for testing. + * + * @param string $name + * The name of the executable for which ::find() will throw an exception. + */ + public static function throwFor(string $name): void { + \Drupal::keyValue('package_manager_test.executable_errors') + ->set($name, TRUE); + } + + /** + * {@inheritdoc} + */ + public function find(string $name): string { + $should_throw = \Drupal::keyValue('package_manager_test.executable_errors') + ->get($name); + + if ($should_throw) { + throw new LogicException(new class () implements TranslatableInterface { + + /** + * {@inheritdoc} + */ + public function trans(?TranslatorInterface $translator = NULL, ?string $locale = NULL): string { + return 'Not found!'; + } + + /** + * {@inheritdoc} + */ + public function __toString(): string { + return $this->trans(); + } + + }); + } + return $this->decorated->find($name); + } + +} diff --git a/core/modules/package_manager/tests/src/Build/PackageInstallSubmoduleTest.php b/core/modules/package_manager/tests/src/Build/PackageInstallSubmoduleTest.php index 18c87b11956..4cc4405d4c8 100644 --- a/core/modules/package_manager/tests/src/Build/PackageInstallSubmoduleTest.php +++ b/core/modules/package_manager/tests/src/Build/PackageInstallSubmoduleTest.php @@ -4,13 +4,15 @@ declare(strict_types=1); namespace Drupal\Tests\package_manager\Build; +use PHPUnit\Framework\Attributes\Group; + /** * Tests installing packages in a stage directory. * - * @group package_manager - * @group #slow * @internal */ +#[Group('package_manager')] +#[Group('#slow')] class PackageInstallSubmoduleTest extends TemplateProjectTestBase { /** diff --git a/core/modules/package_manager/tests/src/Build/PackageInstallTest.php b/core/modules/package_manager/tests/src/Build/PackageInstallTest.php index 362343eaa91..283c6aefa2c 100644 --- a/core/modules/package_manager/tests/src/Build/PackageInstallTest.php +++ b/core/modules/package_manager/tests/src/Build/PackageInstallTest.php @@ -4,13 +4,15 @@ declare(strict_types=1); namespace Drupal\Tests\package_manager\Build; +use PHPUnit\Framework\Attributes\Group; + /** * Tests installing packages in a stage directory. * - * @group package_manager - * @group #slow * @internal */ +#[Group('package_manager')] +#[Group('#slow')] class PackageInstallTest extends TemplateProjectTestBase { /** diff --git a/core/modules/package_manager/tests/src/Build/PackageUpdateTest.php b/core/modules/package_manager/tests/src/Build/PackageUpdateTest.php index 2b9ef4aa894..fbfc3a0b706 100644 --- a/core/modules/package_manager/tests/src/Build/PackageUpdateTest.php +++ b/core/modules/package_manager/tests/src/Build/PackageUpdateTest.php @@ -5,14 +5,15 @@ declare(strict_types=1); namespace Drupal\Tests\package_manager\Build; use Drupal\package_manager_test_api\ControllerSandboxManager; +use PHPUnit\Framework\Attributes\Group; /** * Tests updating packages in a stage directory. * - * @group package_manager - * @group #slow * @internal */ +#[Group('package_manager')] +#[Group('#slow')] class PackageUpdateTest extends TemplateProjectTestBase { /** @@ -39,7 +40,7 @@ class PackageUpdateTest extends TemplateProjectTestBase { // Change both modules' upstream version. static::copyFixtureFilesTo("$fixtures/alpha/1.1.0", $alpha_repo_path); static::copyFixtureFilesTo("$fixtures/updated_module/1.1.0", $updated_module_repo_path); - // Make .git folder + // Make .git folder. // Use the API endpoint to create a stage and update updated_module to // 1.1.0. Even though both modules have version 1.1.0 available, only diff --git a/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php b/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php index 45046a8e2ad..368b2ce7337 100644 --- a/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php +++ b/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php @@ -21,6 +21,16 @@ class ComposerRequirementTest extends PackageManagerTestBase { protected static $modules = ['package_manager']; /** + * {@inheritdoc} + */ + protected static $configSchemaCheckerExclusions = [ + // We test what happens when the configured path to Composer is invalid, + // so we need to be able to skip schema-based validation, which would + // normally confirm that the configured path to Composer is executable. + 'package_manager.settings', + ]; + + /** * Tests that Composer version and path are listed on the status report. */ public function testComposerInfoShown(): void { @@ -47,7 +57,10 @@ class ComposerRequirementTest extends PackageManagerTestBase { $config->set('executables.composer', '/path/to/composer')->save(); $this->getSession()->reload(); $assert_session->statusCodeEquals(200); - $assert_session->pageTextContains('Composer was not found. The error message was: Failed to run process: The command "\'/path/to/composer\' \'--format=json\'" failed.'); + $assert_session->pageTextContains('Composer was not found. The error message was: '); + // Check for the part of the command string that is constant (the path to + // the PHP interpreter will vary). + $assert_session->pageTextContains("/php' '/path/to/composer' '--format=json'\" failed."); } } diff --git a/core/modules/package_manager/tests/src/Functional/SettingsFormTest.php b/core/modules/package_manager/tests/src/Functional/SettingsFormTest.php new file mode 100644 index 00000000000..58b9b2d39db --- /dev/null +++ b/core/modules/package_manager/tests/src/Functional/SettingsFormTest.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\package_manager\Functional; + +use Drupal\Tests\BrowserTestBase; +use PhpTuf\ComposerStager\API\Exception\LogicException; +use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; + +/** + * Tests the Package Manager settings form. + * + * @group package_manager + */ +final class SettingsFormTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['package_manager']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Tests that executable paths can be configured through the settings form. + */ + public function testSettingsForm(): void { + $assert_session = $this->assertSession(); + $account = $this->drupalCreateUser(['administer software updates']); + $this->drupalLogin($account); + + /** @var \PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface $executable_finder */ + $executable_finder = \Drupal::service(ExecutableFinderInterface::class); + try { + $composer_path = $executable_finder->find('composer'); + $rsync_path = $executable_finder->find('rsync'); + } + catch (LogicException) { + $this->markTestSkipped('This test requires Composer and rsync to be available in the PATH.'); + } + + $this->drupalGet('/admin/config/system/package-manager'); + $assert_session->statusCodeEquals(200); + // Submit the settings form with the detected paths, with whitespace added + // to test that it is trimmed out. + $this->submitForm([ + 'composer' => "$composer_path ", + 'rsync' => " $rsync_path", + ], 'Save configuration'); + $assert_session->pageTextContains('The configuration options have been saved.'); + + // Verify the paths were saved in config. + $config = $this->config('package_manager.settings'); + $this->assertSame($composer_path, $config->get('executables.composer')); + $this->assertSame($rsync_path, $config->get('executables.rsync')); + + // Verify the paths are shown in the form. + $this->drupalGet('/admin/config/system/package-manager'); + $assert_session->fieldValueEquals('composer', $composer_path); + $assert_session->fieldValueEquals('rsync', $rsync_path); + + // Ensure that the executable paths are confirmed to be executable. + $this->submitForm([ + 'composer' => 'rm -rf /', + 'rsync' => 'cat /etc/passwd', + ], 'Save configuration'); + $assert_session->statusMessageContains('"rm -rf /" is not an executable file.', 'error'); + $assert_session->statusMessageContains('"cat /etc/passwd" is not an executable file.', 'error'); + } + +} diff --git a/core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php b/core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php index 61f922824bd..e1fb87024cf 100644 --- a/core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php +++ b/core/modules/package_manager/tests/src/Kernel/ComposerInspectorTest.php @@ -81,7 +81,7 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase { ->getProjectRoot(); $inspector = $this->container->get(ComposerInspector::class); - // Overwrite the composer.json file and treat it as a + // Overwrite the composer.json file and treat it as a. $file = new JsonFile($project_root . '/composer.json'); $this->assertTrue($file->exists()); $data = $file->read(); diff --git a/core/modules/package_manager/tests/src/Kernel/ComposerPluginsValidatorTestBase.php b/core/modules/package_manager/tests/src/Kernel/ComposerPluginsValidatorTestBase.php index d9b91b1a760..3100ce4bbf7 100644 --- a/core/modules/package_manager/tests/src/Kernel/ComposerPluginsValidatorTestBase.php +++ b/core/modules/package_manager/tests/src/Kernel/ComposerPluginsValidatorTestBase.php @@ -12,10 +12,9 @@ use Drupal\package_manager\Event\PreCreateEvent; use Drupal\package_manager\ValidationResult; /** - * @group package_manager * @internal */ -class ComposerPluginsValidatorTestBase extends PackageManagerKernelTestBase { +abstract class ComposerPluginsValidatorTestBase extends PackageManagerKernelTestBase { /** * Tests composer plugins are validated during pre-create. diff --git a/core/modules/package_manager/tests/src/Kernel/DirectWriteTest.php b/core/modules/package_manager/tests/src/Kernel/DirectWriteTest.php index 8c34ae00273..90498f55795 100644 --- a/core/modules/package_manager/tests/src/Kernel/DirectWriteTest.php +++ b/core/modules/package_manager/tests/src/Kernel/DirectWriteTest.php @@ -16,6 +16,7 @@ use Drupal\package_manager\Exception\SandboxEventException; use Drupal\package_manager\PathLocator; use Drupal\package_manager\StatusCheckTrait; use Drupal\package_manager\ValidationResult; +use Drupal\package_manager_test_validation\TestExecutableFinder; use PhpTuf\ComposerStager\API\Core\BeginnerInterface; use PhpTuf\ComposerStager\API\Core\CommitterInterface; use PhpTuf\ComposerStager\API\Path\Factory\PathFactoryInterface; @@ -35,6 +36,11 @@ class DirectWriteTest extends PackageManagerKernelTestBase implements EventSubsc use StringTranslationTrait; /** + * {@inheritdoc} + */ + protected static $modules = ['package_manager_test_validation']; + + /** * Whether we are in maintenance mode before a require operation. * * @var bool|null @@ -244,12 +250,10 @@ class DirectWriteTest extends PackageManagerKernelTestBase implements EventSubsc */ public function testPreconditionBypass(string $service_class): void { // Set up conditions where the active and sandbox directories are the same, - // and the path to rsync isn't valid. + // and detecting rsync will fail. $path = $this->container->get(PathFactoryInterface::class) ->create('/the/absolute/apex'); - $this->config('package_manager.settings') - ->set('executables.rsync', "C:\Not Rsync.exe") - ->save(); + TestExecutableFinder::throwFor('rsync'); /** @var \PhpTuf\ComposerStager\API\Precondition\Service\PreconditionInterface $precondition */ $precondition = $this->container->get($service_class); diff --git a/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php b/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php deleted file mode 100644 index db27b06efa8..00000000000 --- a/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\package_manager\Kernel; - -use Drupal\package_manager\ProcessFactory; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; - -/** - * @coversDefaultClass \Drupal\package_manager\ProcessFactory - * @group auto_updates - * @internal - */ -class ProcessFactoryTest extends PackageManagerKernelTestBase { - - /** - * Tests that the process factory prepends the PHP directory to PATH. - */ - public function testPhpDirectoryPrependedToPath(): void { - $factory = $this->container->get(ProcessFactoryInterface::class); - $this->assertInstanceOf(ProcessFactory::class, $factory); - - // Ensure that the directory of the PHP interpreter can be found. - $reflector = new \ReflectionObject($factory); - $method = $reflector->getMethod('getPhpDirectory'); - $php_dir = $method->invoke(NULL); - $this->assertNotEmpty($php_dir); - - // The process factory should always put the PHP interpreter's directory - // at the beginning of the PATH environment variable. - $env = $factory->create(['whoami'])->getEnv(); - $this->assertStringStartsWith("$php_dir:", $env['PATH']); - } - -} diff --git a/core/modules/package_manager/tests/src/Kernel/ServicesTest.php b/core/modules/package_manager/tests/src/Kernel/ServicesTest.php index a1db5a517c1..9e52f9b4075 100644 --- a/core/modules/package_manager/tests/src/Kernel/ServicesTest.php +++ b/core/modules/package_manager/tests/src/Kernel/ServicesTest.php @@ -9,14 +9,12 @@ use Drupal\package_manager\ExecutableFinder; use Drupal\package_manager\LoggingBeginner; use Drupal\package_manager\LoggingCommitter; use Drupal\package_manager\LoggingStager; -use Drupal\package_manager\ProcessFactory; use Drupal\package_manager\TranslatableStringFactory; use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait; use PhpTuf\ComposerStager\API\Core\BeginnerInterface; use PhpTuf\ComposerStager\API\Core\CommitterInterface; use PhpTuf\ComposerStager\API\Core\StagerInterface; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface; /** @@ -38,11 +36,9 @@ class ServicesTest extends KernelTestBase { * Tests that Package Manager's public services can be instantiated. */ public function testPackageManagerServices(): void { - // Ensure that any overridden Composer Stager services were overridden - // correctly. + // Ensure that certain Composer Stager services are decorated correctly. $overrides = [ ExecutableFinderInterface::class => ExecutableFinder::class, - ProcessFactoryInterface::class => ProcessFactory::class, TranslatableFactoryInterface::class => TranslatableStringFactory::class, BeginnerInterface::class => LoggingBeginner::class, StagerInterface::class => LoggingStager::class, diff --git a/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php b/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php new file mode 100644 index 00000000000..91253d377d2 --- /dev/null +++ b/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\package_manager\Unit; + +use Drupal\Core\File\FileSystemInterface; +use Drupal\package_manager\ComposerRunner; +use Drupal\Tests\UnitTestCase; +use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; +use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use Prophecy\Argument; + +// cspell:ignore BINDIR + +/** + * Tests Package Manager's Composer runner service. + * + * @internal + */ +#[CoversClass(ComposerRunner::class)] +#[Group('package_manager')] +class ComposerRunnerTest extends UnitTestCase { + + /** + * Tests that the Composer runner runs Composer through the PHP interpreter. + */ + public function testRunner(): void { + $executable_finder = $this->prophesize(ExecutableFinderInterface::class); + $executable_finder->find('composer') + ->willReturn('/mock/composer') + ->shouldBeCalled(); + + $process_factory = $this->prophesize(ProcessFactoryInterface::class); + $process_factory->create( + // Internally, ComposerRunner uses Symfony's PhpExecutableFinder to locate + // the PHP interpreter, which should resolve to PHP_BINARY a command-line + // test environment. + [PHP_BINARY, '/mock/composer', '--version'], + NULL, + Argument::withKey('COMPOSER_HOME'), + )->shouldBeCalled(); + + $file_system = $this->prophesize(FileSystemInterface::class); + $file_system->getTempDirectory()->shouldBeCalled(); + $file_system->prepareDirectory(Argument::cetera())->shouldBeCalled(); + + $runner = new ComposerRunner( + $executable_finder->reveal(), + $process_factory->reveal(), + $file_system->reveal(), + $this->getConfigFactoryStub([ + 'system.site' => [ + 'uuid' => 'testing', + ], + ]), + ); + $runner->run(['--version']); + } + +} diff --git a/core/modules/package_manager/tests/src/Unit/ExecutableFinderTest.php b/core/modules/package_manager/tests/src/Unit/ExecutableFinderTest.php index 811d7547e04..69becb1fd8d 100644 --- a/core/modules/package_manager/tests/src/Unit/ExecutableFinderTest.php +++ b/core/modules/package_manager/tests/src/Unit/ExecutableFinderTest.php @@ -6,6 +6,7 @@ namespace Drupal\Tests\package_manager\Unit; use Drupal\package_manager\ExecutableFinder; use Drupal\Tests\UnitTestCase; +use org\bovigo\vfs\vfsStream; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; /** @@ -36,4 +37,41 @@ class ExecutableFinderTest extends UnitTestCase { $finder->find('rsync'); } + /** + * Tests that the executable finder tries to use a local copy of Composer. + */ + public function testComposerInstalledInProject(): void { + vfsStream::setup('root', NULL, [ + 'composer-path' => [ + 'bin' => [], + ], + ]); + $composer_path = 'vfs://root/composer-path/bin/composer'; + touch($composer_path); + $this->assertFileExists($composer_path); + + $decorated = $this->prophesize(ExecutableFinderInterface::class); + $decorated->find('composer')->willReturn('the real Composer'); + + $finder = new ExecutableFinder( + $decorated->reveal(), + $this->getConfigFactoryStub([ + 'package_manager.settings' => [ + 'executables' => [], + ], + ]), + ); + $reflector = new \ReflectionProperty($finder, 'composerPath'); + $reflector->setValue($finder, $composer_path); + $this->assertSame($composer_path, $finder->find('composer')); + + // If the executable disappears, or Composer isn't locally installed, the + // decorated executable finder should be called. + unlink($composer_path); + $this->assertSame('the real Composer', $finder->find('composer')); + + $reflector->setValue($finder, FALSE); + $this->assertSame('the real Composer', $finder->find('composer')); + } + } diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index ed7adef588f..b393e8ac30c 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -82,12 +82,28 @@ class PathItem extends FieldItemBase { // If we have an alias, we need to create or update a path alias entity. if ($alias) { - if (!$update || !$pid) { - $path_alias = $path_alias_storage->create([ - 'path' => '/' . $entity->toUrl()->getInternalPath(), - 'alias' => $alias, - 'langcode' => $alias_langcode, - ]); + $properties = [ + 'path' => '/' . $entity->toUrl()->getInternalPath(), + 'alias' => $alias, + 'langcode' => $alias_langcode, + ]; + + if (!$pid) { + // Try to load it from storage before creating it. In some cases the + // path alias could be created before this function runs. For example, + // \Drupal\workspaces\EntityOperations::entityTranslationInsert will + // create a translation, and an associated path alias will be created + // with it. + $query = $path_alias_storage->getQuery()->accessCheck(FALSE); + foreach ($properties as $field => $value) { + $query->condition($field, $value); + } + $ids = $query->execute(); + $pid = $ids ? reset($ids) : $pid; + } + + if (!$pid) { + $path_alias = $path_alias_storage->create($properties); $path_alias->save(); $this->set('pid', $path_alias->id()); } diff --git a/core/modules/path/tests/modules/path_test_misc/path_test_misc.info.yml b/core/modules/path/tests/modules/path_test_misc/path_test_misc.info.yml new file mode 100644 index 00000000000..701a1028914 --- /dev/null +++ b/core/modules/path/tests/modules/path_test_misc/path_test_misc.info.yml @@ -0,0 +1,5 @@ +name: 'Path test miscellaneous utilities' +type: module +description: 'Utilities for path testing' +package: Testing +version: VERSION diff --git a/core/modules/path/tests/modules/path_test_misc/src/Hook/PathTestMiscHooks.php b/core/modules/path/tests/modules/path_test_misc/src/Hook/PathTestMiscHooks.php new file mode 100644 index 00000000000..5fc6662a09d --- /dev/null +++ b/core/modules/path/tests/modules/path_test_misc/src/Hook/PathTestMiscHooks.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\path_test_misc\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\node\NodeInterface; + +/** + * Hook implementations for path_test_misc. + */ +class PathTestMiscHooks { + + /** + * Implements hook_ENTITY_TYPE_presave() for node entities. + * + * This is invoked from testAliasDuplicationPrevention. + */ + #[Hook('node_presave')] + public function nodePresave(NodeInterface $node): void { + if ($node->getTitle() !== 'path duplication test') { + return; + } + + // Update the title to be able to check that this code ran. + $node->setTitle('path duplication test ran'); + + // Create a path alias that has the same values as the one in + // PathItem::postSave. + $path = \Drupal::entityTypeManager()->getStorage('path_alias') + ->create([ + 'path' => '/node/1', + 'alias' => '/my-alias', + 'langcode' => 'en', + ]); + $path->save(); + } + +} diff --git a/core/modules/path/tests/src/Functional/PathAliasTest.php b/core/modules/path/tests/src/Functional/PathAliasTest.php index 1c196dcdd54..6a922c295da 100644 --- a/core/modules/path/tests/src/Functional/PathAliasTest.php +++ b/core/modules/path/tests/src/Functional/PathAliasTest.php @@ -153,7 +153,7 @@ class PathAliasTest extends PathTestBase { // Set alias to second test node. $edit['path[0][value]'] = '/node/' . $node2->id(); - // Leave $edit['alias'] the same + // Leave "$edit['alias']" the same. $this->drupalGet('admin/config/search/path/add'); $this->submitForm($edit, 'Save'); diff --git a/core/modules/path/tests/src/Functional/PathNodeFormTest.php b/core/modules/path/tests/src/Functional/PathNodeFormTest.php index 001ba2f09ba..9763a145dc9 100644 --- a/core/modules/path/tests/src/Functional/PathNodeFormTest.php +++ b/core/modules/path/tests/src/Functional/PathNodeFormTest.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace Drupal\Tests\path\Functional; +use Drupal\node\Entity\Node; +use Drupal\node\NodeInterface; + /** * Tests the Path Node form UI. * @@ -14,7 +17,7 @@ class PathNodeFormTest extends PathTestBase { /** * {@inheritdoc} */ - protected static $modules = ['node', 'path']; + protected static $modules = ['node', 'path', 'path_test_misc']; /** * {@inheritdoc} @@ -59,4 +62,27 @@ class PathNodeFormTest extends PathTestBase { $assert_session->fieldNotExists('path[0][alias]'); } + /** + * Tests that duplicate path aliases don't get created. + */ + public function testAliasDuplicationPrevention(): void { + $this->drupalGet('node/add/page'); + $edit['title[0][value]'] = 'path duplication test'; + $edit['path[0][alias]'] = '/my-alias'; + $this->submitForm($edit, 'Save'); + + // Test that PathItem::postSave detects if a path alias exists + // before creating one. + $aliases = \Drupal::entityTypeManager() + ->getStorage('path_alias') + ->loadMultiple(); + static::assertCount(1, $aliases); + $node = Node::load(1); + static::assertInstanceOf(NodeInterface::class, $node); + + // This updated title gets set in PathTestMiscHooks::nodePresave. This + // is a way of ensuring that bit of test code runs. + static::assertEquals('path duplication test ran', $node->getTitle()); + } + } diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php index 40b2de75cf0..f7601226614 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php @@ -151,8 +151,8 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn // so backslashes in the password need to be doubled up. // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords // will break on this doubling up when the bug is fixed, so check the - // version - // elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') { + // version. + // "elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {". else { $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']); } diff --git a/core/modules/pgsql/src/Hook/PgsqlRequirementsHooks.php b/core/modules/pgsql/src/Hook/PgsqlRequirementsHooks.php index 65fa78a5e71..0e7bbd97628 100644 --- a/core/modules/pgsql/src/Hook/PgsqlRequirementsHooks.php +++ b/core/modules/pgsql/src/Hook/PgsqlRequirementsHooks.php @@ -2,18 +2,14 @@ namespace Drupal\pgsql\Hook; -use Drupal\Core\Database\Database; -use Drupal\Core\Extension\Requirement\RequirementSeverity; -use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\pgsql\Install\Requirements\PgsqlRequirements; /** * Hook implementations for pgsql module. */ class PgsqlRequirementsHooks { - use StringTranslationTrait; - /** * Implements hook_update_requirements(). * @@ -22,34 +18,8 @@ class PgsqlRequirementsHooks { #[Hook('update_requirements')] #[Hook('runtime_requirements')] public function checkRequirements(): array { - $requirements = []; - // Test with PostgreSQL databases for the status of the pg_trgm extension. - if (Database::isActiveConnection()) { - $connection = Database::getConnection(); - - // Set the requirement just for postgres. - if ($connection->driver() == 'pgsql') { - $requirements['pgsql_extension_pg_trgm'] = [ - 'severity' => RequirementSeverity::OK, - 'title' => $this->t('PostgreSQL pg_trgm extension'), - 'value' => $this->t('Available'), - 'description' => $this->t('The pg_trgm PostgreSQL extension is present.'), - ]; - - // If the extension is not available, set the requirement error. - if (!$connection->schema()->extensionExists('pg_trgm')) { - $requirements['pgsql_extension_pg_trgm']['severity'] = RequirementSeverity::Error; - $requirements['pgsql_extension_pg_trgm']['value'] = $this->t('Not created'); - $requirements['pgsql_extension_pg_trgm']['description'] = $this->t('The <a href=":pg_trgm">pg_trgm</a> PostgreSQL extension is not present. The extension is required by Drupal to improve performance when using PostgreSQL. See <a href=":requirements">Drupal database server requirements</a> for more information.', [ - ':pg_trgm' => 'https://www.postgresql.org/docs/current/pgtrgm.html', - ':requirements' => 'https://www.drupal.org/docs/system-requirements/database-server-requirements', - ]); - } - - } - } - - return $requirements; + // We want the identical check from the install time requirements. + return PgsqlRequirements::getRequirements(); } } diff --git a/core/modules/pgsql/tests/src/Kernel/EntityQueryServiceDeprecation.php b/core/modules/pgsql/tests/src/Kernel/EntityQueryServiceDeprecationTest.php index db2c1b88ea3..043b5838d5a 100644 --- a/core/modules/pgsql/tests/src/Kernel/EntityQueryServiceDeprecation.php +++ b/core/modules/pgsql/tests/src/Kernel/EntityQueryServiceDeprecationTest.php @@ -8,17 +8,20 @@ use Drupal\Core\Entity\Query\Sql\QueryFactory as BaseQueryFactory; use Drupal\Core\Entity\Query\Sql\pgsql\QueryFactory as DeprecatedQueryFactory; use Drupal\KernelTests\KernelTestBase; use Drupal\pgsql\EntityQuery\QueryFactory; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; /** * Tests the move of the 'pgsql.entity.query.sql' service. */ -class EntityQueryServiceDeprecation extends KernelTestBase { +#[Group('Database')] +#[Group('pgsql')] +class EntityQueryServiceDeprecationTest extends KernelTestBase { /** * Tests that the core provided service is deprecated. - * - * @group legacy */ + #[IgnoreDeprecations] public function testPostgresServiceDeprecated(): void { $running_driver = $this->container->get('database')->driver(); if ($running_driver === 'pgsql') { diff --git a/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php b/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php index e10ed33d190..1073e697163 100644 --- a/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php +++ b/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php @@ -200,7 +200,7 @@ class SchemaTest extends DriverSpecificSchemaTestBase { $this->assertFalse($this->schema->fieldExists($table_name_new, $field_name)); $this->assertTrue($this->schema->fieldExists($table_name_new, $field_name_new)); - // Adding an unique key + // Adding an unique key. $unique_key_name = $unique_key_introspect_name = 'unique'; $this->schema->addUniqueKey($table_name_new, $unique_key_name, [$field_name_new]); @@ -210,7 +210,7 @@ class SchemaTest extends DriverSpecificSchemaTestBase { $unique_key_introspect_name = $ensure_identifiers_length->invoke($this->schema, $table_name_new, $unique_key_name, 'key'); $this->assertEquals([$field_name_new], $introspect_index_schema->invoke($this->schema, $table_name_new)['unique keys'][$unique_key_introspect_name]); - // Dropping an unique key + // Dropping an unique key. $this->schema->dropUniqueKey($table_name_new, $unique_key_name); // Dropping a field. diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 9fe3dc72b50..790164bf56a 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -78,9 +78,8 @@ function template_preprocess_responsive_image_formatter(&$variables): void { * - responsive_image_style_id: The ID of the responsive image style. */ function template_preprocess_responsive_image(&$variables): void { - // Make sure that width and height are proper values - // If they exists we'll output them - // @see https://www.w3.org/community/respimg/2012/06/18/florians-compromise/ + // Make sure that width and height are proper values, if they exist we'll + // output them. if (isset($variables['width']) && empty($variables['width'])) { unset($variables['width']); unset($variables['height']); diff --git a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php index 5c5e6be5838..7dbd91fb730 100644 --- a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php @@ -537,7 +537,7 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { // Ensure that preview works. $this->previewNodeImage($test_image, $field_name, 'article'); - // Look for a picture tag in the preview output + // Look for a picture tag in the preview output. $this->assertSession()->responseMatches('/picture/'); $nid = $this->uploadNodeImage($test_image, $field_name, 'article'); diff --git a/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php b/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php index 0e85555f524..2cc616e8e62 100644 --- a/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php +++ b/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\responsive_image\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\responsive_image\Entity\ResponsiveImageStyle; use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests the responsive image field UI. - * - * @group responsive_image */ +#[Group('responsive_image')] class ResponsiveImageFieldUiTest extends WebDriverTestBase { use FieldUiJSTestTrait; diff --git a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php index b296446affb..07bee95122c 100644 --- a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php +++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php @@ -54,7 +54,7 @@ trait CookieResourceTestTrait { /** * {@inheritdoc} */ - protected function initAuthentication() { + protected function initAuthentication(): void { $user_login_url = Url::fromRoute('user.login.http') ->setRouteParameter('_format', static::$format); @@ -93,7 +93,7 @@ trait CookieResourceTestTrait { /** * {@inheritdoc} */ - protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) { + protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response): void { // Requests needing cookie authentication but missing it results in a 403 // response. The cookie authentication mechanism sets no response message. // Hence, effectively, this is just the 403 response that one gets as the @@ -121,7 +121,7 @@ trait CookieResourceTestTrait { /** * {@inheritdoc} */ - protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) { + protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options): void { // X-CSRF-Token request header is unnecessary for safe and side effect-free // HTTP methods. No need for additional assertions. // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php index 02866375e54..2a0d3179987 100644 --- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php @@ -358,7 +358,7 @@ abstract class ResourceTestBase extends BrowserTestBase { // sets it to 'text/html' by default. We also cannot detect the presence // of Apache either here in the CLI. For now having this documented here // is all we can do. - // $this->assertFalse($response->hasHeader('Content-Type')); + // "$this->assertFalse($response->hasHeader('Content-Type'));". $this->assertSame('', (string) $response->getBody()); } else { diff --git a/core/modules/search/config/schema/search.schema.yml b/core/modules/search/config/schema/search.schema.yml index 4dc4d53e057..519c37d0760 100644 --- a/core/modules/search/config/schema/search.schema.yml +++ b/core/modules/search/config/schema/search.schema.yml @@ -10,8 +10,7 @@ search.settings: type: integer label: 'AND/OR combination limit' constraints: - Range: - min: 0 + PositiveOrZero: ~ default_page: type: string label: 'Default search page' diff --git a/core/modules/search/search.module b/core/modules/search/search.module index 76d7bf1044e..0dc96f3ee89 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -9,22 +9,6 @@ use Drupal\Component\Utility\Unicode; use Drupal\search\SearchTextProcessorInterface; /** - * Implements hook_theme_suggestions_HOOK(). - */ -function search_theme_suggestions_search_result(array $variables): array { - return ['search_result__' . $variables['plugin_id']]; -} - -/** - * Implements hook_preprocess_HOOK() for block templates. - */ -function search_preprocess_block(&$variables): void { - if ($variables['plugin_id'] == 'search_form_block') { - $variables['attributes']['role'] = 'search'; - } -} - -/** * @defgroup search Search interface * @{ * The Drupal search interface manages a global search mechanism. diff --git a/core/modules/search/src/Hook/SearchThemeHooks.php b/core/modules/search/src/Hook/SearchThemeHooks.php new file mode 100644 index 00000000000..d18f53a8d29 --- /dev/null +++ b/core/modules/search/src/Hook/SearchThemeHooks.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\search\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for search. + */ +class SearchThemeHooks { + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_search_result')] + public function themeSuggestionsSearchResult(array $variables): array { + return [ + 'search_result__' . $variables['plugin_id'], + ]; + } + + /** + * Implements hook_preprocess_HOOK() for block templates. + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + if ($variables['plugin_id'] == 'search_form_block') { + $variables['attributes']['role'] = 'search'; + } + } + +} diff --git a/core/modules/search/tests/src/Functional/SearchCommentCountToggleTest.php b/core/modules/search/tests/src/Functional/SearchCommentCountToggleTest.php index 261df5bafea..0b2e12c4201 100644 --- a/core/modules/search/tests/src/Functional/SearchCommentCountToggleTest.php +++ b/core/modules/search/tests/src/Functional/SearchCommentCountToggleTest.php @@ -76,12 +76,12 @@ class SearchCommentCountToggleTest extends BrowserTestBase { $this->searchableNodes['1 comment'] = $this->drupalCreateNode($node_params); $this->searchableNodes['0 comments'] = $this->drupalCreateNode($node_params); - // Create a comment array + // Create a comment array. $edit_comment = []; $edit_comment['subject[0][value]'] = $this->randomMachineName(); $edit_comment['comment_body[0][value]'] = $this->randomMachineName(); - // Post comment to the test node with comment + // Post comment to the test node with comment. $this->drupalGet('comment/reply/node/' . $this->searchableNodes['1 comment']->id() . '/comment'); $this->submitForm($edit_comment, 'Save'); @@ -99,12 +99,12 @@ class SearchCommentCountToggleTest extends BrowserTestBase { ]; $this->drupalGet('search/node'); - // Test comment count display for nodes with comment status set to Open + // Test comment count display for nodes with comment status set to Open. $this->submitForm($edit, 'Search'); $this->assertSession()->pageTextContains('0 comments'); $this->assertSession()->pageTextContains('1 comment'); - // Test comment count display for nodes with comment status set to Closed + // Test comment count display for nodes with comment status set to Closed. $this->searchableNodes['0 comments']->set('comment', CommentItemInterface::CLOSED); $this->searchableNodes['0 comments']->save(); $this->searchableNodes['1 comment']->set('comment', CommentItemInterface::CLOSED); @@ -114,7 +114,7 @@ class SearchCommentCountToggleTest extends BrowserTestBase { $this->assertSession()->pageTextNotContains('0 comments'); $this->assertSession()->pageTextContains('1 comment'); - // Test comment count display for nodes with comment status set to Hidden + // Test comment count display for nodes with comment status set to Hidden. $this->searchableNodes['0 comments']->set('comment', CommentItemInterface::HIDDEN); $this->searchableNodes['0 comments']->save(); $this->searchableNodes['1 comment']->set('comment', CommentItemInterface::HIDDEN); diff --git a/core/modules/search/tests/src/Functional/SearchConfigSettingsFormTest.php b/core/modules/search/tests/src/Functional/SearchConfigSettingsFormTest.php index d6ca9ced6ed..6e07c0f7674 100644 --- a/core/modules/search/tests/src/Functional/SearchConfigSettingsFormTest.php +++ b/core/modules/search/tests/src/Functional/SearchConfigSettingsFormTest.php @@ -146,7 +146,7 @@ class SearchConfigSettingsFormTest extends BrowserTestBase { $this->drupalGet('admin/config/search/pages'); $this->clickLink('Edit', 1); - // Ensure that the default setting was picked up from the default config + // Ensure that the default setting was picked up from the default config. $this->assertTrue($this->assertSession()->optionExists('edit-extra-type-settings-boost', 'bi')->isSelected()); // Change extra type setting and also modify a common search setting. diff --git a/core/modules/search/tests/src/Functional/SearchNodeUpdateAndDeletionTest.php b/core/modules/search/tests/src/Functional/SearchNodeUpdateAndDeletionTest.php index 94a3b86850b..89c6e465cc6 100644 --- a/core/modules/search/tests/src/Functional/SearchNodeUpdateAndDeletionTest.php +++ b/core/modules/search/tests/src/Functional/SearchNodeUpdateAndDeletionTest.php @@ -65,17 +65,17 @@ class SearchNodeUpdateAndDeletionTest extends BrowserTestBase { $search_index = \Drupal::service('search.index'); assert($search_index instanceof SearchIndexInterface); - // Search the node to verify it appears in search results + // Search the node to verify it appears in search results. $edit = ['keys' => 'knights']; $this->drupalGet('search/node'); $this->submitForm($edit, 'Search'); $this->assertSession()->pageTextContains($node->label()); - // Update the node + // Update the node. $node->body->value = "We want a shrubbery!"; $node->save(); - // Run indexer again + // Run indexer again. $node_search_plugin->updateIndex(); // Search again to verify the new text appears in test results. @@ -100,7 +100,7 @@ class SearchNodeUpdateAndDeletionTest extends BrowserTestBase { // Update the search index. $node_search_plugin->updateIndex(); - // Search the node to verify it appears in search results + // Search the node to verify it appears in search results. $edit = ['keys' => 'dragons']; $this->drupalGet('search/node'); $this->submitForm($edit, 'Search'); diff --git a/core/modules/search/tests/src/Kernel/SearchExcerptTest.php b/core/modules/search/tests/src/Kernel/SearchExcerptTest.php index 76c9551966f..8f9845cbc92 100644 --- a/core/modules/search/tests/src/Kernel/SearchExcerptTest.php +++ b/core/modules/search/tests/src/Kernel/SearchExcerptTest.php @@ -61,7 +61,7 @@ class SearchExcerptTest extends KernelTestBase { $this->assertStringContainsString('í', $result, 'Entities are converted in excerpt'); // The node body that will produce this rendered $text is: - // 123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678 +‘ +‘ +‘ ‘ + // 123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678 +‘ +‘ +‘ ‘. $text = "<div class=\"field field--name-body field--type-text-with-summary field--label-hidden\"><div class=\"field__items\"><div class=\"field__item even\" property=\"content:encoded\"><p>123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678 +‘ +‘ +‘ ‘</p>\n</div></div></div> "; $result = $this->doSearchExcerpt('HTMLTest', $text); $this->assertNotEmpty($result, 'Rendered Multi-byte HTML encodings are not corrupted in search excerpts'); diff --git a/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php b/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php index 2f5a958054b..00732e0d7f8 100644 --- a/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php +++ b/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php @@ -185,11 +185,12 @@ class FieldItemSerializationTest extends NormalizerTestBase { $this->assertTrue($denormalized_entity->field_test_boolean->value); }; - // Asserts denormalizing the entity DOES yield the value we set: - // - when using the detailed representation + // Asserts denormalizing the entity DOES yield the value we set in two + // cases. + // One is when using the detailed representation. $core_normalization['field_test_boolean'][0]['value'] = TRUE; $assert_denormalization($core_normalization); - // - and when using the shorthand representation + // The second is and when using the shorthand representation. $core_normalization['field_test_boolean'][0] = TRUE; $assert_denormalization($core_normalization); @@ -200,11 +201,12 @@ class FieldItemSerializationTest extends NormalizerTestBase { $core_normalization = $this->container->get('serializer')->normalize($this->entity, $format); $this->assertSame('👎', $core_normalization['field_test_boolean'][0]['value']); - // Asserts denormalizing the entity DOES NOT ANYMORE yield the value we set: - // - when using the detailed representation + // Asserts denormalizing the entity DOES NOT ANYMORE yield the value we set + // in two cases. + // One is when using the detailed representation. $core_normalization['field_test_boolean'][0]['value'] = '👍'; $assert_denormalization($core_normalization); - // - and when using the shorthand representation + // The second is when using the shorthand representation. $core_normalization['field_test_boolean'][0] = '👍'; $assert_denormalization($core_normalization); } diff --git a/core/modules/settings_tray/settings_tray.module b/core/modules/settings_tray/settings_tray.module index a1a936d0339..f2460e6eb72 100644 --- a/core/modules/settings_tray/settings_tray.module +++ b/core/modules/settings_tray/settings_tray.module @@ -4,7 +4,6 @@ * @file */ -use Drupal\block\Entity\Block; use Drupal\block\BlockInterface; /** @@ -23,30 +22,3 @@ function _settings_tray_has_block_overrides(BlockInterface $block) { // and remove this function. return \Drupal::config($block->getEntityType()->getConfigPrefix() . '.' . $block->id())->hasOverrides(); } - -/** - * Implements hook_preprocess_HOOK() for block templates. - */ -function settings_tray_preprocess_block(&$variables): void { - // Only blocks that have a settings_tray form and have no configuration - // overrides will have a "Quick Edit" link. We could wait for the contextual - // links to be initialized on the client side, and then add the class and - // data- attribute below there (via JavaScript). But that would mean that it - // would be impossible to show Settings Tray's clickable regions immediately - // when the page loads. When latency is high, this will cause flicker. - // @see \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck - /** @var \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck $access_checker */ - $access_checker = \Drupal::service('access_check.settings_tray.block.settings_tray_form'); - /** @var \Drupal\Core\Block\BlockManagerInterface $block_plugin_manager */ - $block_plugin_manager = \Drupal::service('plugin.manager.block'); - /** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */ - $block_plugin = $block_plugin_manager->createInstance($variables['plugin_id']); - if (isset($variables['elements']['#contextual_links']['block']['route_parameters']['block'])) { - $block = Block::load($variables['elements']['#contextual_links']['block']['route_parameters']['block']); - if ($access_checker->accessBlockPlugin($block_plugin)->isAllowed() && !_settings_tray_has_block_overrides($block)) { - // Add class and attributes to all blocks to allow JavaScript to target. - $variables['attributes']['class'][] = 'settings-tray-editable'; - $variables['attributes']['data-drupal-settingstray'] = 'editable'; - } - } -} diff --git a/core/modules/settings_tray/src/Hook/SettingsTrayThemeHooks.php b/core/modules/settings_tray/src/Hook/SettingsTrayThemeHooks.php new file mode 100644 index 00000000000..741dadba454 --- /dev/null +++ b/core/modules/settings_tray/src/Hook/SettingsTrayThemeHooks.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\settings_tray\Hook; + +use Drupal\block\Entity\Block; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for settings_tray. + */ +class SettingsTrayThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for block templates. + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + // Only blocks that have a settings_tray form and have no configuration + // overrides will have a "Quick Edit" link. We could wait for the contextual + // links to be initialized on the client side, and then add the class and + // data- attribute below there (via JavaScript). But that would mean that it + // would be impossible to show Settings Tray's clickable regions immediately + // when the page loads. When latency is high, this will cause flicker. + // @see \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck + /** @var \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck $access_checker */ + $access_checker = \Drupal::service('access_check.settings_tray.block.settings_tray_form'); + /** @var \Drupal\Core\Block\BlockManagerInterface $block_plugin_manager */ + $block_plugin_manager = \Drupal::service('plugin.manager.block'); + /** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */ + $block_plugin = $block_plugin_manager->createInstance($variables['plugin_id']); + if (isset($variables['elements']['#contextual_links']['block']['route_parameters']['block'])) { + $block = Block::load($variables['elements']['#contextual_links']['block']['route_parameters']['block']); + if ($access_checker->accessBlockPlugin($block_plugin)->isAllowed() && !_settings_tray_has_block_overrides($block)) { + // Add class and attributes to all blocks to allow JavaScript to target. + $variables['attributes']['class'][] = 'settings-tray-editable'; + $variables['attributes']['data-drupal-settingstray'] = 'editable'; + } + } + } + +} diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/ConfigAccessTest.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/ConfigAccessTest.php index 023d3f3a367..2390d415f1d 100644 --- a/core/modules/settings_tray/tests/src/FunctionalJavascript/ConfigAccessTest.php +++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/ConfigAccessTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\settings_tray\FunctionalJavascript; use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\user\Entity\Role; +use PHPUnit\Framework\Attributes\Group; /** * Tests handling of configuration overrides. - * - * @group settings_tray */ +#[Group('settings_tray')] class ConfigAccessTest extends SettingsTrayTestBase { /** diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/OverriddenConfigurationTest.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/OverriddenConfigurationTest.php index 085b9d6def1..c40b17211bb 100644 --- a/core/modules/settings_tray/tests/src/FunctionalJavascript/OverriddenConfigurationTest.php +++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/OverriddenConfigurationTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\settings_tray\FunctionalJavascript; use Drupal\block\Entity\Block; use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\user\Entity\Role; +use PHPUnit\Framework\Attributes\Group; /** * Tests handling of configuration overrides. - * - * @group settings_tray */ +#[Group('settings_tray')] class OverriddenConfigurationTest extends SettingsTrayTestBase { /** diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php index 26c24424f9d..518c8581edf 100644 --- a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php +++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\settings_tray\FunctionalJavascript; use Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationIsClassBlock; use Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationNoneBlock; use Drupal\user\Entity\Role; +use PHPUnit\Framework\Attributes\Group; /** * Testing opening and saving block forms in the off-canvas dialog. - * - * @group settings_tray */ +#[Group('settings_tray')] class SettingsTrayBlockFormTest extends SettingsTrayTestBase { /** diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayTestBase.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayTestBase.php index 15b14a771e8..4b7ba9ca698 100644 --- a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayTestBase.php +++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayTestBase.php @@ -11,7 +11,7 @@ use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase; /** * Base class for Settings Tray tests. */ -class SettingsTrayTestBase extends OffCanvasTestBase { +abstract class SettingsTrayTestBase extends OffCanvasTestBase { use ContextualLinkClickTrait; diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index 2488fd97717..a7285c14f55 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -4,11 +4,8 @@ * @file */ -use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheableMetadata; -use Drupal\Core\Url; use Drupal\shortcut\ShortcutSetInterface; /** @@ -136,89 +133,3 @@ function shortcut_renderable_links($shortcut_set = NULL): array { return $shortcut_links; } - -/** - * Implements hook_preprocess_HOOK() for block templates. - */ -function shortcut_preprocess_block(&$variables): void { - if ($variables['configuration']['provider'] == 'shortcut') { - $variables['attributes']['role'] = 'navigation'; - } -} - -/** - * Implements hook_preprocess_HOOK() for page title templates. - */ -function shortcut_preprocess_page_title(&$variables): void { - // 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() && theme_get_setting('third_party_settings.shortcut.module_link') && !\Drupal::request()->attributes->has('exception')) { - $link = Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath(); - $route_match = \Drupal::routeMatch(); - - // Replicate template_preprocess_html()'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() ? t('Add to %shortcut_set shortcuts', ['%shortcut_set' => $shortcut_set->label()]) : 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() ? t('Remove from %shortcut_set shortcuts', ['%shortcut_set' => $shortcut_set->label()]) : 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('<span class="shortcut-action__icon"></span><span class="shortcut-action__message">@text</span>', ['@text' => $link_text]), - '#url' => Url::fromRoute($route_name, $route_parameters), - '#options' => ['query' => $query], - '#attributes' => [ - 'class' => [ - 'shortcut-action', - 'shortcut-action--' . $link_mode, - ], - ], - ]; - } -} diff --git a/core/modules/shortcut/src/Hook/ShortcutThemeHooks.php b/core/modules/shortcut/src/Hook/ShortcutThemeHooks.php new file mode 100644 index 00000000000..c86a64a140d --- /dev/null +++ b/core/modules/shortcut/src/Hook/ShortcutThemeHooks.php @@ -0,0 +1,111 @@ +<?php + +namespace Drupal\shortcut\Hook; + +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Url; +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Hook implementations for shortcut. + */ +class ShortcutThemeHooks { + use StringTranslationTrait; + + /** + * Implements hook_preprocess_HOOK() for block templates. + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + if ($variables['configuration']['provider'] == 'shortcut') { + $variables['attributes']['role'] = 'navigation'; + } + } + + /** + * Implements hook_preprocess_HOOK() for page title templates. + */ + #[Hook('preprocess_page_title')] + public function preprocessPageTitle(&$variables): void { + // 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() && theme_get_setting('third_party_settings.shortcut.module_link') && !\Drupal::request()->attributes->has('exception')) { + $link = Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath(); + $route_match = \Drupal::routeMatch(); + // Replicate template_preprocess_html()'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('<span class="shortcut-action__icon"></span><span class="shortcut-action__message">@text</span>', [ + '@text' => $link_text, + ]), + '#url' => Url::fromRoute($route_name, $route_parameters), + '#options' => [ + 'query' => $query, + ], + '#attributes' => [ + 'class' => [ + 'shortcut-action', + 'shortcut-action--' . $link_mode, + ], + ], + ]; + } + } + +} diff --git a/core/modules/shortcut/src/Plugin/migrate/destination/EntityShortcutSet.php b/core/modules/shortcut/src/Plugin/migrate/destination/EntityShortcutSet.php index 0f0ec102a84..dfc334b8f1d 100644 --- a/core/modules/shortcut/src/Plugin/migrate/destination/EntityShortcutSet.php +++ b/core/modules/shortcut/src/Plugin/migrate/destination/EntityShortcutSet.php @@ -18,7 +18,7 @@ class EntityShortcutSet extends EntityConfigBase { protected function getEntity(Row $row, array $old_destination_id_values) { $entity = parent::getEntity($row, $old_destination_id_values); // Set the "syncing" flag to TRUE, to avoid duplication of default - // shortcut links + // shortcut links. $entity->setSyncing(TRUE); return $entity; } diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 88f34652b98..854ef2a178e 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -69,14 +69,14 @@ system.cron: type: integer label: 'Requirements warning period' constraints: - # @see system_requirements() + # @see \Drupal\system\Hook\SystemRequirementsHooks Range: min: 60 requirements_error: type: integer label: 'Requirements error period' constraints: - # @see system_requirements() + # @see \Drupal\system\Hook\SystemRequirementsHooks Range: min: 300 logging: @@ -157,15 +157,13 @@ system.diff: label: 'Number of leading lines in a diff' constraints: # @see \Drupal\Component\Diff\DiffFormatter - Range: - min: 0 + PositiveOrZero: ~ lines_trailing: type: integer label: 'Number of trailing lines in a diff' constraints: # @see \Drupal\Component\Diff\DiffFormatter - Range: - min: 0 + PositiveOrZero: ~ system.logging: type: config_object @@ -355,8 +353,7 @@ system.file: type: integer label: 'Maximum age for temporary files' constraints: - Range: - min: 0 + PositiveOrZero: ~ system.image: type: config_object @@ -426,8 +423,7 @@ system.advisories: # Minimum can be set to 0 as it just means the advisories will be retrieved on every call. # @see \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher::getSecurityAdvisories constraints: - Range: - min: 0 + PositiveOrZero: ~ block.settings.system_branding_block: type: block_settings diff --git a/core/modules/system/css/components/reset-appearance.module.css b/core/modules/system/css/components/reset-appearance.module.css deleted file mode 100644 index 59741a85ce8..00000000000 --- a/core/modules/system/css/components/reset-appearance.module.css +++ /dev/null @@ -1,14 +0,0 @@ -/* - * @file - * Utility class to remove browser styles, especially for button. - */ - -.reset-appearance { - margin: 0; - padding: 0; - border: 0 none; - background: transparent; - line-height: inherit; - -webkit-appearance: none; - appearance: none; -} diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index 2f5ae051204..4fdb93c8b45 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -228,7 +228,7 @@ class DbUpdateController extends ControllerBase { $this->keyValueExpirableFactory->get('update_available_release')->deleteAll(); $build['info_header'] = [ - '#markup' => '<p>' . $this->t('Use this utility to update your database whenever a module, theme, or the core software is updated.') . '</p><p>' . $this->t('For more detailed information, see the <a href="https://www.drupal.org/upgrade">upgrading handbook</a>. If you are unsure what these terms mean you should probably contact your hosting provider.') . '</p>', + '#markup' => '<p>' . $this->t('Use this utility to update your database whenever a module, theme, or the core software is updated.') . '</p><p>' . $this->t('For more detailed information, see the <a href="https://www.drupal.org/docs/updating-drupal">Updating Drupal guide</a>. If you are unsure what these terms mean you should probably contact your hosting provider.') . '</p>', ]; $info[] = $this->t("<strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism."); @@ -705,16 +705,20 @@ class DbUpdateController extends ControllerBase { 'title' => $this->t('Front page'), 'url' => Url::fromRoute('<front>')->setOption('base_url', $base_url), ]; - if ($this->account->hasPermission('access administration pages')) { + + $admin_url = Url::fromRoute('system.admin')->setOption('base_url', $base_url); + if ($admin_url->access($this->account)) { $links['admin-pages'] = [ 'title' => $this->t('Administration pages'), - 'url' => Url::fromRoute('system.admin')->setOption('base_url', $base_url), + 'url' => $admin_url, ]; } - if ($this->account->hasPermission('administer site configuration')) { + + $status_report_url = Url::fromRoute('system.status')->setOption('base_url', $base_url); + if ($status_report_url->access($this->account)) { $links['status-report'] = [ 'title' => $this->t('Status report'), - 'url' => Url::fromRoute('system.status')->setOption('base_url', $base_url), + 'url' => $status_report_url, ]; } return $links; diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index b340555c628..fc253ac2f7c 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -280,8 +280,6 @@ class SystemController extends ControllerBase { continue; } - // @todo Add logic for not displaying hidden modules in - // https://drupal.org/node/3117829. $module_name = $modules[$dependency]->info['name']; $theme->module_dependencies_list[$dependency] = $modules[$dependency]->status ? $this->t('@module_name', ['@module_name' => $module_name]) : $this->t('@module_name (<span class="admin-disabled">disabled</span>)', ['@module_name' => $module_name]); diff --git a/core/modules/system/src/CronController.php b/core/modules/system/src/CronController.php index 6ba1e031a4d..59bc6e9289a 100644 --- a/core/modules/system/src/CronController.php +++ b/core/modules/system/src/CronController.php @@ -37,7 +37,7 @@ class CronController extends ControllerBase { public function run() { $this->cron->run(); - // HTTP 204 is "No content", meaning "I did what you asked and we're done." + // HTTP 204 is "No content", meaning "I did what you asked and we're done.". return new Response('', 204); } diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index 90dc9ead38b..551e813c611 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -329,7 +329,7 @@ class ModulesListForm extends FormBase { // Disable the checkbox for required modules. if (!empty($module->info['required'])) { // Used when displaying modules that are required by the installation - // profile + // profile. $row['enable']['#disabled'] = TRUE; $row['#required_by'][] = $distribution . (!empty($module->info['explanation']) ? ' (' . $module->info['explanation'] . ')' : ''); } @@ -375,8 +375,6 @@ class ModulesListForm extends FormBase { // If this module requires other modules, add them to the array. /** @var \Drupal\Core\Extension\Dependency $dependency_object */ foreach ($module->requires as $dependency => $dependency_object) { - // @todo Add logic for not displaying hidden modules in - // https://drupal.org/node/3117829. if ($incompatible = $this->checkDependencyMessage($modules, $dependency, $dependency_object)) { $row['#requires'][$dependency] = $incompatible; $row['enable']['#disabled'] = TRUE; diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php index c999f37fe23..9d51b67f2d3 100644 --- a/core/modules/system/src/Form/ModulesUninstallForm.php +++ b/core/modules/system/src/Form/ModulesUninstallForm.php @@ -106,7 +106,8 @@ class ModulesUninstallForm extends FormBase { include_once DRUPAL_ROOT . '/core/includes/install.inc'; // Get a list of all available modules that can be uninstalled. - $uninstallable = array_filter($this->moduleExtensionList->getList(), function ($module) { + $modules = $this->moduleExtensionList->getList(); + $uninstallable = array_filter($modules, function ($module) { return empty($module->info['required']) && $module->status; }); @@ -137,7 +138,7 @@ class ModulesUninstallForm extends FormBase { $form['modules'] = []; // Only build the rest of the form if there are any modules available to - // uninstall; + // uninstall. if (empty($uninstallable)) { return $form; } @@ -199,7 +200,13 @@ class ModulesUninstallForm extends FormBase { // we can allow this module to be uninstalled. foreach (array_keys($module->required_by) as $dependent) { if ($this->updateRegistry->getInstalledVersion($dependent) !== $this->updateRegistry::SCHEMA_UNINSTALLED) { - $form['modules'][$module->getName()]['#required_by'][] = $dependent; + $module_name = $modules[$dependent]->info['name']; + if ($dependent != strtolower(str_replace(' ', '_', $module_name))) { + $form['modules'][$module->getName()]['#required_by'][] = $module_name . " (" . $dependent . ")"; + } + else { + $form['modules'][$module->getName()]['#required_by'][] = $module_name; + } $form['uninstall'][$module->getName()]['#disabled'] = TRUE; } } diff --git a/core/modules/system/src/Form/ThemeSettingsForm.php b/core/modules/system/src/Form/ThemeSettingsForm.php index 9133eb8aa10..44f44179f5a 100644 --- a/core/modules/system/src/Form/ThemeSettingsForm.php +++ b/core/modules/system/src/Form/ThemeSettingsForm.php @@ -140,7 +140,8 @@ class ThemeSettingsForm extends ConfigFormBase { $themes = $this->themeHandler->listInfo(); - // Default settings are defined in theme_get_setting() in includes/theme.inc + // Default settings are defined in theme_get_setting() in + // includes/theme.inc. if ($theme) { if (!$this->themeHandler->hasUi($theme)) { throw new NotFoundHttpException(); @@ -168,7 +169,7 @@ class ThemeSettingsForm extends ConfigFormBase { '#value' => $config_key, ]; - // Toggle settings + // Toggle settings. $toggles = [ 'node_user_picture' => $this->t('User pictures in posts'), 'comment_user_picture' => $this->t('User pictures in comments'), @@ -176,7 +177,7 @@ class ThemeSettingsForm extends ConfigFormBase { 'favicon' => $this->t('Shortcut icon'), ]; - // Some features are not always available + // Some features are not always available. $disabled = []; if (!user_picture_enabled()) { $disabled['toggle_node_user_picture'] = TRUE; diff --git a/core/modules/system/src/Hook/PageAttachmentsHook.php b/core/modules/system/src/Hook/PageAttachmentsHook.php index fb6335f90c3..3f271571ede 100644 --- a/core/modules/system/src/Hook/PageAttachmentsHook.php +++ b/core/modules/system/src/Hook/PageAttachmentsHook.php @@ -19,7 +19,7 @@ final class PageAttachmentsHook { /** * Implements hook_page_attachments(). * - * @see template_preprocess_maintenance_page() + * @see \Drupal\Core\Theme\ThemePreprocess::preprocessMaintenancePage() * @see \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter */ #[Hook('page_attachments')] diff --git a/core/modules/system/src/Hook/SystemHooks.php b/core/modules/system/src/Hook/SystemHooks.php index 800a1d718f3..e1be05077e2 100644 --- a/core/modules/system/src/Hook/SystemHooks.php +++ b/core/modules/system/src/Hook/SystemHooks.php @@ -339,7 +339,7 @@ class SystemHooks { \Drupal::service('file.htaccess_writer')->ensure(); if (\Drupal::config('system.advisories')->get('enabled')) { // Fetch the security advisories so that they will be pre-fetched during - // _system_advisories_requirements() and system_page_top(). + // systemAdvisoriesRequirements() and system_page_top(). /** @var \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher $fetcher */ $fetcher = \Drupal::service('system.sa_fetcher'); $fetcher->getSecurityAdvisories(); diff --git a/core/modules/system/src/Hook/SystemRequirementsHooks.php b/core/modules/system/src/Hook/SystemRequirementsHooks.php new file mode 100644 index 00000000000..49d318eb9bf --- /dev/null +++ b/core/modules/system/src/Hook/SystemRequirementsHooks.php @@ -0,0 +1,29 @@ +<?php + +namespace Drupal\system\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\system\Install\Requirements\SystemRequirements; + +/** + * Requirements hook implementations for system module. + */ +class SystemRequirementsHooks { + + /** + * Implements hook_update_requirements(). + */ + #[Hook('update_requirements')] + public function updateRequirements(): array { + return SystemRequirements::checkRequirements('update'); + } + + /** + * Implements hook_runtime_requirements(). + */ + #[Hook('runtime_requirements')] + public function runtimeRequirements(): array { + return SystemRequirements::checkRequirements('runtime'); + } + +} diff --git a/core/modules/system/src/Hook/SystemThemeHooks.php b/core/modules/system/src/Hook/SystemThemeHooks.php new file mode 100644 index 00000000000..728a02f7498 --- /dev/null +++ b/core/modules/system/src/Hook/SystemThemeHooks.php @@ -0,0 +1,138 @@ +<?php + +namespace Drupal\system\Hook; + +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for system. + */ +class SystemThemeHooks { + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_html')] + public function themeSuggestionsHtml(array $variables): array { + $path_args = explode('/', trim(\Drupal::service('path.current')->getPath(), '/')); + return theme_get_suggestions($path_args, 'html'); + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_page')] + public function themeSuggestionsPage(array $variables): array { + $path_args = explode('/', trim(\Drupal::service('path.current')->getPath(), '/')); + $suggestions = theme_get_suggestions($path_args, 'page'); + $supported_http_error_codes = [ + 401, + 403, + 404, + ]; + $exception = \Drupal::requestStack()->getCurrentRequest()->attributes->get('exception'); + if ($exception instanceof HttpExceptionInterface && in_array($exception->getStatusCode(), $supported_http_error_codes, TRUE)) { + $suggestions[] = 'page__4xx'; + $suggestions[] = 'page__' . $exception->getStatusCode(); + } + return $suggestions; + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_maintenance_page')] + public function themeSuggestionsMaintenancePage(array $variables): array { + $suggestions = []; + // Dead databases will show error messages so supplying this template will + // allow themers to override the page and the content completely. + $offline = defined('MAINTENANCE_MODE'); + try { + \Drupal::service('path.matcher')->isFrontPage(); + } + catch (\Exception) { + // The database is not yet available. + $offline = TRUE; + } + if ($offline) { + $suggestions[] = 'maintenance_page__offline'; + } + return $suggestions; + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_region')] + public function themeSuggestionsRegion(array $variables): array { + $suggestions = []; + if (!empty($variables['elements']['#region'])) { + $suggestions[] = 'region__' . $variables['elements']['#region']; + } + return $suggestions; + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_field')] + public function themeSuggestionsField(array $variables): array { + $suggestions = []; + $element = $variables['element']; + $suggestions[] = 'field__' . $element['#field_type']; + $suggestions[] = 'field__' . $element['#field_name']; + $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#bundle']; + $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name']; + $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle']; + return $suggestions; + } + + /** + * @} End of "defgroup authorize". + */ + + /** + * Implements hook_preprocess_HOOK() for block templates. + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + switch ($variables['base_plugin_id']) { + case 'system_branding_block': + $variables['site_logo'] = ''; + if ($variables['content']['site_logo']['#access'] && $variables['content']['site_logo']['#uri']) { + $variables['site_logo'] = $variables['content']['site_logo']['#uri']; + } + $variables['site_name'] = ''; + if ($variables['content']['site_name']['#access'] && $variables['content']['site_name']['#markup']) { + $variables['site_name'] = $variables['content']['site_name']['#markup']; + } + $variables['site_slogan'] = ''; + if ($variables['content']['site_slogan']['#access'] && $variables['content']['site_slogan']['#markup']) { + $variables['site_slogan'] = [ + '#markup' => $variables['content']['site_slogan']['#markup'], + ]; + } + break; + } + } + + /** + * Implements hook_preprocess_toolbar(). + */ + #[Hook('preprocess_toolbar')] + public function preprocessToolbar(array &$variables, $hook, $info): void { + // When Claro is the admin theme, Claro overrides the active theme's if that + // active theme is not Claro. Because of these potential overrides, the + // toolbar cache should be invalidated any time the default or admin theme + // changes. + $variables['#cache']['tags'][] = 'config:system.theme'; + // If Claro is the admin theme but not the active theme, still include + // Claro's toolbar preprocessing. + if (_system_is_claro_admin_and_not_active()) { + require_once DRUPAL_ROOT . '/core/themes/claro/claro.theme'; + claro_preprocess_toolbar($variables, $hook, $info); + } + } + +} diff --git a/core/modules/system/src/Install/Requirements/SystemRequirements.php b/core/modules/system/src/Install/Requirements/SystemRequirements.php new file mode 100644 index 00000000000..ee6a1d7a802 --- /dev/null +++ b/core/modules/system/src/Install/Requirements/SystemRequirements.php @@ -0,0 +1,1665 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\system\Install\Requirements; + +use Drupal\Core\Extension\InstallRequirementsInterface; +use Drupal\Component\FileSystem\FileSystem as FileSystemComponent; +use Drupal\Component\Utility\Bytes; +use Drupal\Component\Utility\Environment; +use Drupal\Component\Utility\OpCodeCache; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Database\Database; +use Drupal\Core\DrupalKernel; +use Drupal\Core\Extension\ExtensionLifecycle; +use Drupal\Core\Extension\Requirement\RequirementSeverity; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Link; +use Drupal\Core\Render\Markup; +use Drupal\Core\Site\Settings; +use Drupal\Core\StreamWrapper\PrivateStream; +use Drupal\Core\StreamWrapper\PublicStream; +use Drupal\Core\StringTranslation\ByteSizeMarkup; +use Drupal\Core\StringTranslation\PluralTranslatableMarkup; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Url; +use Drupal\Core\Utility\Error; +use Drupal\Core\Utility\PhpRequirements; +use Psr\Http\Client\ClientExceptionInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Install time requirements for the system module. + */ +class SystemRequirements implements InstallRequirementsInterface { + + /** + * {@inheritdoc} + */ + public static function getRequirements(): array { + return self::checkRequirements('install'); + } + + /** + * Check requirements for a given phase. + * + * @param string $phase + * The phase in which requirements are checked, as documented in + * hook_runtime_requirements() and hook_update_requirements(). + * + * @return array + * An associative array of requirements, as documented in + * hook_runtime_requirements() and hook_update_requirements(). + */ + public static function checkRequirements(string $phase): array { + global $install_state; + + // Get the current default PHP requirements for this version of Drupal. + $minimum_supported_php = PhpRequirements::getMinimumSupportedPhp(); + + // Reset the extension lists. + /** @var \Drupal\Core\Extension\ModuleExtensionList $module_extension_list */ + $module_extension_list = \Drupal::service('extension.list.module'); + $module_extension_list->reset(); + /** @var \Drupal\Core\Extension\ThemeExtensionList $theme_extension_list */ + $theme_extension_list = \Drupal::service('extension.list.theme'); + $theme_extension_list->reset(); + $requirements = []; + + // Report Drupal version + if ($phase == 'runtime') { + $requirements['drupal'] = [ + 'title' => t('Drupal'), + 'value' => \Drupal::VERSION, + 'severity' => RequirementSeverity::Info, + 'weight' => -10, + ]; + + // Display the currently active installation profile, if the site + // is not running the default installation profile. + $profile = \Drupal::installProfile(); + if ($profile != 'standard' && !empty($profile)) { + $info = $module_extension_list->getExtensionInfo($profile); + $requirements['install_profile'] = [ + 'title' => t('Installation profile'), + 'value' => t('%profile_name (%profile%version)', [ + '%profile_name' => $info['name'], + '%profile' => $profile, + '%version' => !empty($info['version']) ? '-' . $info['version'] : '', + ]), + 'severity' => RequirementSeverity::Info, + 'weight' => -9, + ]; + } + + // Gather all obsolete and experimental modules being enabled. + $obsolete_extensions = []; + $deprecated_modules = []; + $experimental_modules = []; + $enabled_modules = \Drupal::moduleHandler()->getModuleList(); + foreach ($enabled_modules as $module => $data) { + $info = $module_extension_list->getExtensionInfo($module); + if (isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER])) { + if ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) { + $experimental_modules[$module] = $info['name']; + } + elseif ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) { + $deprecated_modules[] = ['name' => $info['name'], 'lifecycle_link' => $info['lifecycle_link']]; + } + elseif ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::OBSOLETE) { + $obsolete_extensions[$module] = ['name' => $info['name'], 'lifecycle_link' => $info['lifecycle_link']]; + } + } + } + + // Warn if any experimental modules are installed. + if (!empty($experimental_modules)) { + $requirements['experimental_modules'] = [ + 'title' => t('Experimental modules installed'), + 'value' => t('Experimental modules found: %module_list. <a href=":url">Experimental modules</a> are provided for testing purposes only. Use at your own risk.', ['%module_list' => implode(', ', $experimental_modules), ':url' => 'https://www.drupal.org/core/experimental']), + 'severity' => RequirementSeverity::Warning, + ]; + } + // Warn if any deprecated modules are installed. + if (!empty($deprecated_modules)) { + foreach ($deprecated_modules as $deprecated_module) { + $deprecated_modules_link_list[] = (string) Link::fromTextAndUrl($deprecated_module['name'], Url::fromUri($deprecated_module['lifecycle_link']))->toString(); + } + $requirements['deprecated_modules'] = [ + 'title' => t('Deprecated modules installed'), + 'value' => t('Deprecated modules found: %module_list.', [ + '%module_list' => Markup::create(implode(', ', $deprecated_modules_link_list)), + ]), + 'severity' => RequirementSeverity::Warning, + ]; + } + + // Gather all obsolete and experimental themes being installed. + $experimental_themes = []; + $deprecated_themes = []; + $installed_themes = \Drupal::service('theme_handler')->listInfo(); + foreach ($installed_themes as $theme => $data) { + $info = $theme_extension_list->getExtensionInfo($theme); + if (isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER])) { + if ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) { + $experimental_themes[$theme] = $info['name']; + } + elseif ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) { + $deprecated_themes[] = ['name' => $info['name'], 'lifecycle_link' => $info['lifecycle_link']]; + } + elseif ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::OBSOLETE) { + $obsolete_extensions[$theme] = ['name' => $info['name'], 'lifecycle_link' => $info['lifecycle_link']]; + } + } + } + + // Warn if any experimental themes are installed. + if (!empty($experimental_themes)) { + $requirements['experimental_themes'] = [ + 'title' => t('Experimental themes installed'), + 'value' => t('Experimental themes found: %theme_list. Experimental themes are provided for testing purposes only. Use at your own risk.', ['%theme_list' => implode(', ', $experimental_themes)]), + 'severity' => RequirementSeverity::Warning, + ]; + } + + // Warn if any deprecated themes are installed. + if (!empty($deprecated_themes)) { + foreach ($deprecated_themes as $deprecated_theme) { + $deprecated_themes_link_list[] = (string) Link::fromTextAndUrl($deprecated_theme['name'], Url::fromUri($deprecated_theme['lifecycle_link']))->toString(); + + } + $requirements['deprecated_themes'] = [ + 'title' => t('Deprecated themes installed'), + 'value' => t('Deprecated themes found: %theme_list.', [ + '%theme_list' => Markup::create(implode(', ', $deprecated_themes_link_list)), + ]), + 'severity' => RequirementSeverity::Warning, + ]; + } + + // Warn if any obsolete extensions (themes or modules) are installed. + if (!empty($obsolete_extensions)) { + foreach ($obsolete_extensions as $obsolete_extension) { + $obsolete_extensions_link_list[] = (string) Link::fromTextAndUrl($obsolete_extension['name'], Url::fromUri($obsolete_extension['lifecycle_link']))->toString(); + } + $requirements['obsolete_extensions'] = [ + 'title' => t('Obsolete extensions installed'), + 'value' => t('Obsolete extensions found: %extensions. Obsolete extensions are provided only so that they can be uninstalled cleanly. You should immediately <a href=":uninstall_url">uninstall these extensions</a> since they may be removed in a future release.', [ + '%extensions' => Markup::create(implode(', ', $obsolete_extensions_link_list)), + ':uninstall_url' => Url::fromRoute('system.modules_uninstall')->toString(), + ]), + 'severity' => RequirementSeverity::Warning, + ]; + } + self::systemAdvisoriesRequirements($requirements); + } + + // Web server information. + $request_object = \Drupal::request(); + $software = $request_object->server->get('SERVER_SOFTWARE'); + $requirements['webserver'] = [ + 'title' => t('Web server'), + 'value' => $software, + ]; + + // Tests clean URL support. + if ($phase == 'install' && $install_state['interactive'] && !$request_object->query->has('rewrite') && str_contains($software, 'Apache')) { + // If the Apache rewrite module is not enabled, Apache version must be >= + // 2.2.16 because of the FallbackResource directive in the root .htaccess + // file. Since the Apache version reported by the server is dependent on + // the ServerTokens setting in httpd.conf, we may not be able to + // determine if a given config is valid. Thus we are unable to use + // version_compare() as we need have three possible outcomes: the version + // of Apache is greater than 2.2.16, is less than 2.2.16, or cannot be + // determined accurately. In the first case, we encourage the use of + // mod_rewrite; in the second case, we raise an error regarding the + // minimum Apache version; in the third case, we raise a warning that the + // current version of Apache may not be supported. + $rewrite_warning = FALSE; + $rewrite_error = FALSE; + $apache_version_string = 'Apache'; + + // Determine the Apache version number: major, minor and revision. + if (preg_match('/Apache\/(\d+)\.?(\d+)?\.?(\d+)?/', $software, $matches)) { + $apache_version_string = $matches[0]; + + // Major version number + if ($matches[1] < 2) { + $rewrite_error = TRUE; + } + elseif ($matches[1] == 2) { + if (!isset($matches[2])) { + $rewrite_warning = TRUE; + } + elseif ($matches[2] < 2) { + $rewrite_error = TRUE; + } + elseif ($matches[2] == 2) { + if (!isset($matches[3])) { + $rewrite_warning = TRUE; + } + elseif ($matches[3] < 16) { + $rewrite_error = TRUE; + } + } + } + } + else { + $rewrite_warning = TRUE; + } + + if ($rewrite_warning) { + $requirements['apache_version'] = [ + 'title' => t('Apache version'), + 'value' => $apache_version_string, + 'severity' => RequirementSeverity::Warning, + 'description' => t('Due to the settings for ServerTokens in httpd.conf, it is impossible to accurately determine the version of Apache running on this server. The reported value is @reported, to run Drupal without mod_rewrite, a minimum version of 2.2.16 is needed.', ['@reported' => $apache_version_string]), + ]; + } + + if ($rewrite_error) { + $requirements['Apache version'] = [ + 'title' => t('Apache version'), + 'value' => $apache_version_string, + 'severity' => RequirementSeverity::Error, + 'description' => t('The minimum version of Apache needed to run Drupal without mod_rewrite enabled is 2.2.16. See the <a href=":link">enabling clean URLs</a> page for more information on mod_rewrite.', [':link' => 'https://www.drupal.org/docs/8/clean-urls-in-drupal-8']), + ]; + } + + if (!$rewrite_error && !$rewrite_warning) { + $requirements['rewrite_module'] = [ + 'title' => t('Clean URLs'), + 'value' => t('Disabled'), + 'severity' => RequirementSeverity::Warning, + 'description' => t('Your server is capable of using clean URLs, but it is not enabled. Using clean URLs gives an improved user experience and is recommended. <a href=":link">Enable clean URLs</a>', [':link' => 'https://www.drupal.org/docs/8/clean-urls-in-drupal-8']), + ]; + } + } + + // Verify the user is running a supported PHP version. + // If the site is running a recommended version of PHP, just display it + // as an informational message on the status report. This will be overridden + // with an error or warning if the site is running older PHP versions for + // which Drupal has already or will soon drop support. + $phpversion = $phpversion_label = phpversion(); + if ($phase === 'runtime') { + $phpversion_label = t('@phpversion (<a href=":url">more information</a>)', [ + '@phpversion' => $phpversion, + ':url' => (new Url('system.php'))->toString(), + ]); + } + $requirements['php'] = [ + 'title' => t('PHP'), + 'value' => $phpversion_label, + ]; + + // Check if the PHP version is below what Drupal supports. + if (version_compare($phpversion, $minimum_supported_php) < 0) { + $requirements['php']['description'] = t('Your PHP installation is too old. Drupal requires at least PHP %version. It is recommended to upgrade to PHP version %recommended or higher for the best ongoing support. See <a href="http://php.net/supported-versions.php">PHP\'s version support documentation</a> and the <a href=":php_requirements">Drupal PHP requirements</a> page for more information.', + [ + '%version' => $minimum_supported_php, + '%recommended' => \Drupal::RECOMMENDED_PHP, + ':php_requirements' => 'https://www.drupal.org/docs/system-requirements/php-requirements', + ] + ); + + // If the PHP version is also below the absolute minimum allowed, it's not + // safe to continue with the requirements check, and should always be an + // error. + if (version_compare($phpversion, \Drupal::MINIMUM_PHP) < 0) { + $requirements['php']['severity'] = RequirementSeverity::Error; + return $requirements; + } + // Otherwise, the message should be an error at runtime, and a warning + // during installation or update. + $requirements['php']['severity'] = ($phase === 'runtime') ? RequirementSeverity::Error : RequirementSeverity::Warning; + } + // For PHP versions that are still supported but no longer recommended, + // inform users of what's recommended, allowing them to take action before + // it becomes urgent. + elseif ($phase === 'runtime' && version_compare($phpversion, \Drupal::RECOMMENDED_PHP) < 0) { + $requirements['php']['description'] = t('It is recommended to upgrade to PHP version %recommended or higher for the best ongoing support. See <a href="http://php.net/supported-versions.php">PHP\'s version support documentation</a> and the <a href=":php_requirements">Drupal PHP requirements</a> page for more information.', ['%recommended' => \Drupal::RECOMMENDED_PHP, ':php_requirements' => 'https://www.drupal.org/docs/system-requirements/php-requirements']); + $requirements['php']['severity'] = RequirementSeverity::Info; + } + + // Test for PHP extensions. + $requirements['php_extensions'] = [ + 'title' => t('PHP extensions'), + ]; + + $missing_extensions = []; + $required_extensions = [ + 'date', + 'dom', + 'filter', + 'gd', + 'hash', + 'json', + 'pcre', + 'pdo', + 'session', + 'SimpleXML', + 'SPL', + 'tokenizer', + 'xml', + 'zlib', + ]; + foreach ($required_extensions as $extension) { + if (!extension_loaded($extension)) { + $missing_extensions[] = $extension; + } + } + + if (!empty($missing_extensions)) { + $description = t('Drupal requires you to enable the PHP extensions in the following list (see the <a href=":system_requirements">system requirements page</a> for more information):', [ + ':system_requirements' => 'https://www.drupal.org/docs/system-requirements', + ]); + + // We use twig inline_template to avoid twig's autoescape. + $description = [ + '#type' => 'inline_template', + '#template' => '{{ description }}{{ missing_extensions }}', + '#context' => [ + 'description' => $description, + 'missing_extensions' => [ + '#theme' => 'item_list', + '#items' => $missing_extensions, + ], + ], + ]; + + $requirements['php_extensions']['value'] = t('Disabled'); + $requirements['php_extensions']['severity'] = RequirementSeverity::Error; + $requirements['php_extensions']['description'] = $description; + } + else { + $requirements['php_extensions']['value'] = t('Enabled'); + } + + if ($phase == 'install' || $phase == 'runtime') { + // Check to see if OPcache is installed. + if (!OpCodeCache::isEnabled()) { + $requirements['php_opcache'] = [ + 'value' => t('Not enabled'), + 'severity' => RequirementSeverity::Warning, + 'description' => t('PHP OPcode caching can improve your site\'s performance considerably. It is <strong>highly recommended</strong> to have <a href="http://php.net/manual/opcache.installation.php" target="_blank">OPcache</a> installed on your server.'), + ]; + } + else { + $requirements['php_opcache']['value'] = t('Enabled'); + } + $requirements['php_opcache']['title'] = t('PHP OPcode caching'); + } + + // Check to see if APCu is installed and configured correctly. + if ($phase == 'runtime' && PHP_SAPI != 'cli') { + $requirements['php_apcu_enabled']['title'] = t('PHP APCu caching'); + $requirements['php_apcu_available']['title'] = t('PHP APCu available caching'); + if (extension_loaded('apcu') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN)) { + $memory_info = apcu_sma_info(TRUE); + $apcu_actual_size = ByteSizeMarkup::create($memory_info['seg_size'] * $memory_info['num_seg']); + $apcu_recommended_size = '32 MB'; + $requirements['php_apcu_enabled']['value'] = t('Enabled (@size)', ['@size' => $apcu_actual_size]); + if (Bytes::toNumber(ini_get('apc.shm_size')) * ini_get('apc.shm_segments') < Bytes::toNumber($apcu_recommended_size)) { + $requirements['php_apcu_enabled']['severity'] = RequirementSeverity::Warning; + $requirements['php_apcu_enabled']['description'] = t('Depending on your configuration, Drupal can run with a @apcu_size APCu limit. However, a @apcu_default_size APCu limit (the default) or above is recommended, especially if your site uses additional custom or contributed modules.', [ + '@apcu_size' => $apcu_actual_size, + '@apcu_default_size' => $apcu_recommended_size, + ]); + } + else { + $memory_available = $memory_info['avail_mem'] / ($memory_info['seg_size'] * $memory_info['num_seg']); + if ($memory_available < 0.1) { + $requirements['php_apcu_available']['severity'] = RequirementSeverity::Error; + $requirements['php_apcu_available']['description'] = t('APCu is using over 90% of its allotted memory (@apcu_actual_size). To improve APCu performance, consider increasing this limit.', [ + '@apcu_actual_size' => $apcu_actual_size, + ]); + } + elseif ($memory_available < 0.25) { + $requirements['php_apcu_available']['severity'] = RequirementSeverity::Warning; + $requirements['php_apcu_available']['description'] = t('APCu is using over 75% of its allotted memory (@apcu_actual_size). To improve APCu performance, consider increasing this limit.', [ + '@apcu_actual_size' => $apcu_actual_size, + ]); + } + else { + $requirements['php_apcu_available']['severity'] = RequirementSeverity::OK; + } + $requirements['php_apcu_available']['value'] = t('Memory available: @available.', [ + '@available' => ByteSizeMarkup::create($memory_info['avail_mem']), + ]); + } + } + else { + $requirements['php_apcu_enabled'] += [ + 'value' => t('Not enabled'), + 'severity' => RequirementSeverity::Info, + 'description' => t('PHP APCu caching can improve your site\'s performance considerably. It is <strong>highly recommended</strong> to have <a href="https://www.php.net/manual/apcu.installation.php" target="_blank">APCu</a> installed on your server.'), + ]; + } + } + + if ($phase != 'update') { + // Test whether we have a good source of random bytes. + $requirements['php_random_bytes'] = [ + 'title' => t('Random number generation'), + ]; + try { + $bytes = random_bytes(10); + if (strlen($bytes) != 10) { + throw new \Exception("Tried to generate 10 random bytes, generated '" . strlen($bytes) . "'"); + } + $requirements['php_random_bytes']['value'] = t('Successful'); + } + catch (\Exception $e) { + // If /dev/urandom is not available on a UNIX-like system, check whether + // open_basedir restrictions are the cause. + $open_basedir_blocks_urandom = FALSE; + if (DIRECTORY_SEPARATOR === '/' && !@is_readable('/dev/urandom')) { + $open_basedir = ini_get('open_basedir'); + if ($open_basedir) { + $open_basedir_paths = explode(PATH_SEPARATOR, $open_basedir); + $open_basedir_blocks_urandom = !array_intersect(['/dev', '/dev/', '/dev/urandom'], $open_basedir_paths); + } + } + $args = [ + ':drupal-php' => 'https://www.drupal.org/docs/system-requirements/php-requirements', + '%exception_message' => $e->getMessage(), + ]; + if ($open_basedir_blocks_urandom) { + $requirements['php_random_bytes']['description'] = t('Drupal is unable to generate highly randomized numbers, which means certain security features like password reset URLs are not as secure as they should be. Instead, only a slow, less-secure fallback generator is available. The most likely cause is that open_basedir restrictions are in effect and /dev/urandom is not on the allowed list. See the <a href=":drupal-php">system requirements</a> page for more information. %exception_message', $args); + } + else { + $requirements['php_random_bytes']['description'] = t('Drupal is unable to generate highly randomized numbers, which means certain security features like password reset URLs are not as secure as they should be. Instead, only a slow, less-secure fallback generator is available. See the <a href=":drupal-php">system requirements</a> page for more information. %exception_message', $args); + } + $requirements['php_random_bytes']['value'] = t('Less secure'); + $requirements['php_random_bytes']['severity'] = RequirementSeverity::Error; + } + } + + if ($phase === 'runtime' && PHP_SAPI !== 'cli') { + if (!function_exists('fastcgi_finish_request') && !function_exists('litespeed_finish_request') && !ob_get_status()) { + $requirements['output_buffering'] = [ + 'title' => t('Output Buffering'), + 'error_value' => t('Not enabled'), + 'severity' => RequirementSeverity::Warning, + 'description' => t('<a href="https://www.php.net/manual/en/function.ob-start.php">Output buffering</a> is not enabled. This may degrade Drupal\'s performance. You can enable output buffering by default <a href="https://www.php.net/manual/en/outcontrol.configuration.php#ini.output-buffering">in your PHP settings</a>.'), + ]; + } + } + + if ($phase == 'install' || $phase == 'update') { + // Test for PDO (database). + $requirements['database_extensions'] = [ + 'title' => t('Database support'), + ]; + + // Make sure PDO is available. + $database_ok = extension_loaded('pdo'); + if (!$database_ok) { + $pdo_message = t('Your web server does not appear to support PDO (PHP Data Objects). Ask your hosting provider if they support the native PDO extension. See the <a href=":link">system requirements</a> page for more information.', [ + ':link' => 'https://www.drupal.org/docs/system-requirements/php-requirements#database', + ]); + } + else { + // Make sure at least one supported database driver exists. + if (empty(Database::getDriverList()->getInstallableList())) { + $database_ok = FALSE; + $pdo_message = t('Your web server does not appear to support any common PDO database extensions. Check with your hosting provider to see if they support PDO (PHP Data Objects) and offer any databases that <a href=":drupal-databases">Drupal supports</a>.', [ + ':drupal-databases' => 'https://www.drupal.org/docs/system-requirements/database-server-requirements', + ]); + } + // Make sure the native PDO extension is available, not the older PEAR + // version. (See install_verify_pdo() for details.) + if (!defined('PDO::ATTR_DEFAULT_FETCH_MODE')) { + $database_ok = FALSE; + $pdo_message = t('Your web server seems to have the wrong version of PDO installed. Drupal requires the PDO extension from PHP core. This system has the older PECL version. See the <a href=":link">system requirements</a> page for more information.', [ + ':link' => 'https://www.drupal.org/docs/system-requirements/php-requirements#database', + ]); + } + } + + if (!$database_ok) { + $requirements['database_extensions']['value'] = t('Disabled'); + $requirements['database_extensions']['severity'] = RequirementSeverity::Error; + $requirements['database_extensions']['description'] = $pdo_message; + } + else { + $requirements['database_extensions']['value'] = t('Enabled'); + } + } + + if ($phase === 'runtime' || $phase === 'update') { + // Database information. + $class = Database::getConnection()->getConnectionOptions()['namespace'] . '\\Install\\Tasks'; + /** @var \Drupal\Core\Database\Install\Tasks $tasks */ + $tasks = new $class(); + $requirements['database_system'] = [ + 'title' => t('Database system'), + 'value' => $tasks->name(), + ]; + $requirements['database_system_version'] = [ + 'title' => t('Database system version'), + 'value' => Database::getConnection()->version(), + ]; + + $errors = $tasks->engineVersionRequirementsCheck(); + $error_count = count($errors); + if ($error_count > 0) { + $error_message = [ + '#theme' => 'item_list', + '#items' => $errors, + // Use the comma-list style to display a single error without bullets. + '#context' => ['list_style' => $error_count === 1 ? 'comma-list' : ''], + ]; + $requirements['database_system_version']['severity'] = RequirementSeverity::Error; + $requirements['database_system_version']['description'] = $error_message; + } + } + + if ($phase === 'runtime' || $phase === 'update') { + // Test database JSON support. + $requirements['database_support_json'] = [ + 'title' => t('Database support for JSON'), + 'severity' => RequirementSeverity::OK, + 'value' => t('Available'), + 'description' => t('Drupal requires databases that support JSON storage.'), + ]; + + if (!Database::getConnection()->hasJson()) { + $requirements['database_support_json']['value'] = t('Not available'); + $requirements['database_support_json']['severity'] = RequirementSeverity::Error; + } + } + + // Test PHP memory_limit + $memory_limit = ini_get('memory_limit'); + $requirements['php_memory_limit'] = [ + 'title' => t('PHP memory limit'), + 'value' => $memory_limit == -1 ? t('-1 (Unlimited)') : $memory_limit, + ]; + + if (!Environment::checkMemoryLimit(\Drupal::MINIMUM_PHP_MEMORY_LIMIT, $memory_limit)) { + $description = []; + if ($phase == 'install') { + $description['phase'] = t('Consider increasing your PHP memory limit to %memory_minimum_limit to help prevent errors in the installation process.', ['%memory_minimum_limit' => \Drupal::MINIMUM_PHP_MEMORY_LIMIT]); + } + elseif ($phase == 'update') { + $description['phase'] = t('Consider increasing your PHP memory limit to %memory_minimum_limit to help prevent errors in the update process.', ['%memory_minimum_limit' => \Drupal::MINIMUM_PHP_MEMORY_LIMIT]); + } + elseif ($phase == 'runtime') { + $description['phase'] = t('Depending on your configuration, Drupal can run with a %memory_limit PHP memory limit. However, a %memory_minimum_limit PHP memory limit or above is recommended, especially if your site uses additional custom or contributed modules.', ['%memory_limit' => $memory_limit, '%memory_minimum_limit' => \Drupal::MINIMUM_PHP_MEMORY_LIMIT]); + } + + if (!empty($description['phase'])) { + if ($php_ini_path = get_cfg_var('cfg_file_path')) { + $description['memory'] = t('Increase the memory limit by editing the memory_limit parameter in the file %configuration-file and then restart your web server (or contact your system administrator or hosting provider for assistance).', ['%configuration-file' => $php_ini_path]); + } + else { + $description['memory'] = t('Contact your system administrator or hosting provider for assistance with increasing your PHP memory limit.'); + } + + $handbook_link = t('For more information, see the online handbook entry for <a href=":memory-limit">increasing the PHP memory limit</a>.', [':memory-limit' => 'https://www.drupal.org/node/207036']); + + $description = [ + '#type' => 'inline_template', + '#template' => '{{ description_phase }} {{ description_memory }} {{ handbook }}', + '#context' => [ + 'description_phase' => $description['phase'], + 'description_memory' => $description['memory'], + 'handbook' => $handbook_link, + ], + ]; + + $requirements['php_memory_limit']['description'] = $description; + $requirements['php_memory_limit']['severity'] = RequirementSeverity::Warning; + } + } + + // Test if configuration files and directory are writable. + if ($phase == 'runtime') { + $conf_errors = []; + // Find the site path. Kernel service is not always available at this + // point, but is preferred, when available. + if (\Drupal::hasService('kernel')) { + $site_path = \Drupal::getContainer()->getParameter('site.path'); + } + else { + $site_path = DrupalKernel::findSitePath(Request::createFromGlobals()); + } + // Allow system administrators to disable permissions hardening for the + // site directory. This allows additional files in the site directory to + // be updated when they are managed in a version control system. + if (Settings::get('skip_permissions_hardening')) { + $error_value = t('Protection disabled'); + // If permissions hardening is disabled, then only show a warning for a + // writable file, as a reminder, rather than an error. + $file_protection_severity = RequirementSeverity::Warning; + } + else { + $error_value = t('Not protected'); + // In normal operation, writable files or directories are an error. + $file_protection_severity = RequirementSeverity::Error; + if (!drupal_verify_install_file($site_path, FILE_NOT_WRITABLE, 'dir')) { + $conf_errors[] = t("The directory %file is not protected from modifications and poses a security risk. You must change the directory's permissions to be non-writable.", ['%file' => $site_path]); + } + } + foreach (['settings.php', 'settings.local.php', 'services.yml'] as $conf_file) { + $full_path = $site_path . '/' . $conf_file; + if (file_exists($full_path) && !drupal_verify_install_file($full_path, FILE_EXIST | FILE_READABLE | FILE_NOT_WRITABLE, 'file', !Settings::get('skip_permissions_hardening'))) { + $conf_errors[] = t("The file %file is not protected from modifications and poses a security risk. You must change the file's permissions to be non-writable.", ['%file' => $full_path]); + } + } + if (!empty($conf_errors)) { + if (count($conf_errors) == 1) { + $description = $conf_errors[0]; + } + else { + // We use twig inline_template to avoid double escaping. + $description = [ + '#type' => 'inline_template', + '#template' => '{{ configuration_error_list }}', + '#context' => [ + 'configuration_error_list' => [ + '#theme' => 'item_list', + '#items' => $conf_errors, + ], + ], + ]; + } + $requirements['configuration_files'] = [ + 'value' => $error_value, + 'severity' => $file_protection_severity, + 'description' => $description, + ]; + } + else { + $requirements['configuration_files'] = [ + 'value' => t('Protected'), + ]; + } + $requirements['configuration_files']['title'] = t('Configuration files'); + } + + // Test the contents of the .htaccess files. + if ($phase == 'runtime' && Settings::get('auto_create_htaccess', TRUE)) { + // Try to write the .htaccess files first, to prevent false alarms in + // case (for example) the /tmp directory was wiped. + /** @var \Drupal\Core\File\HtaccessWriterInterface $htaccessWriter */ + $htaccessWriter = \Drupal::service("file.htaccess_writer"); + $htaccessWriter->ensure(); + foreach ($htaccessWriter->defaultProtectedDirs() as $protected_dir) { + $htaccess_file = $protected_dir->getPath() . '/.htaccess'; + // Check for the string which was added to the recommended .htaccess + // file in the latest security update. + if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || !str_contains($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003')) { + $url = 'https://www.drupal.org/SA-CORE-2013-003'; + $requirements[$htaccess_file] = [ + // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString + 'title' => new TranslatableMarkup($protected_dir->getTitle()), + 'value' => t('Not fully protected'), + 'severity' => RequirementSeverity::Error, + 'description' => t('See <a href=":url">@url</a> for information about the recommended .htaccess file which should be added to the %directory directory to help protect against arbitrary code execution.', [':url' => $url, '@url' => $url, '%directory' => $protected_dir->getPath()]), + ]; + } + } + } + + // Report cron status. + if ($phase == 'runtime') { + $cron_config = \Drupal::config('system.cron'); + // Cron warning threshold defaults to two days. + $threshold_warning = $cron_config->get('threshold.requirements_warning'); + // Cron error threshold defaults to two weeks. + $threshold_error = $cron_config->get('threshold.requirements_error'); + + // Determine when cron last ran. + $cron_last = \Drupal::state()->get('system.cron_last'); + if (!is_numeric($cron_last)) { + $cron_last = \Drupal::state()->get('install_time', 0); + } + + // Determine severity based on time since cron last ran. + $severity = RequirementSeverity::Info; + $request_time = \Drupal::time()->getRequestTime(); + if ($request_time - $cron_last > $threshold_error) { + $severity = RequirementSeverity::Error; + } + elseif ($request_time - $cron_last > $threshold_warning) { + $severity = RequirementSeverity::Warning; + } + + // Set summary and description based on values determined above. + $summary = t('Last run @time ago', ['@time' => \Drupal::service('date.formatter')->formatTimeDiffSince($cron_last)]); + + $requirements['cron'] = [ + 'title' => t('Cron maintenance tasks'), + 'severity' => $severity, + 'value' => $summary, + ]; + if ($severity != RequirementSeverity::Info) { + $requirements['cron']['description'][] = [ + [ + '#markup' => t('Cron has not run recently.'), + '#suffix' => ' ', + ], + [ + '#markup' => t('For more information, see the online handbook entry for <a href=":cron-handbook">configuring cron jobs</a>.', [':cron-handbook' => 'https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview']), + '#suffix' => ' ', + ], + ]; + } + $requirements['cron']['description'][] = [ + [ + '#type' => 'link', + '#prefix' => '(', + '#title' => t('more information'), + '#suffix' => ')', + '#url' => Url::fromRoute('system.cron_settings'), + ], + [ + '#prefix' => '<span class="cron-description__run-cron">', + '#suffix' => '</span>', + '#type' => 'link', + '#title' => t('Run cron'), + '#url' => Url::fromRoute('system.run_cron'), + ], + ]; + } + if ($phase != 'install') { + $directories = [ + PublicStream::basePath(), + // By default no private files directory is configured. For private + // files to be secure the admin needs to provide a path outside the + // webroot. + PrivateStream::basePath(), + \Drupal::service('file_system')->getTempDirectory(), + ]; + } + + // During an install we need to make assumptions about the file system + // unless overrides are provided in settings.php. + if ($phase == 'install') { + $directories = []; + if ($file_public_path = Settings::get('file_public_path')) { + $directories[] = $file_public_path; + } + else { + // If we are installing Drupal, the settings.php file might not exist + // yet in the intended site directory, so don't require it. + $request = Request::createFromGlobals(); + $site_path = DrupalKernel::findSitePath($request); + $directories[] = $site_path . '/files'; + } + if ($file_private_path = Settings::get('file_private_path')) { + $directories[] = $file_private_path; + } + if (Settings::get('file_temp_path')) { + $directories[] = Settings::get('file_temp_path'); + } + else { + // If the temporary directory is not overridden use an appropriate + // temporary path for the system. + $directories[] = FileSystemComponent::getOsTemporaryDirectory(); + } + } + + // Check the config directory if it is defined in settings.php. If it isn't + // defined, the installer will create a valid config directory later, but + // during runtime we must always display an error. + $config_sync_directory = Settings::get('config_sync_directory'); + if (!empty($config_sync_directory)) { + // If we're installing Drupal try and create the config sync directory. + if (!is_dir($config_sync_directory) && $phase == 'install') { + \Drupal::service('file_system')->prepareDirectory($config_sync_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + } + if (!is_dir($config_sync_directory)) { + if ($phase == 'install') { + $description = t('An automated attempt to create the directory %directory failed, possibly due to a permissions problem. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see INSTALL.txt or the <a href=":handbook_url">online handbook</a>.', ['%directory' => $config_sync_directory, ':handbook_url' => 'https://www.drupal.org/server-permissions']); + } + else { + $description = t('The directory %directory does not exist.', ['%directory' => $config_sync_directory]); + } + $requirements['config sync directory'] = [ + 'title' => t('Configuration sync directory'), + 'description' => $description, + 'severity' => RequirementSeverity::Error, + ]; + } + } + if ($phase != 'install' && empty($config_sync_directory)) { + $requirements['config sync directory'] = [ + 'title' => t('Configuration sync directory'), + 'value' => t('Not present'), + 'description' => t("Your %file file must define the %setting setting as a string containing the directory in which configuration files can be found.", ['%file' => $site_path . '/settings.php', '%setting' => "\$settings['config_sync_directory']"]), + 'severity' => RequirementSeverity::Error, + ]; + } + + $requirements['file system'] = [ + 'title' => t('File system'), + ]; + + $error = ''; + // For installer, create the directories if possible. + foreach ($directories as $directory) { + if (!$directory) { + continue; + } + if ($phase == 'install') { + \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + } + $is_writable = is_writable($directory); + $is_directory = is_dir($directory); + if (!$is_writable || !$is_directory) { + $description = ''; + $requirements['file system']['value'] = t('Not writable'); + if (!$is_directory) { + $error = t('The directory %directory does not exist.', ['%directory' => $directory]); + } + else { + $error = t('The directory %directory is not writable.', ['%directory' => $directory]); + } + // The files directory requirement check is done only during install and + // runtime. + if ($phase == 'runtime') { + $description = t('You may need to set the correct directory at the <a href=":admin-file-system">file system settings page</a> or change the current directory\'s permissions so that it is writable.', [':admin-file-system' => Url::fromRoute('system.file_system_settings')->toString()]); + } + elseif ($phase == 'install') { + // For the installer UI, we need different wording. 'value' will + // be treated as version, so provide none there. + $description = t('An automated attempt to create this directory failed, possibly due to a permissions problem. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see INSTALL.txt or the <a href=":handbook_url">online handbook</a>.', [':handbook_url' => 'https://www.drupal.org/server-permissions']); + $requirements['file system']['value'] = ''; + } + if (!empty($description)) { + $description = [ + '#type' => 'inline_template', + '#template' => '{{ error }} {{ description }}', + '#context' => [ + 'error' => $error, + 'description' => $description, + ], + ]; + $requirements['file system']['description'] = $description; + $requirements['file system']['severity'] = RequirementSeverity::Error; + } + } + else { + // This function can be called before the config_cache table has been + // created. + if ($phase == 'install' || \Drupal::config('system.file')->get('default_scheme') == 'public') { + $requirements['file system']['value'] = t('Writable (<em>public</em> download method)'); + } + else { + $requirements['file system']['value'] = t('Writable (<em>private</em> download method)'); + } + } + } + + // See if updates are available in update.php. + if ($phase == 'runtime') { + $requirements['update'] = [ + 'title' => t('Database updates'), + 'value' => t('Up to date'), + ]; + + // Check installed modules. + $has_pending_updates = FALSE; + /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */ + $update_registry = \Drupal::service('update.update_hook_registry'); + foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) { + $updates = $update_registry->getAvailableUpdates($module); + if ($updates) { + $default = $update_registry->getInstalledVersion($module); + if (max($updates) > $default) { + $has_pending_updates = TRUE; + break; + } + } + } + if (!$has_pending_updates) { + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $missing_post_update_functions = $post_update_registry->getPendingUpdateFunctions(); + if (!empty($missing_post_update_functions)) { + $has_pending_updates = TRUE; + } + } + + if ($has_pending_updates) { + $requirements['update']['severity'] = RequirementSeverity::Error; + $requirements['update']['value'] = t('Out of date'); + $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href=":update">database update script</a> immediately.', [':update' => Url::fromRoute('system.db_update')->toString()]); + } + + $requirements['entity_update'] = [ + 'title' => t('Entity/field definitions'), + 'value' => t('Up to date'), + ]; + // Verify that no entity updates are pending. + if ($change_list = \Drupal::entityDefinitionUpdateManager()->getChangeSummary()) { + $build = []; + foreach ($change_list as $entity_type_id => $changes) { + $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); + $build[] = [ + '#theme' => 'item_list', + '#title' => $entity_type->getLabel(), + '#items' => $changes, + ]; + } + + $entity_update_issues = \Drupal::service('renderer')->renderInIsolation($build); + $requirements['entity_update']['severity'] = RequirementSeverity::Error; + $requirements['entity_update']['value'] = t('Mismatched entity and/or field definitions'); + $requirements['entity_update']['description'] = t('The following changes were detected in the entity type and field definitions. @updates', ['@updates' => $entity_update_issues]); + } + } + + // Display the deployment identifier if set. + if ($phase == 'runtime') { + if ($deployment_identifier = Settings::get('deployment_identifier')) { + $requirements['deployment identifier'] = [ + 'title' => t('Deployment identifier'), + 'value' => $deployment_identifier, + 'severity' => RequirementSeverity::Info, + ]; + } + } + + // Verify the update.php access setting + if ($phase == 'runtime') { + if (Settings::get('update_free_access')) { + $requirements['update access'] = [ + 'value' => t('Not protected'), + 'severity' => RequirementSeverity::Error, + 'description' => t('The update.php script is accessible to everyone without authentication check, which is a security risk. You must change the @settings_name value in your settings.php back to FALSE.', ['@settings_name' => '$settings[\'update_free_access\']']), + ]; + } + else { + $requirements['update access'] = [ + 'value' => t('Protected'), + ]; + } + $requirements['update access']['title'] = t('Access to update.php'); + } + + // Display an error if a newly introduced dependency in a module is not + // resolved. + if ($phase === 'update' || $phase === 'runtime') { + $create_extension_incompatibility_list = function (array $extension_names, PluralTranslatableMarkup $description, PluralTranslatableMarkup $title, TranslatableMarkup|string $message = '', TranslatableMarkup|string $additional_description = '') { + if ($message === '') { + $message = new TranslatableMarkup('Review the <a href=":url"> suggestions for resolving this incompatibility</a> to repair your installation, and then re-run update.php.', [':url' => 'https://www.drupal.org/docs/updating-drupal/troubleshooting-database-updates']); + } + // Use an inline twig template to: + // - Concatenate MarkupInterface objects and preserve safeness. + // - Use the item_list theme for the extension list. + $template = [ + '#type' => 'inline_template', + '#template' => '{{ description }}{{ extensions }}{{ additional_description }}<br>', + '#context' => [ + 'extensions' => [ + '#theme' => 'item_list', + ], + ], + ]; + $template['#context']['extensions']['#items'] = $extension_names; + $template['#context']['description'] = $description; + $template['#context']['additional_description'] = $additional_description; + return [ + 'title' => $title, + 'value' => [ + 'list' => $template, + 'handbook_link' => [ + '#markup' => $message, + ], + ], + 'severity' => RequirementSeverity::Error, + ]; + }; + $profile = \Drupal::installProfile(); + $files = $module_extension_list->getList(); + $files += $theme_extension_list->getList(); + $core_incompatible_extensions = []; + $php_incompatible_extensions = []; + foreach ($files as $extension_name => $file) { + // Ignore uninstalled extensions and installation profiles. + if (!$file->status || $extension_name == $profile) { + continue; + } + + $name = $file->info['name']; + if (!empty($file->info['core_incompatible'])) { + $core_incompatible_extensions[$file->info['type']][] = $name; + } + + // Check the extension's PHP version. + $php = (string) $file->info['php']; + if (version_compare($php, PHP_VERSION, '>')) { + $php_incompatible_extensions[$file->info['type']][] = $name; + } + + // Check the module's required modules. + /** @var \Drupal\Core\Extension\Dependency $requirement */ + foreach ($file->requires as $requirement) { + $required_module = $requirement->getName(); + // Check if the module exists. + if (!isset($files[$required_module])) { + $requirements["$extension_name-$required_module"] = [ + 'title' => t('Unresolved dependency'), + 'description' => t('@name requires this module.', ['@name' => $name]), + 'value' => t('@required_name (Missing)', ['@required_name' => $required_module]), + 'severity' => RequirementSeverity::Error, + ]; + continue; + } + // Check for an incompatible version. + $required_file = $files[$required_module]; + $required_name = $required_file->info['name']; + // Remove CORE_COMPATIBILITY- only from the start of the string. + $version = preg_replace('/^(' . \Drupal::CORE_COMPATIBILITY . '\-)/', '', $required_file->info['version'] ?? ''); + if (!$requirement->isCompatible($version)) { + $requirements["$extension_name-$required_module"] = [ + 'title' => t('Unresolved dependency'), + 'description' => t('@name requires this module and version. Currently using @required_name version @version', ['@name' => $name, '@required_name' => $required_name, '@version' => $version]), + 'value' => t('@required_name (Version @compatibility required)', ['@required_name' => $required_name, '@compatibility' => $requirement->getConstraintString()]), + 'severity' => RequirementSeverity::Error, + ]; + continue; + } + } + } + if (!empty($core_incompatible_extensions['module'])) { + $requirements['module_core_incompatible'] = $create_extension_incompatibility_list( + $core_incompatible_extensions['module'], + new PluralTranslatableMarkup( + count($core_incompatible_extensions['module']), + 'The following module is installed, but it is incompatible with Drupal @version:', + 'The following modules are installed, but they are incompatible with Drupal @version:', + ['@version' => \Drupal::VERSION] + ), + new PluralTranslatableMarkup( + count($core_incompatible_extensions['module']), + 'Incompatible module', + 'Incompatible modules' + ) + ); + } + if (!empty($core_incompatible_extensions['theme'])) { + $requirements['theme_core_incompatible'] = $create_extension_incompatibility_list( + $core_incompatible_extensions['theme'], + new PluralTranslatableMarkup( + count($core_incompatible_extensions['theme']), + 'The following theme is installed, but it is incompatible with Drupal @version:', + 'The following themes are installed, but they are incompatible with Drupal @version:', + ['@version' => \Drupal::VERSION] + ), + new PluralTranslatableMarkup( + count($core_incompatible_extensions['theme']), + 'Incompatible theme', + 'Incompatible themes' + ) + ); + } + if (!empty($php_incompatible_extensions['module'])) { + $requirements['module_php_incompatible'] = $create_extension_incompatibility_list( + $php_incompatible_extensions['module'], + new PluralTranslatableMarkup( + count($php_incompatible_extensions['module']), + 'The following module is installed, but it is incompatible with PHP @version:', + 'The following modules are installed, but they are incompatible with PHP @version:', + ['@version' => phpversion()] + ), + new PluralTranslatableMarkup( + count($php_incompatible_extensions['module']), + 'Incompatible module', + 'Incompatible modules' + ) + ); + } + if (!empty($php_incompatible_extensions['theme'])) { + $requirements['theme_php_incompatible'] = $create_extension_incompatibility_list( + $php_incompatible_extensions['theme'], + new PluralTranslatableMarkup( + count($php_incompatible_extensions['theme']), + 'The following theme is installed, but it is incompatible with PHP @version:', + 'The following themes are installed, but they are incompatible with PHP @version:', + ['@version' => phpversion()] + ), + new PluralTranslatableMarkup( + count($php_incompatible_extensions['theme']), + 'Incompatible theme', + 'Incompatible themes' + ) + ); + } + + $extension_config = \Drupal::configFactory()->get('core.extension'); + + // Look for removed core modules. + $is_removed_module = function ($extension_name) use ($module_extension_list) { + return !$module_extension_list->exists($extension_name) + && array_key_exists($extension_name, DRUPAL_CORE_REMOVED_MODULE_LIST); + }; + $removed_modules = array_filter(array_keys($extension_config->get('module')), $is_removed_module); + if (!empty($removed_modules)) { + $list = []; + foreach ($removed_modules as $removed_module) { + $list[] = t('<a href=":url">@module</a>', [ + ':url' => "https://www.drupal.org/project/$removed_module", + '@module' => DRUPAL_CORE_REMOVED_MODULE_LIST[$removed_module], + ]); + } + $requirements['removed_module'] = $create_extension_incompatibility_list( + $list, + new PluralTranslatableMarkup( + count($removed_modules), + 'You must add the following contributed module and reload this page.', + 'You must add the following contributed modules and reload this page.' + ), + new PluralTranslatableMarkup( + count($removed_modules), + 'Removed core module', + 'Removed core modules' + ), + new TranslatableMarkup( + 'For more information read the <a href=":url">documentation on deprecated modules.</a>', + [':url' => 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-modules'] + ), + new PluralTranslatableMarkup( + count($removed_modules), + 'This module is installed on your site but is no longer provided by Core.', + 'These modules are installed on your site but are no longer provided by Core.' + ), + ); + } + + // Look for removed core themes. + $is_removed_theme = function ($extension_name) use ($theme_extension_list) { + return !$theme_extension_list->exists($extension_name) + && array_key_exists($extension_name, DRUPAL_CORE_REMOVED_THEME_LIST); + }; + $removed_themes = array_filter(array_keys($extension_config->get('theme')), $is_removed_theme); + if (!empty($removed_themes)) { + $list = []; + foreach ($removed_themes as $removed_theme) { + $list[] = t('<a href=":url">@theme</a>', [ + ':url' => "https://www.drupal.org/project/$removed_theme", + '@theme' => DRUPAL_CORE_REMOVED_THEME_LIST[$removed_theme], + ]); + } + $requirements['removed_theme'] = $create_extension_incompatibility_list( + $list, + new PluralTranslatableMarkup( + count($removed_themes), + 'You must add the following contributed theme and reload this page.', + 'You must add the following contributed themes and reload this page.' + ), + new PluralTranslatableMarkup( + count($removed_themes), + 'Removed core theme', + 'Removed core themes' + ), + new TranslatableMarkup( + 'For more information read the <a href=":url">documentation on deprecated themes.</a>', + [':url' => 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-themes'] + ), + new PluralTranslatableMarkup( + count($removed_themes), + 'This theme is installed on your site but is no longer provided by Core.', + 'These themes are installed on your site but are no longer provided by Core.' + ), + ); + } + + // Look for missing modules. + $is_missing_module = function ($extension_name) use ($module_extension_list) { + return !$module_extension_list->exists($extension_name) && !in_array($extension_name, array_keys(DRUPAL_CORE_REMOVED_MODULE_LIST), TRUE); + }; + $invalid_modules = array_filter(array_keys($extension_config->get('module')), $is_missing_module); + + if (!empty($invalid_modules)) { + $requirements['invalid_module'] = $create_extension_incompatibility_list( + $invalid_modules, + new PluralTranslatableMarkup( + count($invalid_modules), + 'The following module is marked as installed in the core.extension configuration, but it is missing:', + 'The following modules are marked as installed in the core.extension configuration, but they are missing:' + ), + new PluralTranslatableMarkup( + count($invalid_modules), + 'Missing or invalid module', + 'Missing or invalid modules' + ) + ); + } + + // Look for invalid themes. + $is_missing_theme = function ($extension_name) use (&$theme_extension_list) { + return !$theme_extension_list->exists($extension_name) && !in_array($extension_name, array_keys(DRUPAL_CORE_REMOVED_THEME_LIST), TRUE); + }; + $invalid_themes = array_filter(array_keys($extension_config->get('theme')), $is_missing_theme); + if (!empty($invalid_themes)) { + $requirements['invalid_theme'] = $create_extension_incompatibility_list( + $invalid_themes, + new PluralTranslatableMarkup( + count($invalid_themes), + 'The following theme is marked as installed in the core.extension configuration, but it is missing:', + 'The following themes are marked as installed in the core.extension configuration, but they are missing:' + ), + new PluralTranslatableMarkup( + count($invalid_themes), + 'Missing or invalid theme', + 'Missing or invalid themes' + ) + ); + } + } + + // Returns Unicode library status and errors. + $libraries = [ + Unicode::STATUS_SINGLEBYTE => t('Standard PHP'), + Unicode::STATUS_MULTIBYTE => t('PHP Mbstring Extension'), + Unicode::STATUS_ERROR => t('Error'), + ]; + $severities = [ + Unicode::STATUS_SINGLEBYTE => RequirementSeverity::Warning, + Unicode::STATUS_MULTIBYTE => NULL, + Unicode::STATUS_ERROR => RequirementSeverity::Error, + ]; + $failed_check = Unicode::check(); + $library = Unicode::getStatus(); + + $requirements['unicode'] = [ + 'title' => t('Unicode library'), + 'value' => $libraries[$library], + 'severity' => $severities[$library], + ]; + switch ($failed_check) { + case 'mb_strlen': + $requirements['unicode']['description'] = t('Operations on Unicode strings are emulated on a best-effort basis. Install the <a href="http://php.net/mbstring">PHP mbstring extension</a> for improved Unicode support.'); + break; + + case 'mbstring.encoding_translation': + $requirements['unicode']['description'] = t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini <em>mbstring.encoding_translation</em> setting. Refer to the <a href="http://php.net/mbstring">PHP mbstring documentation</a> for more information.'); + break; + } + + if ($phase == 'runtime') { + // Check for update status module. + if (!\Drupal::moduleHandler()->moduleExists('update')) { + $requirements['update status'] = [ + 'value' => t('Not enabled'), + 'severity' => RequirementSeverity::Warning, + 'description' => t('Update notifications are not enabled. It is <strong>highly recommended</strong> that you install the Update Status module from the <a href=":module">module administration page</a> in order to stay up-to-date on new releases. For more information, <a href=":update">Update status handbook page</a>.', [ + ':update' => 'https://www.drupal.org/documentation/modules/update', + ':module' => Url::fromRoute('system.modules_list')->toString(), + ]), + ]; + } + else { + $requirements['update status'] = [ + 'value' => t('Enabled'), + ]; + } + $requirements['update status']['title'] = t('Update notifications'); + + if (Settings::get('rebuild_access')) { + $requirements['rebuild access'] = [ + 'title' => t('Rebuild access'), + 'value' => t('Enabled'), + 'severity' => RequirementSeverity::Error, + 'description' => t('The rebuild_access setting is enabled in settings.php. It is recommended to have this setting disabled unless you are performing a rebuild.'), + ]; + } + } + + // Check if the SameSite cookie attribute is set to a valid value. Since + // this involves checking whether we are using a secure connection this + // only makes sense inside an HTTP request, not on the command line. + if ($phase === 'runtime' && PHP_SAPI !== 'cli') { + $samesite = ini_get('session.cookie_samesite') ?: t('Not set'); + // Check if the SameSite attribute is set to a valid value. If it is set + // to 'None' the request needs to be done over HTTPS. + $valid = match ($samesite) { + 'Lax', 'Strict' => TRUE, + 'None' => $request_object->isSecure(), + default => FALSE, + }; + $requirements['php_session_samesite'] = [ + 'title' => t('SameSite cookie attribute'), + 'value' => $samesite, + 'severity' => $valid ? RequirementSeverity::OK : RequirementSeverity::Warning, + 'description' => t('This attribute should be explicitly set to Lax, Strict or None. If set to None then the request must be made via HTTPS. See <a href=":url" target="_blank">PHP documentation</a>', [ + ':url' => 'https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite', + ]), + ]; + } + + // See if trusted host names have been configured, and warn the user if they + // are not set. + if ($phase == 'runtime') { + $trusted_host_patterns = Settings::get('trusted_host_patterns'); + if (empty($trusted_host_patterns)) { + $requirements['trusted_host_patterns'] = [ + 'title' => t('Trusted Host Settings'), + 'value' => t('Not enabled'), + 'description' => t('The trusted_host_patterns setting is not configured in settings.php. This can lead to security vulnerabilities. It is <strong>highly recommended</strong> that you configure this. See <a href=":url">Protecting against HTTP HOST Header attacks</a> for more information.', [':url' => 'https://www.drupal.org/docs/installing-drupal/trusted-host-settings']), + 'severity' => RequirementSeverity::Error, + ]; + } + else { + $requirements['trusted_host_patterns'] = [ + 'title' => t('Trusted Host Settings'), + 'value' => t('Enabled'), + 'description' => t('The trusted_host_patterns setting is set to allow %trusted_host_patterns', ['%trusted_host_patterns' => implode(', ', $trusted_host_patterns)]), + ]; + } + } + + // When the database driver is provided by a module, then check that the + // providing module is installed. + if ($phase === 'runtime' || $phase === 'update') { + $connection = Database::getConnection(); + $provider = $connection->getProvider(); + if ($provider !== 'core' && !\Drupal::moduleHandler()->moduleExists($provider)) { + $autoload = $connection->getConnectionOptions()['autoload'] ?? ''; + if (str_contains($autoload, 'src/Driver/Database/')) { + $post_update_registry = \Drupal::service('update.post_update_registry'); + $pending_updates = $post_update_registry->getPendingUpdateInformation(); + if (!in_array('enable_provider_database_driver', array_keys($pending_updates['system']['pending'] ?? []), TRUE)) { + // Only show the warning when the post update function has run and + // the module that is providing the database driver is not + // installed. + $requirements['database_driver_provided_by_module'] = [ + 'title' => t('Database driver provided by module'), + 'value' => t('Not installed'), + 'description' => t('The current database driver is provided by the module: %module. The module is currently not installed. You should immediately <a href=":install">install</a> the module.', ['%module' => $provider, ':install' => Url::fromRoute('system.modules_list')->toString()]), + 'severity' => RequirementSeverity::Error, + ]; + } + } + } + } + + // Check xdebug.max_nesting_level, as some pages will not work if it is too + // low. + if (extension_loaded('xdebug')) { + // Setting this value to 256 was considered adequate on Xdebug 2.3 + // (see http://bugs.xdebug.org/bug_view_page.php?bug_id=00001100) + $minimum_nesting_level = 256; + $current_nesting_level = ini_get('xdebug.max_nesting_level'); + + if ($current_nesting_level < $minimum_nesting_level) { + $requirements['xdebug_max_nesting_level'] = [ + 'title' => t('Xdebug settings'), + 'value' => t('xdebug.max_nesting_level is set to %value.', ['%value' => $current_nesting_level]), + 'description' => t('Set <code>xdebug.max_nesting_level=@level</code> in your PHP configuration as some pages in your Drupal site will not work when this setting is too low.', ['@level' => $minimum_nesting_level]), + 'severity' => RequirementSeverity::Error, + ]; + } + } + + // Installations on Windows can run into limitations with MAX_PATH if the + // Drupal root directory is too deep in the filesystem. Generally this + // shows up in cached Twig templates and other public files with long + // directory or file names. There is no definite root directory depth below + // which Drupal is guaranteed to function correctly on Windows. Since + // problems are likely with more than 100 characters in the Drupal root + // path, show an error. + if (str_starts_with(PHP_OS, 'WIN')) { + $depth = strlen(realpath(DRUPAL_ROOT . '/' . PublicStream::basePath())); + if ($depth > 120) { + $requirements['max_path_on_windows'] = [ + 'title' => t('Windows installation depth'), + 'description' => t('The public files directory path is %depth characters. Paths longer than 120 characters will cause problems on Windows.', ['%depth' => $depth]), + 'severity' => RequirementSeverity::Error, + ]; + } + } + // Check to see if dates will be limited to 1901-2038. + if (PHP_INT_SIZE <= 4) { + $requirements['limited_date_range'] = [ + 'title' => t('Limited date range'), + 'value' => t('Your PHP installation has a limited date range.'), + 'description' => t('You are running on a system where PHP is compiled or limited to using 32-bit integers. This will limit the range of dates and timestamps to the years 1901-2038. Read about the <a href=":url">limitations of 32-bit PHP</a>.', [':url' => 'https://www.drupal.org/docs/system-requirements/limitations-of-32-bit-php']), + 'severity' => RequirementSeverity::Warning, + ]; + } + + // During installs from configuration don't support install profiles that + // implement hook_install. + if ($phase == 'install' && !empty($install_state['config_install_path'])) { + $install_hook = $install_state['parameters']['profile'] . '_install'; + if (function_exists($install_hook)) { + $requirements['config_install'] = [ + 'title' => t('Configuration install'), + 'value' => $install_state['parameters']['profile'], + 'description' => t('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.'), + 'severity' => RequirementSeverity::Error, + ]; + } + } + + if ($phase === 'runtime') { + $settings = Settings::getAll(); + if (array_key_exists('install_profile', $settings)) { + // The following message is only informational because not all site + // owners have access to edit their settings.php as it may be + // controlled by their hosting provider. + $requirements['install_profile_in_settings'] = [ + 'title' => t('Install profile in settings'), + 'value' => t("Drupal 9 no longer uses the \$settings['install_profile'] value in settings.php and it should be removed."), + 'severity' => RequirementSeverity::Warning, + ]; + } + } + + // Ensure that no module has a current schema version that is lower than the + // one that was last removed. + if ($phase == 'update') { + $module_handler = \Drupal::moduleHandler(); + /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */ + $update_registry = \Drupal::service('update.update_hook_registry'); + $module_list = []; + // hook_update_last_removed() is a procedural hook hook because we + // do not have classes loaded that would be needed. + // Simply inlining the old hook mechanism is better than making + // ModuleInstaller::invoke() public. + foreach ($module_handler->getModuleList() as $module => $extension) { + $function = $module . '_update_last_removed'; + if (function_exists($function)) { + $last_removed = $function(); + if ($last_removed && $last_removed > $update_registry->getInstalledVersion($module)) { + + /** @var \Drupal\Core\Extension\Extension $module_info */ + $module_info = $module_extension_list->get($module); + $module_list[$module] = [ + 'name' => $module_info->info['name'], + 'last_removed' => $last_removed, + 'installed_version' => $update_registry->getInstalledVersion($module), + ]; + } + } + } + + // If user module is in the list then only show a specific message for + // Drupal core. + if (isset($module_list['user'])) { + $requirements['user_update_last_removed'] = [ + 'title' => t('The version of Drupal you are trying to update from is too old'), + 'description' => t('Updating to Drupal @current_major is only supported from Drupal version @required_min_version or higher. If you are trying to update from an older version, first update to the latest version of Drupal @previous_major. (<a href=":url">Drupal upgrade guide</a>)', [ + '@current_major' => 10, + '@required_min_version' => '9.4.0', + '@previous_major' => 9, + ':url' => 'https://www.drupal.org/docs/upgrading-drupal/drupal-8-and-higher', + ]), + 'severity' => RequirementSeverity::Error, + ]; + } + else { + foreach ($module_list as $module => $data) { + $requirements[$module . '_update_last_removed'] = [ + 'title' => t('Unsupported schema version: @module', ['@module' => $data['name']]), + 'description' => t('The installed version of the %module module is too old to update. Update to an intermediate version first (last removed version: @last_removed_version, installed version: @installed_version).', [ + '%module' => $data['name'], + '@last_removed_version' => $data['last_removed'], + '@installed_version' => $data['installed_version'], + ]), + 'severity' => RequirementSeverity::Error, + ]; + } + } + // Also check post-updates. Only do this if we're not already showing an + // error for hook_update_N(). + $missing_updates = []; + if (empty($module_list)) { + $existing_updates = \Drupal::service('keyvalue')->get('post_update')->get('existing_updates', []); + $post_update_registry = \Drupal::service('update.post_update_registry'); + $modules = \Drupal::moduleHandler()->getModuleList(); + foreach ($modules as $module => $extension) { + $module_info = $module_extension_list->get($module); + $removed_post_updates = $post_update_registry->getRemovedPostUpdates($module); + if ($missing_updates = array_diff(array_keys($removed_post_updates), $existing_updates)) { + $versions = array_unique(array_intersect_key($removed_post_updates, array_flip($missing_updates))); + $description = new PluralTranslatableMarkup(count($versions), + 'The installed version of the %module module is too old to update. Update to a version prior to @versions first (missing updates: @missing_updates).', + 'The installed version of the %module module is too old to update. Update first to a version prior to all of the following: @versions (missing updates: @missing_updates).', + [ + '%module' => $module_info->info['name'], + '@missing_updates' => implode(', ', $missing_updates), + '@versions' => implode(', ', $versions), + ] + ); + $requirements[$module . '_post_update_removed'] = [ + 'title' => t('Missing updates for: @module', ['@module' => $module_info->info['name']]), + 'description' => $description, + 'severity' => RequirementSeverity::Error, + ]; + } + } + } + + if (empty($missing_updates)) { + foreach ($update_registry->getAllEquivalentUpdates() as $module => $equivalent_updates) { + $module_info = $module_extension_list->get($module); + foreach ($equivalent_updates as $future_update => $data) { + $future_update_function_name = $module . '_update_' . $future_update; + $ran_update_function_name = $module . '_update_' . $data['ran_update']; + // If an update was marked as an equivalent by a previous update, + // and both the previous update and the equivalent update are not + // found in the current code base, prevent updating. This indicates + // a site attempting to go 'backwards' in terms of database schema. + // @see \Drupal\Core\Update\UpdateHookRegistry::markFutureUpdateEquivalent() + if (!function_exists($ran_update_function_name) && !function_exists($future_update_function_name)) { + // If the module is provided by core prepend helpful text as the + // module does not exist in composer or Drupal.org. + if (str_starts_with($module_info->getPathname(), 'core/')) { + $future_version_string = 'Drupal Core ' . $data['future_version_string']; + } + else { + $future_version_string = $data['future_version_string']; + } + $requirements[$module . '_equivalent_update_missing'] = [ + 'title' => t('Missing updates for: @module', ['@module' => $module_info->info['name']]), + 'description' => t('The version of the %module module that you are attempting to update to is missing update @future_update (which was marked as an equivalent by @ran_update). Update to at least @future_version_string.', [ + '%module' => $module_info->info['name'], + '@ran_update' => $data['ran_update'], + '@future_update' => $future_update, + '@future_version_string' => $future_version_string, + ]), + 'severity' => RequirementSeverity::Error, + ]; + break; + } + } + } + } + } + + // Add warning when twig debug option is enabled. + if ($phase === 'runtime') { + $development_settings = \Drupal::keyValue('development_settings'); + $twig_debug = $development_settings->get('twig_debug', FALSE); + $twig_cache_disable = $development_settings->get('twig_cache_disable', FALSE); + if ($twig_debug || $twig_cache_disable) { + $requirements['twig_debug_enabled'] = [ + 'title' => t('Twig development mode'), + 'value' => t('Twig development mode settings are turned on. Go to @link to disable them.', [ + '@link' => Link::createFromRoute( + 'development settings page', + 'system.development_settings', + )->toString(), + ]), + 'severity' => RequirementSeverity::Warning, + ]; + } + $render_cache_disabled = $development_settings->get('disable_rendered_output_cache_bins', FALSE); + if ($render_cache_disabled) { + $requirements['render_cache_disabled'] = [ + 'title' => t('Markup caching disabled'), + 'value' => t('Render cache, dynamic page cache, and page cache are bypassed. Go to @link to enable them.', [ + '@link' => Link::createFromRoute( + 'development settings page', + 'system.development_settings', + )->toString(), + ]), + 'severity' => RequirementSeverity::Warning, + ]; + } + } + + return $requirements; + } + + /** + * Display requirements from security advisories. + * + * @param array[] $requirements + * The requirements array as specified in hook_requirements(). + */ + public static function systemAdvisoriesRequirements(array &$requirements): void { + if (!\Drupal::config('system.advisories')->get('enabled')) { + return; + } + + /** @var \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher $fetcher */ + $fetcher = \Drupal::service('system.sa_fetcher'); + try { + $advisories = $fetcher->getSecurityAdvisories(TRUE, 5); + } + catch (ClientExceptionInterface $exception) { + $requirements['system_advisories']['title'] = t('Critical security announcements'); + $requirements['system_advisories']['severity'] = RequirementSeverity::Warning; + $requirements['system_advisories']['description'] = ['#theme' => 'system_security_advisories_fetch_error_message']; + Error::logException(\Drupal::logger('system'), $exception, 'Failed to retrieve security advisory data.'); + return; + } + + if (!empty($advisories)) { + $advisory_links = []; + $severity = RequirementSeverity::Warning; + foreach ($advisories as $advisory) { + if (!$advisory->isPsa()) { + $severity = RequirementSeverity::Error; + } + $advisory_links[] = new Link($advisory->getTitle(), Url::fromUri($advisory->getUrl())); + } + $requirements['system_advisories']['title'] = t('Critical security announcements'); + $requirements['system_advisories']['severity'] = $severity; + $requirements['system_advisories']['description'] = [ + 'list' => [ + '#theme' => 'item_list', + '#items' => $advisory_links, + ], + ]; + if (\Drupal::moduleHandler()->moduleExists('help')) { + $requirements['system_advisories']['description']['help_link'] = Link::createFromRoute( + 'What are critical security announcements?', + 'help.page', ['name' => 'system'], + ['fragment' => 'security-advisories'] + )->toRenderable(); + } + } + } + +} diff --git a/core/modules/system/src/SystemManager.php b/core/modules/system/src/SystemManager.php index 43a53fe0542..563188e017f 100644 --- a/core/modules/system/src/SystemManager.php +++ b/core/modules/system/src/SystemManager.php @@ -120,7 +120,7 @@ class SystemManager { * An array of system requirements. */ public function listRequirements() { - // Load .install files + // Load .install files. include_once DRUPAL_ROOT . '/core/includes/install.inc'; drupal_load_updates(); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 431651d08e2..5bbaa8a5436 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -5,31 +5,9 @@ * Install, update and uninstall functions for the system module. */ -use Drupal\Component\FileSystem\FileSystem as FileSystemComponent; -use Drupal\Component\Utility\Bytes; use Drupal\Component\Utility\Crypt; -use Drupal\Component\Utility\Environment; -use Drupal\Component\Utility\OpCodeCache; -use Drupal\Component\Utility\Unicode; -use Drupal\Core\Database\Database; -use Drupal\Core\DrupalKernel; -use Drupal\Core\Extension\ExtensionLifecycle; -use Drupal\Core\Extension\Requirement\RequirementSeverity; -use Drupal\Core\File\FileSystemInterface; -use Drupal\Core\Link; -use Drupal\Core\Render\Markup; -use Drupal\Core\Site\Settings; -use Drupal\Core\StreamWrapper\PrivateStream; -use Drupal\Core\StreamWrapper\PublicStream; -use Drupal\Core\StringTranslation\ByteSizeMarkup; -use Drupal\Core\StringTranslation\PluralTranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Update\EquivalentUpdate; -use Drupal\Core\Url; -use Drupal\Core\Utility\Error; -use Drupal\Core\Utility\PhpRequirements; -use Psr\Http\Client\ClientExceptionInterface; -use Symfony\Component\HttpFoundation\Request; // cspell:ignore quickedit @@ -62,1565 +40,6 @@ const DRUPAL_CORE_REMOVED_THEME_LIST = [ ]; /** - * Implements hook_requirements(). - */ -function system_requirements($phase): array { - global $install_state; - - // Get the current default PHP requirements for this version of Drupal. - $minimum_supported_php = PhpRequirements::getMinimumSupportedPhp(); - - // Reset the extension lists. - /** @var \Drupal\Core\Extension\ModuleExtensionList $module_extension_list */ - $module_extension_list = \Drupal::service('extension.list.module'); - $module_extension_list->reset(); - /** @var \Drupal\Core\Extension\ThemeExtensionList $theme_extension_list */ - $theme_extension_list = \Drupal::service('extension.list.theme'); - $theme_extension_list->reset(); - $requirements = []; - - // Report Drupal version - if ($phase == 'runtime') { - $requirements['drupal'] = [ - 'title' => t('Drupal'), - 'value' => \Drupal::VERSION, - 'severity' => RequirementSeverity::Info, - 'weight' => -10, - ]; - - // Display the currently active installation profile, if the site - // is not running the default installation profile. - $profile = \Drupal::installProfile(); - if ($profile != 'standard' && !empty($profile)) { - $info = $module_extension_list->getExtensionInfo($profile); - $requirements['install_profile'] = [ - 'title' => t('Installation profile'), - 'value' => t('%profile_name (%profile%version)', [ - '%profile_name' => $info['name'], - '%profile' => $profile, - '%version' => !empty($info['version']) ? '-' . $info['version'] : '', - ]), - 'severity' => RequirementSeverity::Info, - 'weight' => -9, - ]; - } - - // Gather all obsolete and experimental modules being enabled. - $obsolete_extensions = []; - $deprecated_modules = []; - $experimental_modules = []; - $enabled_modules = \Drupal::moduleHandler()->getModuleList(); - foreach ($enabled_modules as $module => $data) { - $info = $module_extension_list->getExtensionInfo($module); - if (isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER])) { - if ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) { - $experimental_modules[$module] = $info['name']; - } - elseif ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) { - $deprecated_modules[] = ['name' => $info['name'], 'lifecycle_link' => $info['lifecycle_link']]; - } - elseif ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::OBSOLETE) { - $obsolete_extensions[$module] = ['name' => $info['name'], 'lifecycle_link' => $info['lifecycle_link']]; - } - } - } - - // Warn if any experimental modules are installed. - if (!empty($experimental_modules)) { - $requirements['experimental_modules'] = [ - 'title' => t('Experimental modules installed'), - 'value' => t('Experimental modules found: %module_list. <a href=":url">Experimental modules</a> are provided for testing purposes only. Use at your own risk.', ['%module_list' => implode(', ', $experimental_modules), ':url' => 'https://www.drupal.org/core/experimental']), - 'severity' => RequirementSeverity::Warning, - ]; - } - // Warn if any deprecated modules are installed. - if (!empty($deprecated_modules)) { - foreach ($deprecated_modules as $deprecated_module) { - $deprecated_modules_link_list[] = (string) Link::fromTextAndUrl($deprecated_module['name'], Url::fromUri($deprecated_module['lifecycle_link']))->toString(); - } - $requirements['deprecated_modules'] = [ - 'title' => t('Deprecated modules installed'), - 'value' => t('Deprecated modules found: %module_list.', [ - '%module_list' => Markup::create(implode(', ', $deprecated_modules_link_list)), - ]), - 'severity' => RequirementSeverity::Warning, - ]; - } - - // Gather all obsolete and experimental themes being installed. - $experimental_themes = []; - $deprecated_themes = []; - $installed_themes = \Drupal::service('theme_handler')->listInfo(); - foreach ($installed_themes as $theme => $data) { - $info = $theme_extension_list->getExtensionInfo($theme); - if (isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER])) { - if ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) { - $experimental_themes[$theme] = $info['name']; - } - elseif ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) { - $deprecated_themes[] = ['name' => $info['name'], 'lifecycle_link' => $info['lifecycle_link']]; - } - elseif ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::OBSOLETE) { - $obsolete_extensions[$theme] = ['name' => $info['name'], 'lifecycle_link' => $info['lifecycle_link']]; - } - } - } - - // Warn if any experimental themes are installed. - if (!empty($experimental_themes)) { - $requirements['experimental_themes'] = [ - 'title' => t('Experimental themes installed'), - 'value' => t('Experimental themes found: %theme_list. Experimental themes are provided for testing purposes only. Use at your own risk.', ['%theme_list' => implode(', ', $experimental_themes)]), - 'severity' => RequirementSeverity::Warning, - ]; - } - - // Warn if any deprecated themes are installed. - if (!empty($deprecated_themes)) { - foreach ($deprecated_themes as $deprecated_theme) { - $deprecated_themes_link_list[] = (string) Link::fromTextAndUrl($deprecated_theme['name'], Url::fromUri($deprecated_theme['lifecycle_link']))->toString(); - - } - $requirements['deprecated_themes'] = [ - 'title' => t('Deprecated themes installed'), - 'value' => t('Deprecated themes found: %theme_list.', [ - '%theme_list' => Markup::create(implode(', ', $deprecated_themes_link_list)), - ]), - 'severity' => RequirementSeverity::Warning, - ]; - } - - // Warn if any obsolete extensions (themes or modules) are installed. - if (!empty($obsolete_extensions)) { - foreach ($obsolete_extensions as $obsolete_extension) { - $obsolete_extensions_link_list[] = (string) Link::fromTextAndUrl($obsolete_extension['name'], Url::fromUri($obsolete_extension['lifecycle_link']))->toString(); - } - $requirements['obsolete_extensions'] = [ - 'title' => t('Obsolete extensions installed'), - 'value' => t('Obsolete extensions found: %extensions. Obsolete extensions are provided only so that they can be uninstalled cleanly. You should immediately <a href=":uninstall_url">uninstall these extensions</a> since they may be removed in a future release.', [ - '%extensions' => Markup::create(implode(', ', $obsolete_extensions_link_list)), - ':uninstall_url' => Url::fromRoute('system.modules_uninstall')->toString(), - ]), - 'severity' => RequirementSeverity::Warning, - ]; - } - _system_advisories_requirements($requirements); - } - - // Web server information. - $request_object = \Drupal::request(); - $software = $request_object->server->get('SERVER_SOFTWARE'); - $requirements['webserver'] = [ - 'title' => t('Web server'), - 'value' => $software, - ]; - - // Tests clean URL support. - if ($phase == 'install' && $install_state['interactive'] && !$request_object->query->has('rewrite') && str_contains($software, 'Apache')) { - // If the Apache rewrite module is not enabled, Apache version must be >= - // 2.2.16 because of the FallbackResource directive in the root .htaccess - // file. Since the Apache version reported by the server is dependent on the - // ServerTokens setting in httpd.conf, we may not be able to determine if a - // given config is valid. Thus we are unable to use version_compare() as we - // need have three possible outcomes: the version of Apache is greater than - // 2.2.16, is less than 2.2.16, or cannot be determined accurately. In the - // first case, we encourage the use of mod_rewrite; in the second case, we - // raise an error regarding the minimum Apache version; in the third case, - // we raise a warning that the current version of Apache may not be - // supported. - $rewrite_warning = FALSE; - $rewrite_error = FALSE; - $apache_version_string = 'Apache'; - - // Determine the Apache version number: major, minor and revision. - if (preg_match('/Apache\/(\d+)\.?(\d+)?\.?(\d+)?/', $software, $matches)) { - $apache_version_string = $matches[0]; - - // Major version number - if ($matches[1] < 2) { - $rewrite_error = TRUE; - } - elseif ($matches[1] == 2) { - if (!isset($matches[2])) { - $rewrite_warning = TRUE; - } - elseif ($matches[2] < 2) { - $rewrite_error = TRUE; - } - elseif ($matches[2] == 2) { - if (!isset($matches[3])) { - $rewrite_warning = TRUE; - } - elseif ($matches[3] < 16) { - $rewrite_error = TRUE; - } - } - } - } - else { - $rewrite_warning = TRUE; - } - - if ($rewrite_warning) { - $requirements['apache_version'] = [ - 'title' => t('Apache version'), - 'value' => $apache_version_string, - 'severity' => RequirementSeverity::Warning, - 'description' => t('Due to the settings for ServerTokens in httpd.conf, it is impossible to accurately determine the version of Apache running on this server. The reported value is @reported, to run Drupal without mod_rewrite, a minimum version of 2.2.16 is needed.', ['@reported' => $apache_version_string]), - ]; - } - - if ($rewrite_error) { - $requirements['Apache version'] = [ - 'title' => t('Apache version'), - 'value' => $apache_version_string, - 'severity' => RequirementSeverity::Error, - 'description' => t('The minimum version of Apache needed to run Drupal without mod_rewrite enabled is 2.2.16. See the <a href=":link">enabling clean URLs</a> page for more information on mod_rewrite.', [':link' => 'https://www.drupal.org/docs/8/clean-urls-in-drupal-8']), - ]; - } - - if (!$rewrite_error && !$rewrite_warning) { - $requirements['rewrite_module'] = [ - 'title' => t('Clean URLs'), - 'value' => t('Disabled'), - 'severity' => RequirementSeverity::Warning, - 'description' => t('Your server is capable of using clean URLs, but it is not enabled. Using clean URLs gives an improved user experience and is recommended. <a href=":link">Enable clean URLs</a>', [':link' => 'https://www.drupal.org/docs/8/clean-urls-in-drupal-8']), - ]; - } - } - - // Verify the user is running a supported PHP version. - // If the site is running a recommended version of PHP, just display it - // as an informational message on the status report. This will be overridden - // with an error or warning if the site is running older PHP versions for - // which Drupal has already or will soon drop support. - $phpversion = $phpversion_label = phpversion(); - if ($phase === 'runtime') { - $phpversion_label = t('@phpversion (<a href=":url">more information</a>)', [ - '@phpversion' => $phpversion, - ':url' => (new Url('system.php'))->toString(), - ]); - } - $requirements['php'] = [ - 'title' => t('PHP'), - 'value' => $phpversion_label, - ]; - - // Check if the PHP version is below what Drupal supports. - if (version_compare($phpversion, $minimum_supported_php) < 0) { - $requirements['php']['description'] = t('Your PHP installation is too old. Drupal requires at least PHP %version. It is recommended to upgrade to PHP version %recommended or higher for the best ongoing support. See <a href="http://php.net/supported-versions.php">PHP\'s version support documentation</a> and the <a href=":php_requirements">Drupal PHP requirements</a> page for more information.', - [ - '%version' => $minimum_supported_php, - '%recommended' => \Drupal::RECOMMENDED_PHP, - ':php_requirements' => 'https://www.drupal.org/docs/system-requirements/php-requirements', - ] - ); - - // If the PHP version is also below the absolute minimum allowed, it's not - // safe to continue with the requirements check, and should always be an - // error. - if (version_compare($phpversion, \Drupal::MINIMUM_PHP) < 0) { - $requirements['php']['severity'] = RequirementSeverity::Error; - return $requirements; - } - // Otherwise, the message should be an error at runtime, and a warning - // during installation or update. - $requirements['php']['severity'] = ($phase === 'runtime') ? RequirementSeverity::Error : RequirementSeverity::Warning; - } - // For PHP versions that are still supported but no longer recommended, - // inform users of what's recommended, allowing them to take action before it - // becomes urgent. - elseif ($phase === 'runtime' && version_compare($phpversion, \Drupal::RECOMMENDED_PHP) < 0) { - $requirements['php']['description'] = t('It is recommended to upgrade to PHP version %recommended or higher for the best ongoing support. See <a href="http://php.net/supported-versions.php">PHP\'s version support documentation</a> and the <a href=":php_requirements">Drupal PHP requirements</a> page for more information.', ['%recommended' => \Drupal::RECOMMENDED_PHP, ':php_requirements' => 'https://www.drupal.org/docs/system-requirements/php-requirements']); - $requirements['php']['severity'] = RequirementSeverity::Info; - } - - // Test for PHP extensions. - $requirements['php_extensions'] = [ - 'title' => t('PHP extensions'), - ]; - - $missing_extensions = []; - $required_extensions = [ - 'date', - 'dom', - 'filter', - 'gd', - 'hash', - 'json', - 'pcre', - 'pdo', - 'session', - 'SimpleXML', - 'SPL', - 'tokenizer', - 'xml', - 'zlib', - ]; - foreach ($required_extensions as $extension) { - if (!extension_loaded($extension)) { - $missing_extensions[] = $extension; - } - } - - if (!empty($missing_extensions)) { - $description = t('Drupal requires you to enable the PHP extensions in the following list (see the <a href=":system_requirements">system requirements page</a> for more information):', [ - ':system_requirements' => 'https://www.drupal.org/docs/system-requirements', - ]); - - // We use twig inline_template to avoid twig's autoescape. - $description = [ - '#type' => 'inline_template', - '#template' => '{{ description }}{{ missing_extensions }}', - '#context' => [ - 'description' => $description, - 'missing_extensions' => [ - '#theme' => 'item_list', - '#items' => $missing_extensions, - ], - ], - ]; - - $requirements['php_extensions']['value'] = t('Disabled'); - $requirements['php_extensions']['severity'] = RequirementSeverity::Error; - $requirements['php_extensions']['description'] = $description; - } - else { - $requirements['php_extensions']['value'] = t('Enabled'); - } - - if ($phase == 'install' || $phase == 'runtime') { - // Check to see if OPcache is installed. - if (!OpCodeCache::isEnabled()) { - $requirements['php_opcache'] = [ - 'value' => t('Not enabled'), - 'severity' => RequirementSeverity::Warning, - 'description' => t('PHP OPcode caching can improve your site\'s performance considerably. It is <strong>highly recommended</strong> to have <a href="http://php.net/manual/opcache.installation.php" target="_blank">OPcache</a> installed on your server.'), - ]; - } - else { - $requirements['php_opcache']['value'] = t('Enabled'); - } - $requirements['php_opcache']['title'] = t('PHP OPcode caching'); - } - - // Check to see if APCu is installed and configured correctly. - if ($phase == 'runtime' && PHP_SAPI != 'cli') { - $requirements['php_apcu_enabled']['title'] = t('PHP APCu caching'); - $requirements['php_apcu_available']['title'] = t('PHP APCu available caching'); - if (extension_loaded('apcu') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN)) { - $memory_info = apcu_sma_info(TRUE); - $apcu_actual_size = ByteSizeMarkup::create($memory_info['seg_size'] * $memory_info['num_seg']); - $apcu_recommended_size = '32 MB'; - $requirements['php_apcu_enabled']['value'] = t('Enabled (@size)', ['@size' => $apcu_actual_size]); - if (Bytes::toNumber(ini_get('apc.shm_size')) * ini_get('apc.shm_segments') < Bytes::toNumber($apcu_recommended_size)) { - $requirements['php_apcu_enabled']['severity'] = RequirementSeverity::Warning; - $requirements['php_apcu_enabled']['description'] = t('Depending on your configuration, Drupal can run with a @apcu_size APCu limit. However, a @apcu_default_size APCu limit (the default) or above is recommended, especially if your site uses additional custom or contributed modules.', [ - '@apcu_size' => $apcu_actual_size, - '@apcu_default_size' => $apcu_recommended_size, - ]); - } - else { - $memory_available = $memory_info['avail_mem'] / ($memory_info['seg_size'] * $memory_info['num_seg']); - if ($memory_available < 0.1) { - $requirements['php_apcu_available']['severity'] = RequirementSeverity::Error; - $requirements['php_apcu_available']['description'] = t('APCu is using over 90% of its allotted memory (@apcu_actual_size). To improve APCu performance, consider increasing this limit.', [ - '@apcu_actual_size' => $apcu_actual_size, - ]); - } - elseif ($memory_available < 0.25) { - $requirements['php_apcu_available']['severity'] = RequirementSeverity::Warning; - $requirements['php_apcu_available']['description'] = t('APCu is using over 75% of its allotted memory (@apcu_actual_size). To improve APCu performance, consider increasing this limit.', [ - '@apcu_actual_size' => $apcu_actual_size, - ]); - } - else { - $requirements['php_apcu_available']['severity'] = RequirementSeverity::OK; - } - $requirements['php_apcu_available']['value'] = t('Memory available: @available.', [ - '@available' => ByteSizeMarkup::create($memory_info['avail_mem']), - ]); - } - } - else { - $requirements['php_apcu_enabled'] += [ - 'value' => t('Not enabled'), - 'severity' => RequirementSeverity::Info, - 'description' => t('PHP APCu caching can improve your site\'s performance considerably. It is <strong>highly recommended</strong> to have <a href="https://www.php.net/manual/apcu.installation.php" target="_blank">APCu</a> installed on your server.'), - ]; - } - } - - if ($phase != 'update') { - // Test whether we have a good source of random bytes. - $requirements['php_random_bytes'] = [ - 'title' => t('Random number generation'), - ]; - try { - $bytes = random_bytes(10); - if (strlen($bytes) != 10) { - throw new \Exception("Tried to generate 10 random bytes, generated '" . strlen($bytes) . "'"); - } - $requirements['php_random_bytes']['value'] = t('Successful'); - } - catch (\Exception $e) { - // If /dev/urandom is not available on a UNIX-like system, check whether - // open_basedir restrictions are the cause. - $open_basedir_blocks_urandom = FALSE; - if (DIRECTORY_SEPARATOR === '/' && !@is_readable('/dev/urandom')) { - $open_basedir = ini_get('open_basedir'); - if ($open_basedir) { - $open_basedir_paths = explode(PATH_SEPARATOR, $open_basedir); - $open_basedir_blocks_urandom = !array_intersect(['/dev', '/dev/', '/dev/urandom'], $open_basedir_paths); - } - } - $args = [ - ':drupal-php' => 'https://www.drupal.org/docs/system-requirements/php-requirements', - '%exception_message' => $e->getMessage(), - ]; - if ($open_basedir_blocks_urandom) { - $requirements['php_random_bytes']['description'] = t('Drupal is unable to generate highly randomized numbers, which means certain security features like password reset URLs are not as secure as they should be. Instead, only a slow, less-secure fallback generator is available. The most likely cause is that open_basedir restrictions are in effect and /dev/urandom is not on the allowed list. See the <a href=":drupal-php">system requirements</a> page for more information. %exception_message', $args); - } - else { - $requirements['php_random_bytes']['description'] = t('Drupal is unable to generate highly randomized numbers, which means certain security features like password reset URLs are not as secure as they should be. Instead, only a slow, less-secure fallback generator is available. See the <a href=":drupal-php">system requirements</a> page for more information. %exception_message', $args); - } - $requirements['php_random_bytes']['value'] = t('Less secure'); - $requirements['php_random_bytes']['severity'] = RequirementSeverity::Error; - } - } - - if ($phase === 'runtime' && PHP_SAPI !== 'cli') { - if (!function_exists('fastcgi_finish_request') && !function_exists('litespeed_finish_request') && !ob_get_status()) { - $requirements['output_buffering'] = [ - 'title' => t('Output Buffering'), - 'error_value' => t('Not enabled'), - 'severity' => RequirementSeverity::Warning, - 'description' => t('<a href="https://www.php.net/manual/en/function.ob-start.php">Output buffering</a> is not enabled. This may degrade Drupal\'s performance. You can enable output buffering by default <a href="https://www.php.net/manual/en/outcontrol.configuration.php#ini.output-buffering">in your PHP settings</a>.'), - ]; - } - } - - if ($phase == 'install' || $phase == 'update') { - // Test for PDO (database). - $requirements['database_extensions'] = [ - 'title' => t('Database support'), - ]; - - // Make sure PDO is available. - $database_ok = extension_loaded('pdo'); - if (!$database_ok) { - $pdo_message = t('Your web server does not appear to support PDO (PHP Data Objects). Ask your hosting provider if they support the native PDO extension. See the <a href=":link">system requirements</a> page for more information.', [ - ':link' => 'https://www.drupal.org/docs/system-requirements/php-requirements#database', - ]); - } - else { - // Make sure at least one supported database driver exists. - if (empty(Database::getDriverList()->getInstallableList())) { - $database_ok = FALSE; - $pdo_message = t('Your web server does not appear to support any common PDO database extensions. Check with your hosting provider to see if they support PDO (PHP Data Objects) and offer any databases that <a href=":drupal-databases">Drupal supports</a>.', [ - ':drupal-databases' => 'https://www.drupal.org/docs/system-requirements/database-server-requirements', - ]); - } - // Make sure the native PDO extension is available, not the older PEAR - // version. (See install_verify_pdo() for details.) - if (!defined('PDO::ATTR_DEFAULT_FETCH_MODE')) { - $database_ok = FALSE; - $pdo_message = t('Your web server seems to have the wrong version of PDO installed. Drupal requires the PDO extension from PHP core. This system has the older PECL version. See the <a href=":link">system requirements</a> page for more information.', [ - ':link' => 'https://www.drupal.org/docs/system-requirements/php-requirements#database', - ]); - } - } - - if (!$database_ok) { - $requirements['database_extensions']['value'] = t('Disabled'); - $requirements['database_extensions']['severity'] = RequirementSeverity::Error; - $requirements['database_extensions']['description'] = $pdo_message; - } - else { - $requirements['database_extensions']['value'] = t('Enabled'); - } - } - - if ($phase === 'runtime' || $phase === 'update') { - // Database information. - $class = Database::getConnection()->getConnectionOptions()['namespace'] . '\\Install\\Tasks'; - /** @var \Drupal\Core\Database\Install\Tasks $tasks */ - $tasks = new $class(); - $requirements['database_system'] = [ - 'title' => t('Database system'), - 'value' => $tasks->name(), - ]; - $requirements['database_system_version'] = [ - 'title' => t('Database system version'), - 'value' => Database::getConnection()->version(), - ]; - - $errors = $tasks->engineVersionRequirementsCheck(); - $error_count = count($errors); - if ($error_count > 0) { - $error_message = [ - '#theme' => 'item_list', - '#items' => $errors, - // Use the comma-list style to display a single error without bullets. - '#context' => ['list_style' => $error_count === 1 ? 'comma-list' : ''], - ]; - $requirements['database_system_version']['severity'] = RequirementSeverity::Error; - $requirements['database_system_version']['description'] = $error_message; - } - } - - if ($phase === 'runtime' || $phase === 'update') { - // Test database JSON support. - $requirements['database_support_json'] = [ - 'title' => t('Database support for JSON'), - 'severity' => RequirementSeverity::OK, - 'value' => t('Available'), - 'description' => t('Drupal requires databases that support JSON storage.'), - ]; - - if (!Database::getConnection()->hasJson()) { - $requirements['database_support_json']['value'] = t('Not available'); - $requirements['database_support_json']['severity'] = RequirementSeverity::Error; - } - } - - // Test PHP memory_limit - $memory_limit = ini_get('memory_limit'); - $requirements['php_memory_limit'] = [ - 'title' => t('PHP memory limit'), - 'value' => $memory_limit == -1 ? t('-1 (Unlimited)') : $memory_limit, - ]; - - if (!Environment::checkMemoryLimit(\Drupal::MINIMUM_PHP_MEMORY_LIMIT, $memory_limit)) { - $description = []; - if ($phase == 'install') { - $description['phase'] = t('Consider increasing your PHP memory limit to %memory_minimum_limit to help prevent errors in the installation process.', ['%memory_minimum_limit' => \Drupal::MINIMUM_PHP_MEMORY_LIMIT]); - } - elseif ($phase == 'update') { - $description['phase'] = t('Consider increasing your PHP memory limit to %memory_minimum_limit to help prevent errors in the update process.', ['%memory_minimum_limit' => \Drupal::MINIMUM_PHP_MEMORY_LIMIT]); - } - elseif ($phase == 'runtime') { - $description['phase'] = t('Depending on your configuration, Drupal can run with a %memory_limit PHP memory limit. However, a %memory_minimum_limit PHP memory limit or above is recommended, especially if your site uses additional custom or contributed modules.', ['%memory_limit' => $memory_limit, '%memory_minimum_limit' => \Drupal::MINIMUM_PHP_MEMORY_LIMIT]); - } - - if (!empty($description['phase'])) { - if ($php_ini_path = get_cfg_var('cfg_file_path')) { - $description['memory'] = t('Increase the memory limit by editing the memory_limit parameter in the file %configuration-file and then restart your web server (or contact your system administrator or hosting provider for assistance).', ['%configuration-file' => $php_ini_path]); - } - else { - $description['memory'] = t('Contact your system administrator or hosting provider for assistance with increasing your PHP memory limit.'); - } - - $handbook_link = t('For more information, see the online handbook entry for <a href=":memory-limit">increasing the PHP memory limit</a>.', [':memory-limit' => 'https://www.drupal.org/node/207036']); - - $description = [ - '#type' => 'inline_template', - '#template' => '{{ description_phase }} {{ description_memory }} {{ handbook }}', - '#context' => [ - 'description_phase' => $description['phase'], - 'description_memory' => $description['memory'], - 'handbook' => $handbook_link, - ], - ]; - - $requirements['php_memory_limit']['description'] = $description; - $requirements['php_memory_limit']['severity'] = RequirementSeverity::Warning; - } - } - - // Test if configuration files and directory are writable. - if ($phase == 'runtime') { - $conf_errors = []; - // Find the site path. Kernel service is not always available at this point, - // but is preferred, when available. - if (\Drupal::hasService('kernel')) { - $site_path = \Drupal::getContainer()->getParameter('site.path'); - } - else { - $site_path = DrupalKernel::findSitePath(Request::createFromGlobals()); - } - // Allow system administrators to disable permissions hardening for the site - // directory. This allows additional files in the site directory to be - // updated when they are managed in a version control system. - if (Settings::get('skip_permissions_hardening')) { - $error_value = t('Protection disabled'); - // If permissions hardening is disabled, then only show a warning for a - // writable file, as a reminder, rather than an error. - $file_protection_severity = RequirementSeverity::Warning; - } - else { - $error_value = t('Not protected'); - // In normal operation, writable files or directories are an error. - $file_protection_severity = RequirementSeverity::Error; - if (!drupal_verify_install_file($site_path, FILE_NOT_WRITABLE, 'dir')) { - $conf_errors[] = t("The directory %file is not protected from modifications and poses a security risk. You must change the directory's permissions to be non-writable.", ['%file' => $site_path]); - } - } - foreach (['settings.php', 'settings.local.php', 'services.yml'] as $conf_file) { - $full_path = $site_path . '/' . $conf_file; - if (file_exists($full_path) && !drupal_verify_install_file($full_path, FILE_EXIST | FILE_READABLE | FILE_NOT_WRITABLE, 'file', !Settings::get('skip_permissions_hardening'))) { - $conf_errors[] = t("The file %file is not protected from modifications and poses a security risk. You must change the file's permissions to be non-writable.", ['%file' => $full_path]); - } - } - if (!empty($conf_errors)) { - if (count($conf_errors) == 1) { - $description = $conf_errors[0]; - } - else { - // We use twig inline_template to avoid double escaping. - $description = [ - '#type' => 'inline_template', - '#template' => '{{ configuration_error_list }}', - '#context' => [ - 'configuration_error_list' => [ - '#theme' => 'item_list', - '#items' => $conf_errors, - ], - ], - ]; - } - $requirements['configuration_files'] = [ - 'value' => $error_value, - 'severity' => $file_protection_severity, - 'description' => $description, - ]; - } - else { - $requirements['configuration_files'] = [ - 'value' => t('Protected'), - ]; - } - $requirements['configuration_files']['title'] = t('Configuration files'); - } - - // Test the contents of the .htaccess files. - if ($phase == 'runtime') { - // Try to write the .htaccess files first, to prevent false alarms in case - // (for example) the /tmp directory was wiped. - /** @var \Drupal\Core\File\HtaccessWriterInterface $htaccessWriter */ - $htaccessWriter = \Drupal::service("file.htaccess_writer"); - $htaccessWriter->ensure(); - foreach ($htaccessWriter->defaultProtectedDirs() as $protected_dir) { - $htaccess_file = $protected_dir->getPath() . '/.htaccess'; - // Check for the string which was added to the recommended .htaccess file - // in the latest security update. - if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || !str_contains($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003')) { - $url = 'https://www.drupal.org/SA-CORE-2013-003'; - $requirements[$htaccess_file] = [ - // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString - 'title' => new TranslatableMarkup($protected_dir->getTitle()), - 'value' => t('Not fully protected'), - 'severity' => RequirementSeverity::Error, - 'description' => t('See <a href=":url">@url</a> for information about the recommended .htaccess file which should be added to the %directory directory to help protect against arbitrary code execution.', [':url' => $url, '@url' => $url, '%directory' => $protected_dir->getPath()]), - ]; - } - } - } - - // Report cron status. - if ($phase == 'runtime') { - $cron_config = \Drupal::config('system.cron'); - // Cron warning threshold defaults to two days. - $threshold_warning = $cron_config->get('threshold.requirements_warning'); - // Cron error threshold defaults to two weeks. - $threshold_error = $cron_config->get('threshold.requirements_error'); - - // Determine when cron last ran. - $cron_last = \Drupal::state()->get('system.cron_last'); - if (!is_numeric($cron_last)) { - $cron_last = \Drupal::state()->get('install_time', 0); - } - - // Determine severity based on time since cron last ran. - $severity = RequirementSeverity::Info; - $request_time = \Drupal::time()->getRequestTime(); - if ($request_time - $cron_last > $threshold_error) { - $severity = RequirementSeverity::Error; - } - elseif ($request_time - $cron_last > $threshold_warning) { - $severity = RequirementSeverity::Warning; - } - - // Set summary and description based on values determined above. - $summary = t('Last run @time ago', ['@time' => \Drupal::service('date.formatter')->formatTimeDiffSince($cron_last)]); - - $requirements['cron'] = [ - 'title' => t('Cron maintenance tasks'), - 'severity' => $severity, - 'value' => $summary, - ]; - if ($severity != RequirementSeverity::Info) { - $requirements['cron']['description'][] = [ - [ - '#markup' => t('Cron has not run recently.'), - '#suffix' => ' ', - ], - [ - '#markup' => t('For more information, see the online handbook entry for <a href=":cron-handbook">configuring cron jobs</a>.', [':cron-handbook' => 'https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview']), - '#suffix' => ' ', - ], - ]; - } - $requirements['cron']['description'][] = [ - [ - '#type' => 'link', - '#prefix' => '(', - '#title' => t('more information'), - '#suffix' => ')', - '#url' => Url::fromRoute('system.cron_settings'), - ], - [ - '#prefix' => '<span class="cron-description__run-cron">', - '#suffix' => '</span>', - '#type' => 'link', - '#title' => t('Run cron'), - '#url' => Url::fromRoute('system.run_cron'), - ], - ]; - } - if ($phase != 'install') { - $directories = [ - PublicStream::basePath(), - // By default no private files directory is configured. For private files - // to be secure the admin needs to provide a path outside the webroot. - PrivateStream::basePath(), - \Drupal::service('file_system')->getTempDirectory(), - ]; - } - - // During an install we need to make assumptions about the file system - // unless overrides are provided in settings.php. - if ($phase == 'install') { - $directories = []; - if ($file_public_path = Settings::get('file_public_path')) { - $directories[] = $file_public_path; - } - else { - // If we are installing Drupal, the settings.php file might not exist yet - // in the intended site directory, so don't require it. - $request = Request::createFromGlobals(); - $site_path = DrupalKernel::findSitePath($request); - $directories[] = $site_path . '/files'; - } - if ($file_private_path = Settings::get('file_private_path')) { - $directories[] = $file_private_path; - } - if (Settings::get('file_temp_path')) { - $directories[] = Settings::get('file_temp_path'); - } - else { - // If the temporary directory is not overridden use an appropriate - // temporary path for the system. - $directories[] = FileSystemComponent::getOsTemporaryDirectory(); - } - } - - // Check the config directory if it is defined in settings.php. If it isn't - // defined, the installer will create a valid config directory later, but - // during runtime we must always display an error. - $config_sync_directory = Settings::get('config_sync_directory'); - if (!empty($config_sync_directory)) { - // If we're installing Drupal try and create the config sync directory. - if (!is_dir($config_sync_directory) && $phase == 'install') { - \Drupal::service('file_system')->prepareDirectory($config_sync_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); - } - if (!is_dir($config_sync_directory)) { - if ($phase == 'install') { - $description = t('An automated attempt to create the directory %directory failed, possibly due to a permissions problem. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see INSTALL.txt or the <a href=":handbook_url">online handbook</a>.', ['%directory' => $config_sync_directory, ':handbook_url' => 'https://www.drupal.org/server-permissions']); - } - else { - $description = t('The directory %directory does not exist.', ['%directory' => $config_sync_directory]); - } - $requirements['config sync directory'] = [ - 'title' => t('Configuration sync directory'), - 'description' => $description, - 'severity' => RequirementSeverity::Error, - ]; - } - } - if ($phase != 'install' && empty($config_sync_directory)) { - $requirements['config sync directory'] = [ - 'title' => t('Configuration sync directory'), - 'value' => t('Not present'), - 'description' => t("Your %file file must define the %setting setting as a string containing the directory in which configuration files can be found.", ['%file' => $site_path . '/settings.php', '%setting' => "\$settings['config_sync_directory']"]), - 'severity' => RequirementSeverity::Error, - ]; - } - - $requirements['file system'] = [ - 'title' => t('File system'), - ]; - - $error = ''; - // For installer, create the directories if possible. - foreach ($directories as $directory) { - if (!$directory) { - continue; - } - if ($phase == 'install') { - \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); - } - $is_writable = is_writable($directory); - $is_directory = is_dir($directory); - if (!$is_writable || !$is_directory) { - $description = ''; - $requirements['file system']['value'] = t('Not writable'); - if (!$is_directory) { - $error = t('The directory %directory does not exist.', ['%directory' => $directory]); - } - else { - $error = t('The directory %directory is not writable.', ['%directory' => $directory]); - } - // The files directory requirement check is done only during install and - // runtime. - if ($phase == 'runtime') { - $description = t('You may need to set the correct directory at the <a href=":admin-file-system">file system settings page</a> or change the current directory\'s permissions so that it is writable.', [':admin-file-system' => Url::fromRoute('system.file_system_settings')->toString()]); - } - elseif ($phase == 'install') { - // For the installer UI, we need different wording. 'value' will - // be treated as version, so provide none there. - $description = t('An automated attempt to create this directory failed, possibly due to a permissions problem. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see INSTALL.txt or the <a href=":handbook_url">online handbook</a>.', [':handbook_url' => 'https://www.drupal.org/server-permissions']); - $requirements['file system']['value'] = ''; - } - if (!empty($description)) { - $description = [ - '#type' => 'inline_template', - '#template' => '{{ error }} {{ description }}', - '#context' => [ - 'error' => $error, - 'description' => $description, - ], - ]; - $requirements['file system']['description'] = $description; - $requirements['file system']['severity'] = RequirementSeverity::Error; - } - } - else { - // This function can be called before the config_cache table has been - // created. - if ($phase == 'install' || \Drupal::config('system.file')->get('default_scheme') == 'public') { - $requirements['file system']['value'] = t('Writable (<em>public</em> download method)'); - } - else { - $requirements['file system']['value'] = t('Writable (<em>private</em> download method)'); - } - } - } - - // See if updates are available in update.php. - if ($phase == 'runtime') { - $requirements['update'] = [ - 'title' => t('Database updates'), - 'value' => t('Up to date'), - ]; - - // Check installed modules. - $has_pending_updates = FALSE; - /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */ - $update_registry = \Drupal::service('update.update_hook_registry'); - foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) { - $updates = $update_registry->getAvailableUpdates($module); - if ($updates) { - $default = $update_registry->getInstalledVersion($module); - if (max($updates) > $default) { - $has_pending_updates = TRUE; - break; - } - } - } - if (!$has_pending_updates) { - /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ - $post_update_registry = \Drupal::service('update.post_update_registry'); - $missing_post_update_functions = $post_update_registry->getPendingUpdateFunctions(); - if (!empty($missing_post_update_functions)) { - $has_pending_updates = TRUE; - } - } - - if ($has_pending_updates) { - $requirements['update']['severity'] = RequirementSeverity::Error; - $requirements['update']['value'] = t('Out of date'); - $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href=":update">database update script</a> immediately.', [':update' => Url::fromRoute('system.db_update')->toString()]); - } - - $requirements['entity_update'] = [ - 'title' => t('Entity/field definitions'), - 'value' => t('Up to date'), - ]; - // Verify that no entity updates are pending. - if ($change_list = \Drupal::entityDefinitionUpdateManager()->getChangeSummary()) { - $build = []; - foreach ($change_list as $entity_type_id => $changes) { - $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); - $build[] = [ - '#theme' => 'item_list', - '#title' => $entity_type->getLabel(), - '#items' => $changes, - ]; - } - - $entity_update_issues = \Drupal::service('renderer')->renderInIsolation($build); - $requirements['entity_update']['severity'] = RequirementSeverity::Error; - $requirements['entity_update']['value'] = t('Mismatched entity and/or field definitions'); - $requirements['entity_update']['description'] = t('The following changes were detected in the entity type and field definitions. @updates', ['@updates' => $entity_update_issues]); - } - } - - // Display the deployment identifier if set. - if ($phase == 'runtime') { - if ($deployment_identifier = Settings::get('deployment_identifier')) { - $requirements['deployment identifier'] = [ - 'title' => t('Deployment identifier'), - 'value' => $deployment_identifier, - 'severity' => RequirementSeverity::Info, - ]; - } - } - - // Verify the update.php access setting - if ($phase == 'runtime') { - if (Settings::get('update_free_access')) { - $requirements['update access'] = [ - 'value' => t('Not protected'), - 'severity' => RequirementSeverity::Error, - 'description' => t('The update.php script is accessible to everyone without authentication check, which is a security risk. You must change the @settings_name value in your settings.php back to FALSE.', ['@settings_name' => '$settings[\'update_free_access\']']), - ]; - } - else { - $requirements['update access'] = [ - 'value' => t('Protected'), - ]; - } - $requirements['update access']['title'] = t('Access to update.php'); - } - - // Display an error if a newly introduced dependency in a module is not - // resolved. - if ($phase === 'update' || $phase === 'runtime') { - $create_extension_incompatibility_list = function (array $extension_names, PluralTranslatableMarkup $description, PluralTranslatableMarkup $title, TranslatableMarkup|string $message = '', TranslatableMarkup|string $additional_description = '') { - if ($message === '') { - $message = new TranslatableMarkup('Review the <a href=":url"> suggestions for resolving this incompatibility</a> to repair your installation, and then re-run update.php.', [':url' => 'https://www.drupal.org/docs/updating-drupal/troubleshooting-database-updates']); - } - // Use an inline twig template to: - // - Concatenate MarkupInterface objects and preserve safeness. - // - Use the item_list theme for the extension list. - $template = [ - '#type' => 'inline_template', - '#template' => '{{ description }}{{ extensions }}{{ additional_description }}<br>', - '#context' => [ - 'extensions' => [ - '#theme' => 'item_list', - ], - ], - ]; - $template['#context']['extensions']['#items'] = $extension_names; - $template['#context']['description'] = $description; - $template['#context']['additional_description'] = $additional_description; - return [ - 'title' => $title, - 'value' => [ - 'list' => $template, - 'handbook_link' => [ - '#markup' => $message, - ], - ], - 'severity' => RequirementSeverity::Error, - ]; - }; - $profile = \Drupal::installProfile(); - $files = $module_extension_list->getList(); - $files += $theme_extension_list->getList(); - $core_incompatible_extensions = []; - $php_incompatible_extensions = []; - foreach ($files as $extension_name => $file) { - // Ignore uninstalled extensions and installation profiles. - if (!$file->status || $extension_name == $profile) { - continue; - } - - $name = $file->info['name']; - if (!empty($file->info['core_incompatible'])) { - $core_incompatible_extensions[$file->info['type']][] = $name; - } - - // Check the extension's PHP version. - $php = $file->info['php']; - if (version_compare($php, PHP_VERSION, '>')) { - $php_incompatible_extensions[$file->info['type']][] = $name; - } - - // Check the module's required modules. - /** @var \Drupal\Core\Extension\Dependency $requirement */ - foreach ($file->requires as $requirement) { - $required_module = $requirement->getName(); - // Check if the module exists. - if (!isset($files[$required_module])) { - $requirements["$extension_name-$required_module"] = [ - 'title' => t('Unresolved dependency'), - 'description' => t('@name requires this module.', ['@name' => $name]), - 'value' => t('@required_name (Missing)', ['@required_name' => $required_module]), - 'severity' => RequirementSeverity::Error, - ]; - continue; - } - // Check for an incompatible version. - $required_file = $files[$required_module]; - $required_name = $required_file->info['name']; - // Remove CORE_COMPATIBILITY- only from the start of the string. - $version = preg_replace('/^(' . \Drupal::CORE_COMPATIBILITY . '\-)/', '', $required_file->info['version'] ?? ''); - if (!$requirement->isCompatible($version)) { - $requirements["$extension_name-$required_module"] = [ - 'title' => t('Unresolved dependency'), - 'description' => t('@name requires this module and version. Currently using @required_name version @version', ['@name' => $name, '@required_name' => $required_name, '@version' => $version]), - 'value' => t('@required_name (Version @compatibility required)', ['@required_name' => $required_name, '@compatibility' => $requirement->getConstraintString()]), - 'severity' => RequirementSeverity::Error, - ]; - continue; - } - } - } - if (!empty($core_incompatible_extensions['module'])) { - $requirements['module_core_incompatible'] = $create_extension_incompatibility_list( - $core_incompatible_extensions['module'], - new PluralTranslatableMarkup( - count($core_incompatible_extensions['module']), - 'The following module is installed, but it is incompatible with Drupal @version:', - 'The following modules are installed, but they are incompatible with Drupal @version:', - ['@version' => \Drupal::VERSION] - ), - new PluralTranslatableMarkup( - count($core_incompatible_extensions['module']), - 'Incompatible module', - 'Incompatible modules' - ) - ); - } - if (!empty($core_incompatible_extensions['theme'])) { - $requirements['theme_core_incompatible'] = $create_extension_incompatibility_list( - $core_incompatible_extensions['theme'], - new PluralTranslatableMarkup( - count($core_incompatible_extensions['theme']), - 'The following theme is installed, but it is incompatible with Drupal @version:', - 'The following themes are installed, but they are incompatible with Drupal @version:', - ['@version' => \Drupal::VERSION] - ), - new PluralTranslatableMarkup( - count($core_incompatible_extensions['theme']), - 'Incompatible theme', - 'Incompatible themes' - ) - ); - } - if (!empty($php_incompatible_extensions['module'])) { - $requirements['module_php_incompatible'] = $create_extension_incompatibility_list( - $php_incompatible_extensions['module'], - new PluralTranslatableMarkup( - count($php_incompatible_extensions['module']), - 'The following module is installed, but it is incompatible with PHP @version:', - 'The following modules are installed, but they are incompatible with PHP @version:', - ['@version' => phpversion()] - ), - new PluralTranslatableMarkup( - count($php_incompatible_extensions['module']), - 'Incompatible module', - 'Incompatible modules' - ) - ); - } - if (!empty($php_incompatible_extensions['theme'])) { - $requirements['theme_php_incompatible'] = $create_extension_incompatibility_list( - $php_incompatible_extensions['theme'], - new PluralTranslatableMarkup( - count($php_incompatible_extensions['theme']), - 'The following theme is installed, but it is incompatible with PHP @version:', - 'The following themes are installed, but they are incompatible with PHP @version:', - ['@version' => phpversion()] - ), - new PluralTranslatableMarkup( - count($php_incompatible_extensions['theme']), - 'Incompatible theme', - 'Incompatible themes' - ) - ); - } - - $extension_config = \Drupal::configFactory()->get('core.extension'); - - // Look for removed core modules. - $is_removed_module = function ($extension_name) use ($module_extension_list) { - return !$module_extension_list->exists($extension_name) - && array_key_exists($extension_name, DRUPAL_CORE_REMOVED_MODULE_LIST); - }; - $removed_modules = array_filter(array_keys($extension_config->get('module')), $is_removed_module); - if (!empty($removed_modules)) { - $list = []; - foreach ($removed_modules as $removed_module) { - $list[] = t('<a href=":url">@module</a>', [ - ':url' => "https://www.drupal.org/project/$removed_module", - '@module' => DRUPAL_CORE_REMOVED_MODULE_LIST[$removed_module], - ]); - } - $requirements['removed_module'] = $create_extension_incompatibility_list( - $list, - new PluralTranslatableMarkup( - count($removed_modules), - 'You must add the following contributed module and reload this page.', - 'You must add the following contributed modules and reload this page.' - ), - new PluralTranslatableMarkup( - count($removed_modules), - 'Removed core module', - 'Removed core modules' - ), - new TranslatableMarkup( - 'For more information read the <a href=":url">documentation on deprecated modules.</a>', - [':url' => 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-modules'] - ), - new PluralTranslatableMarkup( - count($removed_modules), - 'This module is installed on your site but is no longer provided by Core.', - 'These modules are installed on your site but are no longer provided by Core.' - ), - ); - } - - // Look for removed core themes. - $is_removed_theme = function ($extension_name) use ($theme_extension_list) { - return !$theme_extension_list->exists($extension_name) - && array_key_exists($extension_name, DRUPAL_CORE_REMOVED_THEME_LIST); - }; - $removed_themes = array_filter(array_keys($extension_config->get('theme')), $is_removed_theme); - if (!empty($removed_themes)) { - $list = []; - foreach ($removed_themes as $removed_theme) { - $list[] = t('<a href=":url">@theme</a>', [ - ':url' => "https://www.drupal.org/project/$removed_theme", - '@theme' => DRUPAL_CORE_REMOVED_THEME_LIST[$removed_theme], - ]); - } - $requirements['removed_theme'] = $create_extension_incompatibility_list( - $list, - new PluralTranslatableMarkup( - count($removed_themes), - 'You must add the following contributed theme and reload this page.', - 'You must add the following contributed themes and reload this page.' - ), - new PluralTranslatableMarkup( - count($removed_themes), - 'Removed core theme', - 'Removed core themes' - ), - new TranslatableMarkup( - 'For more information read the <a href=":url">documentation on deprecated themes.</a>', - [':url' => 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-themes'] - ), - new PluralTranslatableMarkup( - count($removed_themes), - 'This theme is installed on your site but is no longer provided by Core.', - 'These themes are installed on your site but are no longer provided by Core.' - ), - ); - } - - // Look for missing modules. - $is_missing_module = function ($extension_name) use ($module_extension_list) { - return !$module_extension_list->exists($extension_name) && !in_array($extension_name, array_keys(DRUPAL_CORE_REMOVED_MODULE_LIST), TRUE); - }; - $invalid_modules = array_filter(array_keys($extension_config->get('module')), $is_missing_module); - - if (!empty($invalid_modules)) { - $requirements['invalid_module'] = $create_extension_incompatibility_list( - $invalid_modules, - new PluralTranslatableMarkup( - count($invalid_modules), - 'The following module is marked as installed in the core.extension configuration, but it is missing:', - 'The following modules are marked as installed in the core.extension configuration, but they are missing:' - ), - new PluralTranslatableMarkup( - count($invalid_modules), - 'Missing or invalid module', - 'Missing or invalid modules' - ) - ); - } - - // Look for invalid themes. - $is_missing_theme = function ($extension_name) use (&$theme_extension_list) { - return !$theme_extension_list->exists($extension_name) && !in_array($extension_name, array_keys(DRUPAL_CORE_REMOVED_THEME_LIST), TRUE); - }; - $invalid_themes = array_filter(array_keys($extension_config->get('theme')), $is_missing_theme); - if (!empty($invalid_themes)) { - $requirements['invalid_theme'] = $create_extension_incompatibility_list( - $invalid_themes, - new PluralTranslatableMarkup( - count($invalid_themes), - 'The following theme is marked as installed in the core.extension configuration, but it is missing:', - 'The following themes are marked as installed in the core.extension configuration, but they are missing:' - ), - new PluralTranslatableMarkup( - count($invalid_themes), - 'Missing or invalid theme', - 'Missing or invalid themes' - ) - ); - } - } - - // Returns Unicode library status and errors. - $libraries = [ - Unicode::STATUS_SINGLEBYTE => t('Standard PHP'), - Unicode::STATUS_MULTIBYTE => t('PHP Mbstring Extension'), - Unicode::STATUS_ERROR => t('Error'), - ]; - $severities = [ - Unicode::STATUS_SINGLEBYTE => RequirementSeverity::Warning, - Unicode::STATUS_MULTIBYTE => NULL, - Unicode::STATUS_ERROR => RequirementSeverity::Error, - ]; - $failed_check = Unicode::check(); - $library = Unicode::getStatus(); - - $requirements['unicode'] = [ - 'title' => t('Unicode library'), - 'value' => $libraries[$library], - 'severity' => $severities[$library], - ]; - switch ($failed_check) { - case 'mb_strlen': - $requirements['unicode']['description'] = t('Operations on Unicode strings are emulated on a best-effort basis. Install the <a href="http://php.net/mbstring">PHP mbstring extension</a> for improved Unicode support.'); - break; - - case 'mbstring.encoding_translation': - $requirements['unicode']['description'] = t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini <em>mbstring.encoding_translation</em> setting. Refer to the <a href="http://php.net/mbstring">PHP mbstring documentation</a> for more information.'); - break; - } - - if ($phase == 'runtime') { - // Check for update status module. - if (!\Drupal::moduleHandler()->moduleExists('update')) { - $requirements['update status'] = [ - 'value' => t('Not enabled'), - 'severity' => RequirementSeverity::Warning, - 'description' => t('Update notifications are not enabled. It is <strong>highly recommended</strong> that you install the Update Status module from the <a href=":module">module administration page</a> in order to stay up-to-date on new releases. For more information, <a href=":update">Update status handbook page</a>.', [ - ':update' => 'https://www.drupal.org/documentation/modules/update', - ':module' => Url::fromRoute('system.modules_list')->toString(), - ]), - ]; - } - else { - $requirements['update status'] = [ - 'value' => t('Enabled'), - ]; - } - $requirements['update status']['title'] = t('Update notifications'); - - if (Settings::get('rebuild_access')) { - $requirements['rebuild access'] = [ - 'title' => t('Rebuild access'), - 'value' => t('Enabled'), - 'severity' => RequirementSeverity::Error, - 'description' => t('The rebuild_access setting is enabled in settings.php. It is recommended to have this setting disabled unless you are performing a rebuild.'), - ]; - } - } - - // Check if the SameSite cookie attribute is set to a valid value. Since this - // involves checking whether we are using a secure connection this only makes - // sense inside an HTTP request, not on the command line. - if ($phase === 'runtime' && PHP_SAPI !== 'cli') { - $samesite = ini_get('session.cookie_samesite') ?: t('Not set'); - // Check if the SameSite attribute is set to a valid value. If it is set to - // 'None' the request needs to be done over HTTPS. - $valid = match ($samesite) { - 'Lax', 'Strict' => TRUE, - 'None' => $request_object->isSecure(), - default => FALSE, - }; - $requirements['php_session_samesite'] = [ - 'title' => t('SameSite cookie attribute'), - 'value' => $samesite, - 'severity' => $valid ? RequirementSeverity::OK : RequirementSeverity::Warning, - 'description' => t('This attribute should be explicitly set to Lax, Strict or None. If set to None then the request must be made via HTTPS. See <a href=":url" target="_blank">PHP documentation</a>', [ - ':url' => 'https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite', - ]), - ]; - } - - // See if trusted host names have been configured, and warn the user if they - // are not set. - if ($phase == 'runtime') { - $trusted_host_patterns = Settings::get('trusted_host_patterns'); - if (empty($trusted_host_patterns)) { - $requirements['trusted_host_patterns'] = [ - 'title' => t('Trusted Host Settings'), - 'value' => t('Not enabled'), - 'description' => t('The trusted_host_patterns setting is not configured in settings.php. This can lead to security vulnerabilities. It is <strong>highly recommended</strong> that you configure this. See <a href=":url">Protecting against HTTP HOST Header attacks</a> for more information.', [':url' => 'https://www.drupal.org/docs/installing-drupal/trusted-host-settings']), - 'severity' => RequirementSeverity::Error, - ]; - } - else { - $requirements['trusted_host_patterns'] = [ - 'title' => t('Trusted Host Settings'), - 'value' => t('Enabled'), - 'description' => t('The trusted_host_patterns setting is set to allow %trusted_host_patterns', ['%trusted_host_patterns' => implode(', ', $trusted_host_patterns)]), - ]; - } - } - - // When the database driver is provided by a module, then check that the - // providing module is installed. - if ($phase === 'runtime' || $phase === 'update') { - $connection = Database::getConnection(); - $provider = $connection->getProvider(); - if ($provider !== 'core' && !\Drupal::moduleHandler()->moduleExists($provider)) { - $autoload = $connection->getConnectionOptions()['autoload'] ?? ''; - if (str_contains($autoload, 'src/Driver/Database/')) { - $post_update_registry = \Drupal::service('update.post_update_registry'); - $pending_updates = $post_update_registry->getPendingUpdateInformation(); - if (!in_array('enable_provider_database_driver', array_keys($pending_updates['system']['pending'] ?? []), TRUE)) { - // Only show the warning when the post update function has run and - // the module that is providing the database driver is not installed. - $requirements['database_driver_provided_by_module'] = [ - 'title' => t('Database driver provided by module'), - 'value' => t('Not installed'), - 'description' => t('The current database driver is provided by the module: %module. The module is currently not installed. You should immediately <a href=":install">install</a> the module.', ['%module' => $provider, ':install' => Url::fromRoute('system.modules_list')->toString()]), - 'severity' => RequirementSeverity::Error, - ]; - } - } - } - } - - // Check xdebug.max_nesting_level, as some pages will not work if it is too - // low. - if (extension_loaded('xdebug')) { - // Setting this value to 256 was considered adequate on Xdebug 2.3 - // (see http://bugs.xdebug.org/bug_view_page.php?bug_id=00001100) - $minimum_nesting_level = 256; - $current_nesting_level = ini_get('xdebug.max_nesting_level'); - - if ($current_nesting_level < $minimum_nesting_level) { - $requirements['xdebug_max_nesting_level'] = [ - 'title' => t('Xdebug settings'), - 'value' => t('xdebug.max_nesting_level is set to %value.', ['%value' => $current_nesting_level]), - 'description' => t('Set <code>xdebug.max_nesting_level=@level</code> in your PHP configuration as some pages in your Drupal site will not work when this setting is too low.', ['@level' => $minimum_nesting_level]), - 'severity' => RequirementSeverity::Error, - ]; - } - } - - // Installations on Windows can run into limitations with MAX_PATH if the - // Drupal root directory is too deep in the filesystem. Generally this shows - // up in cached Twig templates and other public files with long directory or - // file names. There is no definite root directory depth below which Drupal is - // guaranteed to function correctly on Windows. Since problems are likely - // with more than 100 characters in the Drupal root path, show an error. - if (str_starts_with(PHP_OS, 'WIN')) { - $depth = strlen(realpath(DRUPAL_ROOT . '/' . PublicStream::basePath())); - if ($depth > 120) { - $requirements['max_path_on_windows'] = [ - 'title' => t('Windows installation depth'), - 'description' => t('The public files directory path is %depth characters. Paths longer than 120 characters will cause problems on Windows.', ['%depth' => $depth]), - 'severity' => RequirementSeverity::Error, - ]; - } - } - // Check to see if dates will be limited to 1901-2038. - if (PHP_INT_SIZE <= 4) { - $requirements['limited_date_range'] = [ - 'title' => t('Limited date range'), - 'value' => t('Your PHP installation has a limited date range.'), - 'description' => t('You are running on a system where PHP is compiled or limited to using 32-bit integers. This will limit the range of dates and timestamps to the years 1901-2038. Read about the <a href=":url">limitations of 32-bit PHP</a>.', [':url' => 'https://www.drupal.org/docs/system-requirements/limitations-of-32-bit-php']), - 'severity' => RequirementSeverity::Warning, - ]; - } - - // During installs from configuration don't support install profiles that - // implement hook_install. - if ($phase == 'install' && !empty($install_state['config_install_path'])) { - $install_hook = $install_state['parameters']['profile'] . '_install'; - if (function_exists($install_hook)) { - $requirements['config_install'] = [ - 'title' => t('Configuration install'), - 'value' => $install_state['parameters']['profile'], - 'description' => t('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.'), - 'severity' => RequirementSeverity::Error, - ]; - } - } - - if ($phase === 'runtime') { - $settings = Settings::getAll(); - if (array_key_exists('install_profile', $settings)) { - // The following message is only informational because not all site owners - // have access to edit their settings.php as it may be controlled by their - // hosting provider. - $requirements['install_profile_in_settings'] = [ - 'title' => t('Install profile in settings'), - 'value' => t("Drupal 9 no longer uses the \$settings['install_profile'] value in settings.php and it should be removed."), - 'severity' => RequirementSeverity::Warning, - ]; - } - } - - // Ensure that no module has a current schema version that is lower than the - // one that was last removed. - if ($phase == 'update') { - $module_handler = \Drupal::moduleHandler(); - /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */ - $update_registry = \Drupal::service('update.update_hook_registry'); - $module_list = []; - // hook_update_last_removed() is a procedural hook hook because we - // do not have classes loaded that would be needed. - // Simply inlining the old hook mechanism is better than making - // ModuleInstaller::invoke() public. - foreach ($module_handler->getModuleList() as $module => $extension) { - $function = $module . '_update_last_removed'; - if (function_exists($function)) { - $last_removed = $function(); - if ($last_removed && $last_removed > $update_registry->getInstalledVersion($module)) { - - /** @var \Drupal\Core\Extension\Extension $module_info */ - $module_info = $module_extension_list->get($module); - $module_list[$module] = [ - 'name' => $module_info->info['name'], - 'last_removed' => $last_removed, - 'installed_version' => $update_registry->getInstalledVersion($module), - ]; - } - } - } - - // If user module is in the list then only show a specific message for - // Drupal core. - if (isset($module_list['user'])) { - $requirements['user_update_last_removed'] = [ - 'title' => t('The version of Drupal you are trying to update from is too old'), - 'description' => t('Updating to Drupal @current_major is only supported from Drupal version @required_min_version or higher. If you are trying to update from an older version, first update to the latest version of Drupal @previous_major. (<a href=":url">Drupal upgrade guide</a>)', [ - '@current_major' => 10, - '@required_min_version' => '9.4.0', - '@previous_major' => 9, - ':url' => 'https://www.drupal.org/docs/upgrading-drupal/drupal-8-and-higher', - ]), - 'severity' => RequirementSeverity::Error, - ]; - } - else { - foreach ($module_list as $module => $data) { - $requirements[$module . '_update_last_removed'] = [ - 'title' => t('Unsupported schema version: @module', ['@module' => $data['name']]), - 'description' => t('The installed version of the %module module is too old to update. Update to an intermediate version first (last removed version: @last_removed_version, installed version: @installed_version).', [ - '%module' => $data['name'], - '@last_removed_version' => $data['last_removed'], - '@installed_version' => $data['installed_version'], - ]), - 'severity' => RequirementSeverity::Error, - ]; - } - } - // Also check post-updates. Only do this if we're not already showing an - // error for hook_update_N(). - $missing_updates = []; - if (empty($module_list)) { - $existing_updates = \Drupal::service('keyvalue')->get('post_update')->get('existing_updates', []); - $post_update_registry = \Drupal::service('update.post_update_registry'); - $modules = \Drupal::moduleHandler()->getModuleList(); - foreach ($modules as $module => $extension) { - $module_info = $module_extension_list->get($module); - $removed_post_updates = $post_update_registry->getRemovedPostUpdates($module); - if ($missing_updates = array_diff(array_keys($removed_post_updates), $existing_updates)) { - $versions = array_unique(array_intersect_key($removed_post_updates, array_flip($missing_updates))); - $description = new PluralTranslatableMarkup(count($versions), - 'The installed version of the %module module is too old to update. Update to a version prior to @versions first (missing updates: @missing_updates).', - 'The installed version of the %module module is too old to update. Update first to a version prior to all of the following: @versions (missing updates: @missing_updates).', - [ - '%module' => $module_info->info['name'], - '@missing_updates' => implode(', ', $missing_updates), - '@versions' => implode(', ', $versions), - ] - ); - $requirements[$module . '_post_update_removed'] = [ - 'title' => t('Missing updates for: @module', ['@module' => $module_info->info['name']]), - 'description' => $description, - 'severity' => RequirementSeverity::Error, - ]; - } - } - } - - if (empty($missing_updates)) { - foreach ($update_registry->getAllEquivalentUpdates() as $module => $equivalent_updates) { - $module_info = $module_extension_list->get($module); - foreach ($equivalent_updates as $future_update => $data) { - $future_update_function_name = $module . '_update_' . $future_update; - $ran_update_function_name = $module . '_update_' . $data['ran_update']; - // If an update was marked as an equivalent by a previous update, and - // both the previous update and the equivalent update are not found in - // the current code base, prevent updating. This indicates a site - // attempting to go 'backwards' in terms of database schema. - // @see \Drupal\Core\Update\UpdateHookRegistry::markFutureUpdateEquivalent() - if (!function_exists($ran_update_function_name) && !function_exists($future_update_function_name)) { - // If the module is provided by core prepend helpful text as the - // module does not exist in composer or Drupal.org. - if (str_starts_with($module_info->getPathname(), 'core/')) { - $future_version_string = 'Drupal Core ' . $data['future_version_string']; - } - else { - $future_version_string = $data['future_version_string']; - } - $requirements[$module . '_equivalent_update_missing'] = [ - 'title' => t('Missing updates for: @module', ['@module' => $module_info->info['name']]), - 'description' => t('The version of the %module module that you are attempting to update to is missing update @future_update (which was marked as an equivalent by @ran_update). Update to at least @future_version_string.', [ - '%module' => $module_info->info['name'], - '@ran_update' => $data['ran_update'], - '@future_update' => $future_update, - '@future_version_string' => $future_version_string, - ]), - 'severity' => RequirementSeverity::Error, - ]; - break; - } - } - } - } - } - - // Add warning when twig debug option is enabled. - if ($phase === 'runtime') { - $development_settings = \Drupal::keyValue('development_settings'); - $twig_debug = $development_settings->get('twig_debug', FALSE); - $twig_cache_disable = $development_settings->get('twig_cache_disable', FALSE); - if ($twig_debug || $twig_cache_disable) { - $requirements['twig_debug_enabled'] = [ - 'title' => t('Twig development mode'), - 'value' => t('Twig development mode settings are turned on. Go to @link to disable them.', [ - '@link' => Link::createFromRoute( - 'development settings page', - 'system.development_settings', - )->toString(), - ]), - 'severity' => RequirementSeverity::Warning, - ]; - } - $render_cache_disabled = $development_settings->get('disable_rendered_output_cache_bins', FALSE); - if ($render_cache_disabled) { - $requirements['render_cache_disabled'] = [ - 'title' => t('Markup caching disabled'), - 'value' => t('Render cache, dynamic page cache, and page cache are bypassed. Go to @link to enable them.', [ - '@link' => Link::createFromRoute( - 'development settings page', - 'system.development_settings', - )->toString(), - ]), - 'severity' => RequirementSeverity::Warning, - ]; - } - } - - return $requirements; -} - -/** * Implements hook_install(). */ function system_install(): void { @@ -1686,57 +105,6 @@ function system_update_11200(): void { } /** - * Display requirements from security advisories. - * - * @param array[] $requirements - * The requirements array as specified in hook_requirements(). - */ -function _system_advisories_requirements(array &$requirements): void { - if (!\Drupal::config('system.advisories')->get('enabled')) { - return; - } - - /** @var \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher $fetcher */ - $fetcher = \Drupal::service('system.sa_fetcher'); - try { - $advisories = $fetcher->getSecurityAdvisories(TRUE, 5); - } - catch (ClientExceptionInterface $exception) { - $requirements['system_advisories']['title'] = t('Critical security announcements'); - $requirements['system_advisories']['severity'] = RequirementSeverity::Warning; - $requirements['system_advisories']['description'] = ['#theme' => 'system_security_advisories_fetch_error_message']; - Error::logException(\Drupal::logger('system'), $exception, 'Failed to retrieve security advisory data.'); - return; - } - - if (!empty($advisories)) { - $advisory_links = []; - $severity = RequirementSeverity::Warning; - foreach ($advisories as $advisory) { - if (!$advisory->isPsa()) { - $severity = RequirementSeverity::Error; - } - $advisory_links[] = new Link($advisory->getTitle(), Url::fromUri($advisory->getUrl())); - } - $requirements['system_advisories']['title'] = t('Critical security announcements'); - $requirements['system_advisories']['severity'] = $severity; - $requirements['system_advisories']['description'] = [ - 'list' => [ - '#theme' => 'item_list', - '#items' => $advisory_links, - ], - ]; - if (\Drupal::moduleHandler()->moduleExists('help')) { - $requirements['system_advisories']['description']['help_link'] = Link::createFromRoute( - 'What are critical security announcements?', - 'help.page', ['name' => 'system'], - ['fragment' => 'security-advisories'] - )->toRenderable(); - } - } -} - -/** * Invalidate container because the module handler has changed. */ function system_update_11100(): void { diff --git a/core/modules/system/system.libraries.yml b/core/modules/system/system.libraries.yml index acb02dd1f4b..cd7165bffbb 100644 --- a/core/modules/system/system.libraries.yml +++ b/core/modules/system/system.libraries.yml @@ -8,7 +8,6 @@ base: css/components/clearfix.module.css: { weight: -10 } css/components/hidden.module.css: { weight: -10 } css/components/js.module.css: { weight: -10 } - css/components/reset-appearance.module.css: { weight: -10 } admin: version: VERSION diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 0fbcf9b6c1f..8fac07911e9 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -9,7 +9,6 @@ use Drupal\Core\Extension\Extension; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; /** * Disabled option on forms and settings. @@ -61,81 +60,6 @@ function system_hook_info(): array { } /** - * Implements hook_theme_suggestions_HOOK(). - */ -function system_theme_suggestions_html(array $variables): array { - $path_args = explode('/', trim(\Drupal::service('path.current')->getPath(), '/')); - return theme_get_suggestions($path_args, 'html'); -} - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function system_theme_suggestions_page(array $variables): array { - $path_args = explode('/', trim(\Drupal::service('path.current')->getPath(), '/')); - $suggestions = theme_get_suggestions($path_args, 'page'); - - $supported_http_error_codes = [401, 403, 404]; - $exception = \Drupal::requestStack()->getCurrentRequest()->attributes->get('exception'); - if ($exception instanceof HttpExceptionInterface && in_array($exception->getStatusCode(), $supported_http_error_codes, TRUE)) { - $suggestions[] = 'page__4xx'; - $suggestions[] = 'page__' . $exception->getStatusCode(); - } - - return $suggestions; -} - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function system_theme_suggestions_maintenance_page(array $variables): array { - $suggestions = []; - - // Dead databases will show error messages so supplying this template will - // allow themers to override the page and the content completely. - $offline = defined('MAINTENANCE_MODE'); - try { - \Drupal::service('path.matcher')->isFrontPage(); - } - catch (Exception) { - // The database is not yet available. - $offline = TRUE; - } - if ($offline) { - $suggestions[] = 'maintenance_page__offline'; - } - - return $suggestions; -} - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function system_theme_suggestions_region(array $variables): array { - $suggestions = []; - if (!empty($variables['elements']['#region'])) { - $suggestions[] = 'region__' . $variables['elements']['#region']; - } - return $suggestions; -} - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function system_theme_suggestions_field(array $variables): array { - $suggestions = []; - $element = $variables['element']; - - $suggestions[] = 'field__' . $element['#field_type']; - $suggestions[] = 'field__' . $element['#field_name']; - $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#bundle']; - $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name']; - $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle']; - - return $suggestions; -} - -/** * Prepares variables for the list of available bundles. * * Default template: entity-add-list.html.twig. @@ -307,34 +231,6 @@ function system_authorized_batch_process() { } /** - * @} End of "defgroup authorize". - */ - -/** - * Implements hook_preprocess_HOOK() for block templates. - */ -function system_preprocess_block(&$variables): void { - switch ($variables['base_plugin_id']) { - case 'system_branding_block': - $variables['site_logo'] = ''; - if ($variables['content']['site_logo']['#access'] && $variables['content']['site_logo']['#uri']) { - $variables['site_logo'] = $variables['content']['site_logo']['#uri']; - } - $variables['site_name'] = ''; - if ($variables['content']['site_name']['#access'] && $variables['content']['site_name']['#markup']) { - $variables['site_name'] = $variables['content']['site_name']['#markup']; - } - $variables['site_slogan'] = ''; - if ($variables['content']['site_slogan']['#access'] && $variables['content']['site_slogan']['#markup']) { - $variables['site_slogan'] = [ - '#markup' => $variables['content']['site_slogan']['#markup'], - ]; - } - break; - } -} - -/** * Checks the existence of the directory specified in $form_element. * * This function is called from the system_settings form to check all core @@ -478,21 +374,3 @@ function _system_is_claro_admin_and_not_active() { $active_theme = \Drupal::theme()->getActiveTheme()->getName(); return $active_theme !== 'claro' && $admin_theme === 'claro'; } - -/** - * Implements hook_preprocess_toolbar(). - */ -function system_preprocess_toolbar(array &$variables, $hook, $info): void { - // When Claro is the admin theme, Claro overrides the active theme's if that - // active theme is not Claro. Because of these potential overrides, the - // toolbar cache should be invalidated any time the default or admin theme - // changes. - $variables['#cache']['tags'][] = 'config:system.theme'; - - // If Claro is the admin theme but not the active theme, still include Claro's - // toolbar preprocessing. - if (_system_is_claro_admin_and_not_active()) { - require_once DRUPAL_ROOT . '/core/themes/claro/claro.theme'; - claro_preprocess_toolbar($variables, $hook, $info); - } -} diff --git a/core/modules/system/templates/details.html.twig b/core/modules/system/templates/details.html.twig index 20e4ea7193e..dcb1cf354ce 100644 --- a/core/modules/system/templates/details.html.twig +++ b/core/modules/system/templates/details.html.twig @@ -34,7 +34,11 @@ </div> {% endif %} - {{ description }} + {%- if description -%} + {% set description_attributes = create_attribute({id: attributes['aria-describedby']}) %} + <div{{ description_attributes }}>{{ description }}</div> + {%- endif -%} + {{ children }} {{ value }} </details> diff --git a/core/modules/system/templates/image.html.twig b/core/modules/system/templates/image.html.twig index 6411eaa3d07..1f6d19d6c3e 100644 --- a/core/modules/system/templates/image.html.twig +++ b/core/modules/system/templates/image.html.twig @@ -7,7 +7,7 @@ * - attributes: HTML attributes for the img tag. * - style_name: (optional) The name of the image style applied. * - * @see template_preprocess_image() + * @see \Drupal\Core\Theme\ImagePreprocess::preprocessImage() * * @ingroup themeable */ diff --git a/core/modules/system/templates/install-page.html.twig b/core/modules/system/templates/install-page.html.twig index f6091fd3b95..d9144e6a154 100644 --- a/core/modules/system/templates/install-page.html.twig +++ b/core/modules/system/templates/install-page.html.twig @@ -6,7 +6,7 @@ * All available variables are mirrored in page.html.twig. * Some may be blank but they are provided for consistency. * - * @see template_preprocess_install_page() + * @see \Drupal\Core\Theme\ThemePreprocess::preprocessInstallPage() * * @ingroup themeable */ diff --git a/core/modules/system/templates/item-list.html.twig b/core/modules/system/templates/item-list.html.twig index 1462cf41ae0..c2babdab978 100644 --- a/core/modules/system/templates/item-list.html.twig +++ b/core/modules/system/templates/item-list.html.twig @@ -16,7 +16,7 @@ * - context: A list of contextual data associated with the list. May contain: * - list_style: The custom list style. * - * @see template_preprocess_item_list() + * @see \Drupal\Core\Theme\ThemePreprocess::preprocessItemList() * * @ingroup themeable */ diff --git a/core/modules/system/templates/maintenance-page.html.twig b/core/modules/system/templates/maintenance-page.html.twig index 748ed5a3aa4..06fb6065f7a 100644 --- a/core/modules/system/templates/maintenance-page.html.twig +++ b/core/modules/system/templates/maintenance-page.html.twig @@ -6,7 +6,7 @@ * All available variables are mirrored in page.html.twig. * Some may be blank but they are provided for consistency. * - * @see template_preprocess_maintenance_page() + * @see \Drupal\Core\Theme\ThemePreprocess::preprocessMaintenancePage() * * @ingroup themeable */ diff --git a/core/modules/system/templates/menu-local-action.html.twig b/core/modules/system/templates/menu-local-action.html.twig index 0eb03a9534a..e0280d5fcbc 100644 --- a/core/modules/system/templates/menu-local-action.html.twig +++ b/core/modules/system/templates/menu-local-action.html.twig @@ -7,7 +7,7 @@ * - attributes: HTML attributes for the wrapper element. * - link: A rendered link element. * - * @see template_preprocess_menu_local_action() + * @see \Drupal\Core\Menu\MenuPreprocess::preprocessMenuLocalAction() * * @ingroup themeable */ diff --git a/core/modules/system/templates/menu-local-task.html.twig b/core/modules/system/templates/menu-local-task.html.twig index ec02a8d530c..b2a743940a7 100644 --- a/core/modules/system/templates/menu-local-task.html.twig +++ b/core/modules/system/templates/menu-local-task.html.twig @@ -11,7 +11,7 @@ * Note: This template renders the content for each task item in * menu-local-tasks.html.twig. * - * @see template_preprocess_menu_local_task() + * @see \Drupal\Core\Menu\MenuPreprocess::preprocessMenuLocalTask() * * @ingroup themeable */ diff --git a/core/modules/system/templates/pager.html.twig b/core/modules/system/templates/pager.html.twig index 199f0578dbd..75047c1b95f 100644 --- a/core/modules/system/templates/pager.html.twig +++ b/core/modules/system/templates/pager.html.twig @@ -28,7 +28,7 @@ * at the first page. * - next: Present if the visible list of pages ends before the last page. * - * @see template_preprocess_pager() + * @see \Drupal\Core\Pager\PagerPreprocess::preprocessPager() * * @ingroup themeable */ diff --git a/core/modules/system/templates/region.html.twig b/core/modules/system/templates/region.html.twig index 219e14b0a4b..ddcaaa192df 100644 --- a/core/modules/system/templates/region.html.twig +++ b/core/modules/system/templates/region.html.twig @@ -9,7 +9,7 @@ * - region: The name of the region variable as defined in the theme's * .info.yml file. * - * @see template_preprocess_region() + * @see \Drupal\Core\Theme\ThemePreprocess::preprocessRegion() * * @ingroup themeable */ diff --git a/core/modules/system/templates/table.html.twig b/core/modules/system/templates/table.html.twig index cfcb0bf976c..6a73cc1152a 100644 --- a/core/modules/system/templates/table.html.twig +++ b/core/modules/system/templates/table.html.twig @@ -38,7 +38,7 @@ * - no_striping: A boolean indicating that the row should receive no striping. * - header_columns: The number of columns in the header. * - * @see template_preprocess_table() + * @see \Drupal\Core\Theme\ThemePreprocess::preprocessTable() * * @ingroup themeable */ diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml index f9ca25544ab..95b7a1e4c0f 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml @@ -43,6 +43,13 @@ ajax_test.insert_links_inline_wrapper: requirements: _access: 'TRUE' +ajax_test.insert_links_table_wrapper: + path: '/ajax-test/insert-table-wrapper' + defaults: + _controller: '\Drupal\ajax_test\Controller\AjaxTestController::insertLinksTableWrapper' + requirements: + _access: 'TRUE' + ajax_test.dialog_close: path: '/ajax-test/dialog-close' defaults: diff --git a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php index aea5322ce2d..b0adea8e7c7 100644 --- a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php @@ -141,6 +141,36 @@ class AjaxTestController { } /** + * Returns a render array of links that directly Drupal.ajax(). + * + * @return array + * Renderable array of AJAX response contents. + */ + public function insertLinksTableWrapper(): array { + $build['links'] = [ + 'ajax_target' => [ + '#markup' => '<div class="ajax-target-wrapper"><table><tbody id="ajax-target"></tbody></table></div>', + ], + 'links' => [ + '#theme' => 'links', + '#attached' => ['library' => ['ajax_test/ajax_insert']], + ], + ]; + + $build['links']['links']['#links']['table-row'] = [ + 'title' => 'Link table-row', + 'url' => Url::fromRoute('ajax_test.ajax_render_types', ['type' => 'table-row']), + 'attributes' => [ + 'class' => ['ajax-insert'], + 'data-method' => 'html', + 'data-effect' => 'none', + ], + ]; + + return $build; + } + + /** * Returns a render array that will be rendered by AjaxRenderer. * * Verifies that the response incorporates JavaScript settings generated @@ -336,6 +366,7 @@ class AjaxTestController { 'comment-not-wrapped' => '<!-- COMMENT --><div class="comment-not-wrapped">comment-not-wrapped</div>', 'svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect x="0" y="0" height="10" width="10" fill="green"></rect></svg>', 'empty' => '', + 'table-row' => '<tr><td>table-row</td></tr>', ]; $render_multiple_root = [ 'mixed' => ' foo <!-- COMMENT --> foo bar<div class="a class"><p>some string</p></div> additional not wrapped strings, <!-- ANOTHER COMMENT --> <p>final string</p>', diff --git a/core/modules/system/tests/modules/batch_test/src/BatchTestCallbacks.php b/core/modules/system/tests/modules/batch_test/src/BatchTestCallbacks.php index 24da9ebff84..aa4dc2e1a29 100644 --- a/core/modules/system/tests/modules/batch_test/src/BatchTestCallbacks.php +++ b/core/modules/system/tests/modules/batch_test/src/BatchTestCallbacks.php @@ -111,7 +111,7 @@ class BatchTestCallbacks { // 'finished' callback. $batch_test_helper->stack("op 5 id $id"); $context['results'][5][] = $id; - // This test is to test finished > 1 + // This test is to test finished > 1. $context['finished'] = 3.14; } diff --git a/core/modules/system/tests/modules/container_initialize/container_initialize.info.yml b/core/modules/system/tests/modules/container_initialize/container_initialize.info.yml new file mode 100644 index 00000000000..46411d2ea54 --- /dev/null +++ b/core/modules/system/tests/modules/container_initialize/container_initialize.info.yml @@ -0,0 +1,5 @@ +name: 'Container initialize' +type: module +description: 'Support module for HookCollectorPass testing.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/container_initialize/container_initialize.module b/core/modules/system/tests/modules/container_initialize/container_initialize.module new file mode 100644 index 00000000000..5c8e0aff74e --- /dev/null +++ b/core/modules/system/tests/modules/container_initialize/container_initialize.module @@ -0,0 +1,10 @@ +<?php + +/** + * @file + * Used to test bare container calls in .module files. + */ + +declare(strict_types=1); + +\Drupal::getContainer()->getParameter('site.path'); diff --git a/core/modules/system/tests/modules/element_info_test/src/Element/Deprecated.php b/core/modules/system/tests/modules/element_info_test/src/Element/Deprecated.php index f54ad8f9a9a..c052c5a488f 100644 --- a/core/modules/system/tests/modules/element_info_test/src/Element/Deprecated.php +++ b/core/modules/system/tests/modules/element_info_test/src/Element/Deprecated.php @@ -17,7 +17,8 @@ class Deprecated extends RenderElementBase { * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + $elementInfoManager = \Drupal::service('plugin.manager.element_info'); + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); @trigger_error(__CLASS__ . ' is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3068104', E_USER_DEPRECATED); } diff --git a/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php b/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php index ea72bd033fb..53150495cb9 100644 --- a/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php +++ b/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php @@ -4,8 +4,9 @@ declare(strict_types=1); namespace Drupal\element_info_test\Hook; -use Drupal\element_info_test\ElementInfoTestNumberBuilder; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\element_info_test\ElementInfoTestNumberBuilder; +use Drupal\element_info_test\Render\Element\Details; /** * Hook implementations for element_info_test. @@ -30,6 +31,9 @@ class ElementInfoTestHooks { if (\Drupal::state()->get('hook_element_plugin_alter:remove_weight', FALSE)) { unset($definitions['weight']); } + + $definitions['details']['class'] = Details::class; + $definitions['details']['provider'] = 'element_info_test'; } } diff --git a/core/modules/system/tests/modules/element_info_test/src/Render/Element/Details.php b/core/modules/system/tests/modules/element_info_test/src/Render/Element/Details.php new file mode 100644 index 00000000000..8e4d84558ca --- /dev/null +++ b/core/modules/system/tests/modules/element_info_test/src/Render/Element/Details.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\element_info_test\Render\Element; + +use Drupal\Core\Render\Attribute\RenderElement; +use Drupal\Core\Render\Element; +use Drupal\Core\Render\Element\RenderElementBase; + +/** + * Provides a render element for a details element. + * + * Properties: + * + * @property $title + * The title of the details container. Defaults to "Details". + * @property $open + * Indicates whether the container should be open by default. + * Defaults to FALSE. + * @property $custom + * Confirm that this class has been swapped properly. + * @property $summary_attributes + * An array of attributes to apply to the <summary> + * element. + */ +#[RenderElement('details')] +class Details extends RenderElementBase { + + /** + * {@inheritdoc} + */ + public function getInfo(): array { + return [ + '#open' => FALSE, + '#summary_attributes' => [], + '#custom' => 'Custom', + ]; + } + + /** + * Adds form element theming to details. + * + * @param array $element + * An associative array containing the properties and children of the + * details. + * + * @return array + * The modified element. + */ + public static function preRenderDetails($element): array { + Element::setAttributes($element, ['custom']); + + return $element; + } + +} diff --git a/core/modules/system/tests/modules/experimental_module_requirements_test/experimental_module_requirements_test.install b/core/modules/system/tests/modules/experimental_module_requirements_test/experimental_module_requirements_test.install deleted file mode 100644 index 483a1d01717..00000000000 --- a/core/modules/system/tests/modules/experimental_module_requirements_test/experimental_module_requirements_test.install +++ /dev/null @@ -1,24 +0,0 @@ -<?php - -/** - * @file - * Experimental Test Requirements module to test hook_requirements(). - */ - -declare(strict_types=1); - -use Drupal\Core\Extension\Requirement\RequirementSeverity; - -/** - * Implements hook_requirements(). - */ -function experimental_module_requirements_test_requirements(): array { - $requirements = []; - if (\Drupal::state()->get('experimental_module_requirements_test_requirements', FALSE)) { - $requirements['experimental_module_requirements_test_requirements'] = [ - 'severity' => RequirementSeverity::Error, - 'description' => t('The Experimental Test Requirements module can not be installed.'), - ]; - } - return $requirements; -} diff --git a/core/modules/system/tests/modules/experimental_module_requirements_test/src/Install/Requirements/ExperimentalModuleRequirementsTestRequirements.php b/core/modules/system/tests/modules/experimental_module_requirements_test/src/Install/Requirements/ExperimentalModuleRequirementsTestRequirements.php new file mode 100644 index 00000000000..53834f77c0e --- /dev/null +++ b/core/modules/system/tests/modules/experimental_module_requirements_test/src/Install/Requirements/ExperimentalModuleRequirementsTestRequirements.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\experimental_module_requirements_test\Install\Requirements; + +use Drupal\Core\Extension\InstallRequirementsInterface; +use Drupal\Core\Extension\Requirement\RequirementSeverity; + +/** + * Install time requirements for the Experimental Requirements Test module. + */ +class ExperimentalModuleRequirementsTestRequirements implements InstallRequirementsInterface { + + /** + * {@inheritdoc} + */ + public static function getRequirements(): array { + $requirements = []; + if (\Drupal::state()->get('experimental_module_requirements_test_requirements', FALSE)) { + $requirements['experimental_module_requirements_test_requirements'] = [ + 'severity' => RequirementSeverity::Error, + 'description' => t('The Experimental Test Requirements module can not be installed.'), + ]; + } + return $requirements; + } + +} diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php index 9babda83ddc..226eb705802 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php @@ -48,6 +48,11 @@ class FormTestGroupDetailsForm extends FormBase { 'data-summary-attribute' => 'test', ], ]; + $form['description_attributes'] = [ + '#type' => 'details', + '#title' => 'Details element with description', + '#description' => 'I am a details description', + ]; return $form; } diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestOptionalContainerForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestOptionalContainerForm.php index 62157258cea..314d4675182 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/FormTestOptionalContainerForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestOptionalContainerForm.php @@ -37,7 +37,7 @@ class FormTestOptionalContainerForm extends FormBase { '#optional' => FALSE, ]; - // Non-empty containers + // Non-empty containers. $form['nonempty_optional'] = [ '#type' => 'container', '#attributes' => ['class' => ['nonempty_optional']], diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestStorageForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestStorageForm.php index 637399b3638..cde0058a5db 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/FormTestStorageForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestStorageForm.php @@ -34,7 +34,7 @@ class FormTestStorageForm extends FormBase { if ($form_state->isRebuilding()) { $form_state->setUserInput([]); } - // Initialize + // Initialize. $storage = $form_state->getStorage(); $session = $this->getRequest()->getSession(); if (empty($storage)) { @@ -42,7 +42,7 @@ class FormTestStorageForm extends FormBase { if (empty($user_input)) { $session->set('constructions', 0); } - // Put the initial thing into the storage + // Put the initial thing into the storage. $storage = [ 'thing' => [ 'title' => 'none', diff --git a/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php index 6de8e4ad185..b6b54647efa 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php @@ -446,7 +446,7 @@ class JavascriptStatesForm extends FormBase { '#title' => 'Textfield in details', ]; - // Select trigger + // Select trigger. $form['header_select'] = [ '#type' => 'html_tag', '#tag' => 'h3', diff --git a/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php b/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php index cda2b92b347..441ebaa1d12 100644 --- a/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php +++ b/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php @@ -6,6 +6,8 @@ namespace Drupal\form_test\Hook; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Render\Element\Submit; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\form_test\Callbacks; @@ -16,6 +18,8 @@ class FormTestHooks { use StringTranslationTrait; + public function __construct(protected ElementInfoManagerInterface $elementInfoManager) {} + /** * Implements hook_form_FORM_ID_alter(). */ @@ -55,13 +59,10 @@ class FormTestHooks { */ #[Hook('form_user_register_form_alter')] public function formUserRegisterFormAlter(&$form, FormStateInterface $form_state) : void { - $form['test_rebuild'] = [ - '#type' => 'submit', - '#value' => $this->t('Rebuild'), - '#submit' => [ - [Callbacks::class, 'userRegisterFormRebuild'], - ], - ]; + $submit = $this->elementInfoManager->fromRenderable($form) + ->createChild('test_rebuild', Submit::class); + $submit->value = $this->t('Rebuild'); + $submit->submit = [[Callbacks::class, 'userRegisterFormRebuild']]; } /** @@ -69,11 +70,12 @@ class FormTestHooks { */ #[Hook('form_form_test_vertical_tabs_access_form_alter')] public function formFormTestVerticalTabsAccessFormAlter(&$form, &$form_state, $form_id) : void { - $form['vertical_tabs1']['#access'] = FALSE; - $form['vertical_tabs2']['#access'] = FALSE; - $form['tabs3']['#access'] = TRUE; - $form['fieldset1']['#access'] = FALSE; - $form['container']['#access'] = FALSE; + $element_object = $this->elementInfoManager->fromRenderable($form); + $element_object->getChild('vertical_tabs1')->access = FALSE; + $element_object->getChild('vertical_tabs2')->access = FALSE; + $element_object->getChild('tab3')->access = FALSE; + $element_object->getChild('fieldset1')->access = FALSE; + $element_object->getChild('container')->access = FALSE; } } diff --git a/core/modules/system/tests/modules/icon_test/config/schema/icon_test.schema.yml b/core/modules/system/tests/modules/icon_test/config/schema/icon_test.schema.yml index 113b165e50e..ed96b94968d 100644 --- a/core/modules/system/tests/modules/icon_test/config/schema/icon_test.schema.yml +++ b/core/modules/system/tests/modules/icon_test/config/schema/icon_test.schema.yml @@ -8,14 +8,12 @@ icon.icon_pack_options.test_path: type: integer label: 'Width' constraints: - Range: - min: 0 + PositiveOrZero: ~ height: type: integer label: 'Height' constraints: - Range: - min: 0 + PositiveOrZero: ~ icon.icon_pack_options.test_svg: type: mapping @@ -27,8 +25,7 @@ icon.icon_pack_options.test_svg: type: integer label: 'Size' constraints: - Range: - min: 0 + PositiveOrZero: ~ icon.icon_pack_options.test_svg_sprite: type: mapping @@ -40,14 +37,12 @@ icon.icon_pack_options.test_svg_sprite: type: integer label: 'Width' constraints: - Range: - min: 0 + PositiveOrZero: ~ height: type: integer label: 'Height' constraints: - Range: - min: 0 + PositiveOrZero: ~ alt: type: label label: 'Alt' @@ -62,14 +57,12 @@ icon.icon_pack_options.test_settings: type: integer label: 'Width' constraints: - Range: - min: 0 + PositiveOrZero: ~ height: type: integer label: 'Height' constraints: - Range: - min: 0 + PositiveOrZero: ~ title: type: label label: 'Title' diff --git a/core/modules/system/tests/modules/layout_test/layout_test.module b/core/modules/system/tests/modules/layout_test/layout_test.module deleted file mode 100644 index 18459369b0a..00000000000 --- a/core/modules/system/tests/modules/layout_test/layout_test.module +++ /dev/null @@ -1,15 +0,0 @@ -<?php - -/** - * @file - * Provides hook implementations for Layout Test. - */ - -declare(strict_types=1); - -/** - * Implements hook_preprocess_HOOK() for layout templates. - */ -function template_preprocess_layout_test_2col(&$variables): void { - $variables['region_attributes']['left']->addClass('class-added-by-preprocess'); -} diff --git a/core/modules/system/tests/modules/layout_test/src/Hook/LayoutTestThemeHooks.php b/core/modules/system/tests/modules/layout_test/src/Hook/LayoutTestThemeHooks.php new file mode 100644 index 00000000000..35774e5de4b --- /dev/null +++ b/core/modules/system/tests/modules/layout_test/src/Hook/LayoutTestThemeHooks.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\layout_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for layout_test. + */ +class LayoutTestThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for layout templates. + */ + #[Hook('preprocess_layout_test_2col')] + public function templatePreprocessLayoutTest2col(&$variables): void { + $variables['region_attributes']['left']->addClass('class-added-by-preprocess'); + } + +} diff --git a/core/modules/system/tests/modules/module_test/module_test.file.inc b/core/modules/system/tests/modules/module_test/module_test.file.inc deleted file mode 100644 index 5ba5f5614ca..00000000000 --- a/core/modules/system/tests/modules/module_test/module_test.file.inc +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -/** - * @file - * Install, update and uninstall functions for the module_test module. - * - * Provides a hook to test \Drupal::moduleHandler()->getImplementationInfo() - * loading includes. - */ - -declare(strict_types=1); - -/** - * Implements hook_test_hook(). - */ -function module_test_test_hook(): array { - return ['module_test' => 'success!']; -} - -/** - * Implements hook_test_reset_implementations_hook(). - */ -function module_test_test_reset_implementations_hook(): string { - return __FUNCTION__; -} - -/** - * Implements hook_test_reset_implementations_alter(). - */ -function module_test_test_reset_implementations_alter(array &$data): void { - $data[] = __FUNCTION__; -} diff --git a/core/modules/system/tests/modules/module_test/module_test.module b/core/modules/system/tests/modules/module_test/module_test.module index 35561f09c8a..b3f3e131a58 100644 --- a/core/modules/system/tests/modules/module_test/module_test.module +++ b/core/modules/system/tests/modules/module_test/module_test.module @@ -7,57 +7,6 @@ declare(strict_types=1); -use Drupal\Core\Extension\Extension; - -/** - * Implements hook_system_info_alter(). - * - * Manipulate module dependencies to test dependency chains. - */ -function module_test_system_info_alter(&$info, Extension $file, $type): void { - if (\Drupal::state()->get('module_test.dependency') == 'missing dependency') { - if ($file->getName() == 'dblog') { - // Make dblog module depend on config. - $info['dependencies'][] = 'config'; - } - elseif ($file->getName() == 'config') { - // Make config module depend on a non-existing module. - $info['dependencies'][] = 'foo'; - } - } - elseif (\Drupal::state()->get('module_test.dependency') == 'dependency') { - if ($file->getName() == 'dblog') { - // Make dblog module depend on config. - $info['dependencies'][] = 'config'; - } - elseif ($file->getName() == 'config') { - // Make config module depend on help module. - $info['dependencies'][] = 'help'; - } - elseif ($file->getName() == 'entity_test') { - // Make entity test module depend on help module. - $info['dependencies'][] = 'help'; - } - } - elseif (\Drupal::state()->get('module_test.dependency') == 'version dependency') { - if ($file->getName() == 'dblog') { - // Make dblog module depend on config. - $info['dependencies'][] = 'config'; - } - elseif ($file->getName() == 'config') { - // Make config module depend on a specific version of help module. - $info['dependencies'][] = 'help (1.x)'; - } - elseif ($file->getName() == 'help') { - // Set help module to a version compatible with the above. - $info['version'] = '8.x-1.0'; - } - } - if ($file->getName() == 'stark' && $type == 'theme') { - $info['regions']['test_region'] = 'Test region'; - } -} - /** * Implements hook_hook_info(). */ @@ -77,21 +26,3 @@ function module_test_load($param) { $result = \Drupal::moduleHandler()->invokeAll('test_hook'); return $result[$param]; } - -/** - * Implements hook_modules_installed(). - */ -function module_test_modules_installed($modules): void { - // Record the ordered list of modules that were passed in to this hook so we - // can check that the modules were enabled in the correct sequence. - \Drupal::state()->set('module_test.install_order', $modules); -} - -/** - * Implements hook_modules_uninstalled(). - */ -function module_test_modules_uninstalled($modules): void { - // Record the ordered list of modules that were passed in to this hook so we - // can check that the modules were uninstalled in the correct sequence. - \Drupal::state()->set('module_test.uninstall_order', $modules); -} diff --git a/core/modules/system/tests/modules/module_test/src/Hook/ModuleTestFileThemeHooks.php b/core/modules/system/tests/modules/module_test/src/Hook/ModuleTestFileThemeHooks.php new file mode 100644 index 00000000000..381a9458260 --- /dev/null +++ b/core/modules/system/tests/modules/module_test/src/Hook/ModuleTestFileThemeHooks.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\module_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for module_test. + */ +class ModuleTestFileThemeHooks { + + /** + * Implements hook_test_hook(). + */ + #[Hook('test_hook')] + public function testHook(): array { + return [ + 'module_test' => 'success!', + ]; + } + + /** + * Implements hook_test_reset_implementations_hook(). + */ + #[Hook('test_reset_implementations_hook')] + public function testResetImplementationsHook(): string { + return 'module_test_test_reset_implementations_hook'; + } + + /** + * Implements hook_test_reset_implementations_alter(). + */ + #[Hook('test_reset_implementations_alter')] + public function testResetImplementationsAlter(array &$data): void { + $data[] = 'module_test_test_reset_implementations_alter'; + } + +} diff --git a/core/modules/system/tests/modules/module_test/src/Hook/ModuleTestThemeHooks.php b/core/modules/system/tests/modules/module_test/src/Hook/ModuleTestThemeHooks.php new file mode 100644 index 00000000000..a34adbe620c --- /dev/null +++ b/core/modules/system/tests/modules/module_test/src/Hook/ModuleTestThemeHooks.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\module_test\Hook; + +use Drupal\Core\Extension\Extension; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for module_test. + */ +class ModuleTestThemeHooks { + + /** + * Implements hook_system_info_alter(). + * + * Manipulate module dependencies to test dependency chains. + */ + #[Hook('system_info_alter')] + public function systemInfoAlter(&$info, Extension $file, $type): void { + if (\Drupal::state()->get('module_test.dependency') == 'missing dependency') { + if ($file->getName() == 'dblog') { + // Make dblog module depend on config. + $info['dependencies'][] = 'config'; + } + elseif ($file->getName() == 'config') { + // Make config module depend on a non-existing module. + $info['dependencies'][] = 'foo'; + } + } + elseif (\Drupal::state()->get('module_test.dependency') == 'dependency') { + if ($file->getName() == 'dblog') { + // Make dblog module depend on config. + $info['dependencies'][] = 'config'; + } + elseif ($file->getName() == 'config') { + // Make config module depend on help module. + $info['dependencies'][] = 'help'; + } + elseif ($file->getName() == 'entity_test') { + // Make entity test module depend on help module. + $info['dependencies'][] = 'help'; + } + } + elseif (\Drupal::state()->get('module_test.dependency') == 'version dependency') { + if ($file->getName() == 'dblog') { + // Make dblog module depend on config. + $info['dependencies'][] = 'config'; + } + elseif ($file->getName() == 'config') { + // Make config module depend on a specific version of help module. + $info['dependencies'][] = 'help (1.x)'; + } + elseif ($file->getName() == 'help') { + // Set help module to a version compatible with the above. + $info['version'] = '8.x-1.0'; + } + } + if ($file->getName() == 'stark' && $type == 'theme') { + $info['regions']['test_region'] = 'Test region'; + } + } + + /** + * Implements hook_modules_installed(). + */ + #[Hook('modules_installed')] + public function modulesInstalled($modules): void { + // Record the ordered list of modules that were passed in to this hook so we + // can check that the modules were enabled in the correct sequence. + \Drupal::state()->set('module_test.install_order', $modules); + } + + /** + * Implements hook_modules_uninstalled(). + */ + #[Hook('modules_uninstalled')] + public function modulesUninstalled($modules): void { + // Record the ordered list of modules that were passed in to this hook so we + // can check that the modules were uninstalled in the correct sequence. + \Drupal::state()->set('module_test.uninstall_order', $modules); + } + +} diff --git a/core/modules/system/tests/modules/olivero_test/olivero_test.module b/core/modules/system/tests/modules/olivero_test/olivero_test.module deleted file mode 100644 index 5d697759254..00000000000 --- a/core/modules/system/tests/modules/olivero_test/olivero_test.module +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -/** - * @file - * Functions to support testing the Olivero theme. - */ - -declare(strict_types=1); - -/** - * Implements hook_preprocess_field_multiple_value_form(). - */ -function olivero_test_preprocess_field_multiple_value_form(&$variables): void { - // Set test multiple value form field to disabled - if ($variables["element"]["#field_name"] === "field_multiple_value_form_field") { - $variables['element']['#disabled'] = TRUE; - } -} - -/** - * Implements hook_preprocess_html(). - */ -function olivero_test_preprocess_html(&$variables): void { - $variables['#attached']['library'][] = 'olivero_test/log-errors'; -} diff --git a/core/modules/system/tests/modules/olivero_test/src/Hook/OliveroTestThemeHooks.php b/core/modules/system/tests/modules/olivero_test/src/Hook/OliveroTestThemeHooks.php new file mode 100644 index 00000000000..1a5f7247a74 --- /dev/null +++ b/core/modules/system/tests/modules/olivero_test/src/Hook/OliveroTestThemeHooks.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\olivero_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for olivero_test. + */ +class OliveroTestThemeHooks { + + /** + * Implements hook_preprocess_field_multiple_value_form(). + */ + #[Hook('preprocess_field_multiple_value_form')] + public function preprocessFieldMultipleValueForm(&$variables): void { + // Set test multiple value form field to disabled. + if ($variables["element"]["#field_name"] === "field_multiple_value_form_field") { + $variables['element']['#disabled'] = TRUE; + } + } + + /** + * Implements hook_preprocess_html(). + */ + #[Hook('preprocess_html')] + public function preprocessHtml(&$variables): void { + $variables['#attached']['library'][] = 'olivero_test/log-errors'; + } + +} diff --git a/core/modules/system/tests/modules/pager_test/pager_test.module b/core/modules/system/tests/modules/pager_test/pager_test.module deleted file mode 100644 index e4556eddba3..00000000000 --- a/core/modules/system/tests/modules/pager_test/pager_test.module +++ /dev/null @@ -1,42 +0,0 @@ -<?php - -/** - * @file - * Hook implementations for this module. - */ - -declare(strict_types=1); - -/** - * Implements hook_preprocess_HOOK(). - */ -function pager_test_preprocess_pager(&$variables): void { - // Nothing to do if there is only one page. - $element = $variables['pager']['#element']; - /** @var \Drupal\Core\Pager\PagerManagerInterface $pager_manager */ - $pager_manager = \Drupal::service('pager.manager'); - $pager = $pager_manager->getPager($element); - - // Nothing to do if there is no pager. - if (!isset($pager)) { - return; - } - - // Nothing to do if there is only one page. - if ($pager->getTotalPages() <= 1) { - return; - } - - foreach ($variables['items']['pages'] as &$pager_item) { - $pager_item['attributes']['pager-test'] = 'yes'; - $pager_item['attributes']->addClass('lizards'); - } - unset($pager_item); - - foreach (['first', 'previous', 'next', 'last'] as $special_pager_item) { - if (isset($variables['items'][$special_pager_item])) { - $variables['items'][$special_pager_item]['attributes']->addClass('lizards'); - $variables['items'][$special_pager_item]['attributes']['pager-test'] = $special_pager_item; - } - } -} diff --git a/core/modules/system/tests/modules/pager_test/src/Hook/PagerTestThemeHooks.php b/core/modules/system/tests/modules/pager_test/src/Hook/PagerTestThemeHooks.php new file mode 100644 index 00000000000..ae72036a552 --- /dev/null +++ b/core/modules/system/tests/modules/pager_test/src/Hook/PagerTestThemeHooks.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\pager_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for pager_test. + */ +class PagerTestThemeHooks { + + /** + * Implements hook_preprocess_HOOK(). + */ + #[Hook('preprocess_pager')] + public function preprocessPager(&$variables): void { + // Nothing to do if there is only one page. + $element = $variables['pager']['#element']; + /** @var \Drupal\Core\Pager\PagerManagerInterface $pager_manager */ + $pager_manager = \Drupal::service('pager.manager'); + $pager = $pager_manager->getPager($element); + // Nothing to do if there is no pager. + if (!isset($pager)) { + return; + } + // Nothing to do if there is only one page. + if ($pager->getTotalPages() <= 1) { + return; + } + foreach ($variables['items']['pages'] as &$pager_item) { + $pager_item['attributes']['pager-test'] = 'yes'; + $pager_item['attributes']->addClass('lizards'); + } + unset($pager_item); + foreach ([ + 'first', + 'previous', + 'next', + 'last', + ] as $special_pager_item) { + if (isset($variables['items'][$special_pager_item])) { + $variables['items'][$special_pager_item]['attributes']->addClass('lizards'); + $variables['items'][$special_pager_item]['attributes']['pager-test'] = $special_pager_item; + } + } + } + +} diff --git a/core/modules/system/tests/modules/router_test_directory/router_test.module b/core/modules/system/tests/modules/router_test_directory/router_test.module deleted file mode 100644 index 2158075059c..00000000000 --- a/core/modules/system/tests/modules/router_test_directory/router_test.module +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -/** - * @file - * Test module. - */ - -declare(strict_types=1); - -use Drupal\Core\Url; - -/** - * Implements hook_preprocess_HOOK(). - * - * Performs an operation that calls the RouteProvider's collection method - * during an exception page view. (which is rendered during a subrequest.) - * - * @see \Drupal\FunctionalTests\Routing\RouteCachingQueryAlteredTest - */ -function router_test_preprocess_page(&$variables): void { - $request = \Drupal::request(); - if ($request->getPathInfo() === '/router-test/rejects-query-strings') { - // Create a URL from the request, e.g. for a breadcrumb or other contextual - // information. - Url::createFromRequest($request); - } -} diff --git a/core/modules/system/tests/modules/router_test_directory/src/Hook/RouterTestThemeHooks.php b/core/modules/system/tests/modules/router_test_directory/src/Hook/RouterTestThemeHooks.php new file mode 100644 index 00000000000..da642b8b13b --- /dev/null +++ b/core/modules/system/tests/modules/router_test_directory/src/Hook/RouterTestThemeHooks.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\router_test\Hook; + +use Drupal\Core\Url; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for router_test. + */ +class RouterTestThemeHooks { + + /** + * Implements hook_preprocess_HOOK(). + * + * Performs an operation that calls the RouteProvider's collection method + * during an exception page view. (which is rendered during a subrequest.) + * + * @see \Drupal\FunctionalTests\Routing\RouteCachingQueryAlteredTest + */ + #[Hook('preprocess_page')] + public function preprocessPage(&$variables): void { + $request = \Drupal::request(); + if ($request->getPathInfo() === '/router-test/rejects-query-strings') { + // Create a URL from the request, e.g. for a breadcrumb or other contextual + // information. + Url::createFromRequest($request); + } + } + +} diff --git a/core/modules/system/tests/modules/system_test/src/Controller/OptionalServiceSystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/OptionalServiceSystemTestController.php new file mode 100644 index 00000000000..b57e4c88397 --- /dev/null +++ b/core/modules/system/tests/modules/system_test/src/Controller/OptionalServiceSystemTestController.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\system_test\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\dblog\Logger\DbLog; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * A controller that specifies an optional dependency. + */ +class OptionalServiceSystemTestController extends ControllerBase { + + public function __construct( + #[Autowire('logger.dblog')] + public readonly ?DbLog $dbLog, + ) {} + +} diff --git a/core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php b/core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php index 9045a4c8b9c..9027509a19c 100644 --- a/core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php +++ b/core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php @@ -23,6 +23,26 @@ final class HtmxTestAttachmentsController extends ControllerBase { } /** + * Builds a response with a `beforebegin` swap. + * + * @return mixed[] + * A render array. + */ + public function before(): array { + return self::generateHtmxButton('beforebegin'); + } + + /** + * Builds a response with an `afterend` swap.. + * + * @return mixed[] + * A render array. + */ + public function after(): array { + return self::generateHtmxButton('afterend'); + } + + /** * Builds the HTMX response. * * @return mixed[] @@ -44,12 +64,22 @@ final class HtmxTestAttachmentsController extends ControllerBase { } /** + * We need a static callback that ignores callback parameters. + * + * @return array + * The render array. + */ + public static function replaceWithAjax(): array { + return static::generateHtmxButton(); + } + + /** * Static helper to for reusable render array. * * @return array * The render array. */ - public static function generateHtmxButton(): array { + public static function generateHtmxButton(string $swap = ''): array { $url = Url::fromRoute('test_htmx.attachments.replace'); $build['replace'] = [ '#type' => 'html_tag', @@ -68,6 +98,9 @@ final class HtmxTestAttachmentsController extends ControllerBase { ], ], ]; + if ($swap !== '') { + $build['replace']['#attributes']['data-hx-swap'] = $swap; + } $build['content'] = [ '#type' => 'container', diff --git a/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php b/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php index 8fffbbc5f40..f812a99582d 100644 --- a/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php +++ b/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php @@ -32,7 +32,7 @@ class HtmxTestAjaxForm extends FormBase { '#ajax' => [ 'callback' => [ HtmxTestAttachmentsController::class, - 'generateHtmxButton', + 'replaceWithAjax', ], 'wrapper' => 'ajax-test-container', ], diff --git a/core/modules/system/tests/modules/test_htmx/test_htmx.routing.yml b/core/modules/system/tests/modules/test_htmx/test_htmx.routing.yml index 406c3027f3b..33dca377c71 100644 --- a/core/modules/system/tests/modules/test_htmx/test_htmx.routing.yml +++ b/core/modules/system/tests/modules/test_htmx/test_htmx.routing.yml @@ -6,6 +6,22 @@ test_htmx.attachments.page: requirements: _permission: 'access content' +test_htmx.attachments.before: + path: '/htmx-test-attachments/before' + defaults: + _title: 'Page' + _controller: '\Drupal\test_htmx\Controller\HtmxTestAttachmentsController::before' + requirements: + _permission: 'access content' + +test_htmx.attachments.after: + path: '/htmx-test-attachments/after' + defaults: + _title: 'Page' + _controller: '\Drupal\test_htmx\Controller\HtmxTestAttachmentsController::after' + requirements: + _permission: 'access content' + test_htmx.attachments.replace: path: '/htmx-test-attachments/replace' defaults: diff --git a/core/modules/system/tests/modules/theme_region_test/src/Hook/ThemeRegionTestThemeHooks.php b/core/modules/system/tests/modules/theme_region_test/src/Hook/ThemeRegionTestThemeHooks.php new file mode 100644 index 00000000000..5cff4c19531 --- /dev/null +++ b/core/modules/system/tests/modules/theme_region_test/src/Hook/ThemeRegionTestThemeHooks.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\theme_region_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for theme_region_test. + */ +class ThemeRegionTestThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for region templates. + */ + #[Hook('preprocess_region')] + public function preprocessRegion(&$variables): void { + if ($variables['region'] == 'sidebar_first') { + $variables['attributes']['class'][] = 'new_class'; + } + } + +} diff --git a/core/modules/system/tests/modules/theme_region_test/theme_region_test.module b/core/modules/system/tests/modules/theme_region_test/theme_region_test.module deleted file mode 100644 index 5e9dc670a47..00000000000 --- a/core/modules/system/tests/modules/theme_region_test/theme_region_test.module +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -/** - * @file - * Provides hook implementations for testing purposes. - */ - -declare(strict_types=1); - -/** - * Implements hook_preprocess_HOOK() for region templates. - */ -function theme_region_test_preprocess_region(&$variables): void { - if ($variables['region'] == 'sidebar_first') { - $variables['attributes']['class'][] = 'new_class'; - } -} diff --git a/core/modules/system/tests/modules/theme_test/src/EventSubscriber/ThemeTestSubscriber.php b/core/modules/system/tests/modules/theme_test/src/EventSubscriber/ThemeTestSubscriber.php index 6c6f67d77eb..1d953ff8c33 100644 --- a/core/modules/system/tests/modules/theme_test/src/EventSubscriber/ThemeTestSubscriber.php +++ b/core/modules/system/tests/modules/theme_test/src/EventSubscriber/ThemeTestSubscriber.php @@ -79,27 +79,10 @@ class ThemeTestSubscriber implements EventSubscriberInterface { } /** - * Ensures that the theme registry was not initialized. - */ - public function onView(RequestEvent $event) { - $current_route = $this->currentRouteMatch->getRouteName(); - $entity_autocomplete_route = [ - 'system.entity_autocomplete', - ]; - - if (in_array($current_route, $entity_autocomplete_route)) { - if ($this->container->initialized('theme.registry')) { - throw new \Exception('registry initialized'); - } - } - } - - /** * {@inheritdoc} */ public static function getSubscribedEvents(): array { $events[KernelEvents::REQUEST][] = ['onRequest']; - $events[KernelEvents::VIEW][] = ['onView', -1000]; return $events; } diff --git a/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks1.php b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks1.php new file mode 100644 index 00000000000..ddd0e7efc39 --- /dev/null +++ b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks1.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\theme_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for theme_test. + */ +class ThemeTestThemeHooks1 { + + /** + * Implements hook_preprocess_HOOK() for HTML document templates. + */ + #[Hook('preprocess_html')] + public function preprocessHtml(&$variables): void { + $variables['html_attributes']['theme_test_html_attribute'] = 'theme test html attribute value'; + $variables['attributes']['theme_test_body_attribute'] = 'theme test body attribute value'; + $variables['attributes']['theme_test_page_variable'] = 'Page variable is an array.'; + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_theme_test_preprocess_suggestions')] + public function themeSuggestionsThemeTestPreprocessSuggestions($variables): array { + return [ + 'theme_test_preprocess_suggestions__' . $variables['foo'], + ]; + } + + /** + * Implements hook_preprocess_HOOK(). + */ + #[Hook('preprocess_theme_test_preprocess_suggestions')] + public function preprocessThemeTestPreprocessSuggestions(&$variables): void { + $variables['foo'] = 'Theme hook implementor=theme_theme_test_preprocess_suggestions().'; + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_theme_test_suggestion_provided')] + public function themeSuggestionsThemeTestSuggestionProvided(array $variables): array { + return [ + 'theme_test_suggestion_provided__foo', + ]; + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_node')] + public function themeSuggestionsNode(array $variables): array { + $xss = '<script type="text/javascript">alert(\'yo\');</script>'; + $suggestions[] = 'node__' . $xss; + return $suggestions; + } + +} diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index 72c92352b61..b987fb0d40f 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -8,30 +8,6 @@ declare(strict_types=1); /** - * Implements hook_preprocess_HOOK() for HTML document templates. - */ -function theme_test_preprocess_html(&$variables): void { - $variables['html_attributes']['theme_test_html_attribute'] = 'theme test html attribute value'; - $variables['attributes']['theme_test_body_attribute'] = 'theme test body attribute value'; - - $variables['attributes']['theme_test_page_variable'] = 'Page variable is an array.'; -} - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function theme_test_theme_suggestions_theme_test_preprocess_suggestions($variables): array { - return ['theme_test_preprocess_suggestions__' . $variables['foo']]; -} - -/** - * Implements hook_preprocess_HOOK(). - */ -function theme_test_preprocess_theme_test_preprocess_suggestions(&$variables): void { - $variables['foo'] = 'Theme hook implementor=theme_theme_test_preprocess_suggestions().'; -} - -/** * Prepares variables for test render element templates. * * Default template: theme-test-render-element.html.twig. @@ -45,23 +21,6 @@ function template_preprocess_theme_test_render_element(&$variables): void { } /** - * Implements hook_theme_suggestions_HOOK(). - */ -function theme_test_theme_suggestions_theme_test_suggestion_provided(array $variables): array { - return ['theme_test_suggestion_provided__foo']; -} - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function theme_test_theme_suggestions_node(array $variables): array { - $xss = '<script type="text/javascript">alert(\'yo\');</script>'; - $suggestions[] = 'node__' . $xss; - - return $suggestions; -} - -/** * Implements template_preprocess_HOOK() for theme_test_registered_by_module. */ function template_preprocess_theme_test_registered_by_module(): void { diff --git a/core/modules/system/tests/src/Functional/Database/SelectTableSortDefaultTest.php b/core/modules/system/tests/src/Functional/Database/SelectTableSortDefaultTest.php index 60b7c12b5a1..72e95946a5d 100644 --- a/core/modules/system/tests/src/Functional/Database/SelectTableSortDefaultTest.php +++ b/core/modules/system/tests/src/Functional/Database/SelectTableSortDefaultTest.php @@ -28,7 +28,7 @@ class SelectTableSortDefaultTest extends DatabaseTestBase { ['field' => 'Task ID', 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'], ['field' => 'Task', 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'], ['field' => 'Task', 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'], - // More elements here + // More elements here. ]; @@ -56,7 +56,7 @@ class SelectTableSortDefaultTest extends DatabaseTestBase { ['field' => 'Task ID', 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'], ['field' => 'Task', 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'], ['field' => 'Task', 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'], - // More elements here + // More elements here. ]; diff --git a/core/modules/system/tests/src/Functional/Datetime/DrupalDateTimeTest.php b/core/modules/system/tests/src/Functional/Datetime/DrupalDateTimeTest.php deleted file mode 100644 index cbef10d726e..00000000000 --- a/core/modules/system/tests/src/Functional/Datetime/DrupalDateTimeTest.php +++ /dev/null @@ -1,108 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\system\Functional\Datetime; - -use Drupal\Core\Datetime\DrupalDateTime; -use Drupal\Tests\BrowserTestBase; -use Drupal\user\Entity\User; - -/** - * Tests DrupalDateTime functionality. - * - * @group Datetime - */ -class DrupalDateTimeTest extends BrowserTestBase { - - /** - * Set up required modules. - * - * @var string[] - */ - protected static $modules = []; - - /** - * {@inheritdoc} - */ - protected $defaultTheme = 'stark'; - - /** - * Tests that DrupalDateTime can detect the right timezone to use. - * - * Test with a variety of less commonly used timezone names to - * help ensure that the system timezone will be different than the - * stated timezones. - */ - public function testDateTimezone(): void { - $date_string = '2007-01-31 21:00:00'; - - // Make sure no site timezone has been set. - $this->config('system.date') - ->set('timezone.user.configurable', 0) - ->set('timezone.default', NULL) - ->save(); - - // Detect the system timezone. - $system_timezone = date_default_timezone_get(); - - // Create a date object with an unspecified timezone, which should - // end up using the system timezone. - $date = new DrupalDateTime($date_string); - $timezone = $date->getTimezone()->getName(); - $this->assertSame($system_timezone, $timezone, 'DrupalDateTime uses the system timezone when there is no site timezone.'); - - // Create a date object with a specified timezone. - $date = new DrupalDateTime($date_string, 'America/Yellowknife'); - $timezone = $date->getTimezone()->getName(); - $this->assertSame('America/Yellowknife', $timezone, 'DrupalDateTime uses the specified timezone if provided.'); - - // Set a site timezone. - $this->config('system.date')->set('timezone.default', 'Europe/Warsaw')->save(); - - // Create a date object with an unspecified timezone, which should - // end up using the site timezone. - $date = new DrupalDateTime($date_string); - $timezone = $date->getTimezone()->getName(); - $this->assertSame('Europe/Warsaw', $timezone, 'DrupalDateTime uses the site timezone if provided.'); - - // Create user. - $this->config('system.date')->set('timezone.user.configurable', 1)->save(); - $test_user = $this->drupalCreateUser([]); - $this->drupalLogin($test_user); - - // Set up the user with a different timezone than the site. - $edit = ['mail' => $test_user->getEmail(), 'timezone' => 'Asia/Manila']; - $this->drupalGet('user/' . $test_user->id() . '/edit'); - $this->submitForm($edit, 'Save'); - - // Reload the user and reset the timezone in AccountProxy::setAccount(). - \Drupal::entityTypeManager()->getStorage('user')->resetCache(); - $this->container->get('current_user')->setAccount(User::load($test_user->id())); - - // Create a date object with an unspecified timezone, which should - // end up using the user timezone. - $date = new DrupalDateTime($date_string); - $timezone = $date->getTimezone()->getName(); - $this->assertSame('Asia/Manila', $timezone, 'DrupalDateTime uses the user timezone, if configurable timezones are used and it is set.'); - } - - /** - * Tests the ability to override the time zone in the format method. - */ - public function testTimezoneFormat(): void { - // Create a date in UTC - $date = DrupalDateTime::createFromTimestamp(87654321, 'UTC'); - - // Verify that the date format method displays the default time zone. - $this->assertEquals('1972/10/11 12:25:21 UTC', $date->format('Y/m/d H:i:s e'), 'Date has default UTC time zone and correct date/time.'); - - // Verify that the format method can override the time zone. - $this->assertEquals('1972/10/11 08:25:21 America/New_York', $date->format('Y/m/d H:i:s e', ['timezone' => 'America/New_York']), 'Date displayed overridden time zone and correct date/time'); - - // Verify that the date format method still displays the default time zone - // for the date object. - $this->assertEquals('1972/10/11 12:25:21 UTC', $date->format('Y/m/d H:i:s e'), 'Date still has default UTC time zone and correct date/time'); - } - -} diff --git a/core/modules/system/tests/src/Functional/Entity/EntityAddUITest.php b/core/modules/system/tests/src/Functional/Entity/EntityAddUITest.php index 77eaa48575b..f570d803176 100644 --- a/core/modules/system/tests/src/Functional/Entity/EntityAddUITest.php +++ b/core/modules/system/tests/src/Functional/Entity/EntityAddUITest.php @@ -62,20 +62,23 @@ class EntityAddUITest extends BrowserTestBase { $this->drupalGet('/entity_test_with_bundle/add'); $this->assertSession()->addressEquals('/entity_test_with_bundle/add/test'); - // Two bundles exist, confirm both are shown. + // Two bundles exist. Confirm both are shown and that they are ordered + // alphabetically by their labels, not by their IDs. EntityTestBundle::create([ 'id' => 'test2', - 'label' => 'Test2 label', + 'label' => 'Aaa Test2 label', 'description' => 'My test2 description', ])->save(); $this->drupalGet('/entity_test_with_bundle/add'); $this->assertSession()->linkExists('Test label'); - $this->assertSession()->linkExists('Test2 label'); + $this->assertSession()->linkExists('Aaa Test2 label'); $this->assertSession()->pageTextContains('My test description'); $this->assertSession()->pageTextContains('My test2 description'); - $this->clickLink('Test2 label'); + $this->assertSession()->pageTextMatches('/Aaa Test2 label(.*)Test label/'); + + $this->clickLink('Aaa Test2 label'); $this->drupalGet('/entity_test_with_bundle/add/test2'); $this->submitForm(['name[0][value]' => 'test name'], 'Save'); @@ -106,7 +109,7 @@ class EntityAddUITest extends BrowserTestBase { $this->drupalGet('/entity_test_with_bundle/add'); $this->assertSession()->statusCodeEquals(200); $this->assertSession()->linkExists('Test label'); - $this->assertSession()->linkExists('Test2 label'); + $this->assertSession()->linkExists('Aaa Test2 label'); $this->assertSession()->linkNotExists('Forbidden to create bundle'); $this->assertSession()->linkNotExists('Test3 label'); $this->clickLink('Test label'); @@ -129,7 +132,7 @@ class EntityAddUITest extends BrowserTestBase { $this->drupalGet('/entity_test_with_bundle/add'); $this->assertSession()->linkNotExists('Forbidden to create bundle'); $this->assertSession()->linkNotExists('Test label'); - $this->assertSession()->linkNotExists('Test2 label'); + $this->assertSession()->linkNotExists('Aaa Test2 label'); $this->assertSession()->linkNotExists('Test3 label'); $this->assertSession()->linkExists('Add a new test entity bundle.'); } diff --git a/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php b/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php index fa6bda652de..61ea15323c1 100644 --- a/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php +++ b/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php @@ -77,7 +77,7 @@ class FileTransferTest extends BrowserTestBase { $this->_writeDirectory($base . DIRECTORY_SEPARATOR . $key, $file); } else { - // Just write the filename into the file + // Just write the filename into the file. file_put_contents($base . DIRECTORY_SEPARATOR . $file, $file); } } diff --git a/core/modules/system/tests/src/Functional/Form/ElementTest.php b/core/modules/system/tests/src/Functional/Form/ElementTest.php index 4a9755fac7f..294c4e2ec34 100644 --- a/core/modules/system/tests/src/Functional/Form/ElementTest.php +++ b/core/modules/system/tests/src/Functional/Form/ElementTest.php @@ -38,6 +38,7 @@ class ElementTest extends BrowserTestBase { $this->testFormAutocomplete(); $this->testFormElementErrors(); $this->testDetailsSummaryAttributes(); + $this->testDetailsDescriptionAttributes(); } /** @@ -153,10 +154,10 @@ class ElementTest extends BrowserTestBase { * Tests the submit_button attribute. */ protected function testSubmitButtonAttribute(): void { - // Set the submit_button attribute to true + // Set the submit_button attribute to true. $this->drupalGet('form-test/submit-button-attribute'); $this->assertSession()->elementsCount('xpath', '//input[@type="submit"]', 1); - // Set the submit_button attribute to false + // Set the submit_button attribute to false. $this->drupalGet('form-test/submit-button-attribute/1'); $this->assertSession()->elementsCount('xpath', '//input[@type="button"]', 1); } @@ -230,4 +231,13 @@ class ElementTest extends BrowserTestBase { $this->assertSession()->elementExists('css', 'summary[data-summary-attribute="test"]'); } + /** + * Tests description attributes of details. + */ + protected function testDetailsDescriptionAttributes(): void { + $this->drupalGet('form-test/group-details'); + $this->assertSession()->elementExists('css', 'details[aria-describedby="edit-description-attributes--description"]'); + $this->assertSession()->elementExists('css', 'div[id="edit-description-attributes--description"]'); + } + } diff --git a/core/modules/system/tests/src/Functional/Form/FormTest.php b/core/modules/system/tests/src/Functional/Form/FormTest.php index f45e45e6159..c7cb67c72c7 100644 --- a/core/modules/system/tests/src/Functional/Form/FormTest.php +++ b/core/modules/system/tests/src/Functional/Form/FormTest.php @@ -306,12 +306,12 @@ class FormTest extends BrowserTestBase { ]; $this->submitForm($edit, 'Submit'); // Verify that the error message is displayed with invalid token even when - // required fields are filled.' + // required fields are filled. $this->assertSession()->elementExists('xpath', '//div[contains(@class, "error")]'); $this->assertSession()->pageTextContains('The form has become outdated.'); $this->assertSession()->fieldValueEquals('integer_step', 5); - // Check a form with a URL field + // Check a form with a URL field. $this->drupalGet(Url::fromRoute('form_test.url')); $this->assertSession() ->elementExists('css', 'input[name="form_token"]') diff --git a/core/modules/system/tests/src/Functional/Hook/HookCollectorPassTest.php b/core/modules/system/tests/src/Functional/Hook/HookCollectorPassTest.php new file mode 100644 index 00000000000..59d69d1242c --- /dev/null +++ b/core/modules/system/tests/src/Functional/Hook/HookCollectorPassTest.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\system\Functional\Hook; + +use Drupal\Tests\BrowserTestBase; +use Drupal\Core\Url; + +/** + * Tests services in .module files. + * + * @group Hook + */ +class HookCollectorPassTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['container_initialize']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Tests installing a module with a Drupal container call outside functions. + * + * If this is removed then it needs to be moved to a test that installs modules through + * admin/modules. + */ + public function testContainerOutsideFunction(): void { + $settings['settings']['rebuild_access'] = (object) [ + 'value' => TRUE, + 'required' => TRUE, + ]; + + // This simulates installing the module and running a cache rebuild in a + // separate request. + $this->writeSettings($settings); + $this->rebuildAll(); + $this->drupalGet(Url::fromUri('base:core/rebuild.php')); + $this->assertSession()->pageTextNotContains('ContainerNotInitializedException'); + // Successful response from rebuild.php should redirect to the front page. + $this->assertSession()->addressEquals('/'); + + // If this file is removed then this test needs to be updated to trigger + // the container rebuild error from https://www.drupal.org/i/3505049 + $config_module_file = $this->root . '/core/modules/system/tests/modules/container_initialize/container_initialize.module'; + $this->assertFileExists($config_module_file, 'This test depends on a container call in a .module file'); + // Confirm that the file still has a bare container call. + $bare_container = "declare(strict_types=1); + +\Drupal::getContainer()->getParameter('site.path'); +"; + $file_content = file_get_contents($config_module_file); + $this->assertStringContainsString($bare_container, $file_content, 'container_initialize.module container test feature is missing.'); + } + +} diff --git a/core/modules/system/tests/src/Functional/Menu/LocalTasksTest.php b/core/modules/system/tests/src/Functional/Menu/LocalTasksTest.php index f2df8a91458..c7d6f6d7d2b 100644 --- a/core/modules/system/tests/src/Functional/Menu/LocalTasksTest.php +++ b/core/modules/system/tests/src/Functional/Menu/LocalTasksTest.php @@ -170,7 +170,7 @@ class LocalTasksTest extends BrowserTestBase { $this->assertEquals('Settings', $result[0]->getText(), 'The settings tab is active.'); $this->assertEquals('Derive 1', $result[1]->getText(), 'The derive1 tab is active.'); - // Ensures that the local tasks contains the proper 'provider key' + // Ensures that the local tasks contains the proper 'provider key'. $definitions = $this->container->get('plugin.manager.menu.local_task')->getDefinitions(); $this->assertEquals('menu_test', $definitions['menu_test.local_task_test_tasks_view']['provider']); $this->assertEquals('menu_test', $definitions['menu_test.local_task_test_tasks_edit']['provider']); diff --git a/core/modules/system/tests/src/Functional/Module/DependencyTest.php b/core/modules/system/tests/src/Functional/Module/DependencyTest.php index 7f2d218388a..2a51e4b8de8 100644 --- a/core/modules/system/tests/src/Functional/Module/DependencyTest.php +++ b/core/modules/system/tests/src/Functional/Module/DependencyTest.php @@ -142,7 +142,7 @@ class DependencyTest extends ModuleTestBase { $this->assertSession()->fieldEnabled('modules[system_no_module_version_dependency_test][enable]'); $this->assertSession()->fieldDisabled('modules[system_no_module_version_test][enable]'); - // Remove the version requirement from the dependency definition + // Remove the version requirement from the dependency definition. $info = [ 'type' => 'module', 'core_version_requirement' => '*', @@ -253,9 +253,8 @@ class DependencyTest extends ModuleTestBase { $this->resetAll(); $this->assertModules(['module_test'], TRUE); \Drupal::state()->set('module_test.dependency', 'dependency'); - // module_test creates a dependency chain: - // - dblog depends on config - // - config depends on help + // module_test creates a dependency chain: dblog depends on config which + // depends on help. $expected_order = ['help', 'config', 'dblog']; // Enable the modules through the UI, verifying that the dependency chain diff --git a/core/modules/system/tests/src/Functional/Module/UninstallTest.php b/core/modules/system/tests/src/Functional/Module/UninstallTest.php index aeace5bb488..6d1b93cc50d 100644 --- a/core/modules/system/tests/src/Functional/Module/UninstallTest.php +++ b/core/modules/system/tests/src/Functional/Module/UninstallTest.php @@ -22,7 +22,15 @@ class UninstallTest extends BrowserTestBase { /** * {@inheritdoc} */ - protected static $modules = ['module_test', 'user', 'views', 'node']; + protected static $modules = [ + 'ckeditor5', + 'filter', + 'module_test', + 'node', + 'user', + 'views', + 'views_ui', + ]; /** * {@inheritdoc} @@ -118,6 +126,13 @@ class UninstallTest extends BrowserTestBase { // Delete the node to allow node to be uninstalled. $node->delete(); + // Ensure dependent module full names are shown. + $this->assertSession()->pageTextContains('Required by: Views UI'); + // Ensure matching machine names do not display. + $this->assertSession()->pageTextNotContains('Required by: Views UI (views_ui)'); + // Ensure machine names that do not match do display. + $this->assertSession()->pageTextContains('Text Editor (editor)'); + // Uninstall module_test. $edit = []; $edit['uninstall[module_test]'] = TRUE; diff --git a/core/modules/system/tests/src/Functional/Pager/PagerTest.php b/core/modules/system/tests/src/Functional/Pager/PagerTest.php index 9d0cae26b41..3cbde95efb4 100644 --- a/core/modules/system/tests/src/Functional/Pager/PagerTest.php +++ b/core/modules/system/tests/src/Functional/Pager/PagerTest.php @@ -179,7 +179,7 @@ class PagerTest extends BrowserTestBase { // We loop through the page with the test data query parameters, and check // that the active page for each pager element has the expected page - // (1-indexed) and resulting query parameter + // (1-indexed) and resulting query parameter. foreach ($test_data as $data) { $input_query = str_replace(' ', '%20', $data['input_query']); $this->drupalGet($this->getAbsoluteUrl(parse_url($this->getUrl())['path'] . $input_query), ['external' => TRUE]); diff --git a/core/modules/system/tests/src/Functional/ParamConverter/UpcastingTest.php b/core/modules/system/tests/src/Functional/ParamConverter/UpcastingTest.php index a3a89112afe..165fb692952 100644 --- a/core/modules/system/tests/src/Functional/ParamConverter/UpcastingTest.php +++ b/core/modules/system/tests/src/Functional/ParamConverter/UpcastingTest.php @@ -41,19 +41,19 @@ class UpcastingTest extends BrowserTestBase { $user = $this->drupalCreateUser(['access content']); $foo = 'bar'; - // paramconverter_test/test_user_node_foo/{user}/{node}/{foo} + // Test "paramconverter_test/test_user_node_foo/{user}/{node}/{foo}". $this->drupalGet("paramconverter_test/test_user_node_foo/" . $user->id() . '/' . $node->id() . "/$foo"); // Verify user and node upcast by entity name. $this->assertSession()->pageTextContains("user: {$user->label()}, node: {$node->label()}, foo: $foo"); - // paramconverter_test/test_node_user_user/{node}/{foo}/{user} - // options.parameters.foo.type = entity:user + // Test "paramconverter_test/test_node_user_user/{node}/{foo}/{user}" with + // "options.parameters.foo.type = entity:user". $this->drupalGet("paramconverter_test/test_node_user_user/" . $node->id() . "/" . $user->id() . "/" . $user->id()); // Verify foo converted to user as well. $this->assertSession()->pageTextContains("user: {$user->label()}, node: {$node->label()}, foo: {$user->label()}"); - // paramconverter_test/test_node_node_foo/{user}/{node}/{foo} - // options.parameters.user.type = entity:node + // Test "paramconverter_test/test_node_node_foo/{user}/{node}/{foo}" with + // "options.parameters.user.type = entity:node". $this->drupalGet("paramconverter_test/test_node_node_foo/" . $node->id() . "/" . $node->id() . "/$foo"); // Verify that user is upcast to node (rather than to user). $this->assertSession()->pageTextContains("user: {$node->label()}, node: {$node->label()}, foo: $foo"); @@ -65,8 +65,8 @@ class UpcastingTest extends BrowserTestBase { public function testSameTypes(): void { $node = $this->drupalCreateNode(['title' => $this->randomMachineName(8)]); $parent = $this->drupalCreateNode(['title' => $this->randomMachineName(8)]); - // paramconverter_test/node/{node}/set/parent/{parent} - // options.parameters.parent.type = entity:node + // Test "paramconverter_test/node/{node}/set/parent/{parent}" with + // "options.parameters.parent.type = entity:node". $this->drupalGet("paramconverter_test/node/" . $node->id() . "/set/parent/" . $parent->id()); $this->assertSession()->pageTextContains("Setting '" . $parent->getTitle() . "' as parent of '" . $node->getTitle() . "'."); } diff --git a/core/modules/system/tests/src/Functional/System/AccessDeniedTest.php b/core/modules/system/tests/src/Functional/System/AccessDeniedTest.php index 0163c594a06..51d8d20713e 100644 --- a/core/modules/system/tests/src/Functional/System/AccessDeniedTest.php +++ b/core/modules/system/tests/src/Functional/System/AccessDeniedTest.php @@ -115,7 +115,7 @@ class AccessDeniedTest extends BrowserTestBase { $this->assertSession()->statusCodeEquals(403); $this->assertSession()->pageTextContains('Username'); - // Log back in, set the custom 403 page to /user/login and remove the block + // Log back in, set the custom 403 page to /user/login and remove the block. $this->drupalLogin($this->adminUser); $this->config('system.site')->set('page.403', '/user/login')->save(); $block->disable()->save(); diff --git a/core/modules/system/tests/src/Functional/System/PageTitleTest.php b/core/modules/system/tests/src/Functional/System/PageTitleTest.php index d225842a236..cf2a5a61512 100644 --- a/core/modules/system/tests/src/Functional/System/PageTitleTest.php +++ b/core/modules/system/tests/src/Functional/System/PageTitleTest.php @@ -126,7 +126,7 @@ class PageTitleTest extends BrowserTestBase { $this->assertSession()->titleEquals('Foo | Drupal'); $this->assertSession()->elementTextEquals('xpath', '//h1[@class="page-title"]', 'Foo'); - // Test forms + // Test forms. $this->drupalGet('form-test/object-builder'); $this->assertSession()->titleEquals('Test dynamic title | Drupal'); diff --git a/core/modules/system/tests/src/Functional/System/SitesDirectoryHardeningTest.php b/core/modules/system/tests/src/Functional/System/SitesDirectoryHardeningTest.php index 6e47278edad..32487fa8604 100644 --- a/core/modules/system/tests/src/Functional/System/SitesDirectoryHardeningTest.php +++ b/core/modules/system/tests/src/Functional/System/SitesDirectoryHardeningTest.php @@ -92,8 +92,7 @@ class SitesDirectoryHardeningTest extends BrowserTestBase { * An array of system requirements. */ protected function checkSystemRequirements() { - \Drupal::moduleHandler()->loadInclude('system', 'install'); - return system_requirements('runtime'); + return \Drupal::moduleHandler()->invoke('system', 'runtime_requirements'); } /** diff --git a/core/modules/system/tests/src/Functional/System/StatusTest.php b/core/modules/system/tests/src/Functional/System/StatusTest.php index 972ac14fb2c..b5e32759a73 100644 --- a/core/modules/system/tests/src/Functional/System/StatusTest.php +++ b/core/modules/system/tests/src/Functional/System/StatusTest.php @@ -97,6 +97,8 @@ class StatusTest extends BrowserTestBase { $this->drupalGet('admin/reports/status/php'); $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains('PHP'); + $this->assertSession()->pageTextNotContains('$_COOKIE'); $settings['settings']['sa_core_2023_004_phpinfo_flags'] = (object) [ 'value' => INFO_ALL, diff --git a/core/modules/system/tests/src/Functional/Theme/FastTest.php b/core/modules/system/tests/src/Functional/Theme/FastTest.php deleted file mode 100644 index 5cbb7d8f277..00000000000 --- a/core/modules/system/tests/src/Functional/Theme/FastTest.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\system\Functional\Theme; - -use Drupal\Tests\BrowserTestBase; -use Drupal\user\Entity\User; - -/** - * Tests autocompletion not loading registry. - * - * @group Theme - */ -class FastTest extends BrowserTestBase { - - /** - * {@inheritdoc} - */ - protected static $modules = ['theme_test']; - - /** - * {@inheritdoc} - */ - protected $defaultTheme = 'stark'; - - /** - * User allowed to access use profiles. - * - * @var \Drupal\user\Entity\User - */ - protected User $account; - - /** - * {@inheritdoc} - */ - protected function setUp(): void { - parent::setUp(); - $this->account = $this->drupalCreateUser(['access user profiles']); - } - - /** - * Tests access to user autocompletion and verify the correct results. - */ - public function testUserAutocomplete(): void { - $this->drupalLogin($this->account); - $this->drupalGet('user/autocomplete', ['query' => ['q' => $this->account->getAccountName()]]); - $this->assertSession()->responseContains($this->account->getAccountName()); - $this->assertSession()->pageTextNotContains('registry initialized'); - } - -} diff --git a/core/modules/system/tests/src/FunctionalJavascript/ActiveLinkTest.php b/core/modules/system/tests/src/FunctionalJavascript/ActiveLinkTest.php index 5d2a2848aa0..4767c4d1b98 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/ActiveLinkTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/ActiveLinkTest.php @@ -5,14 +5,14 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests active link JS behavior. * * @see Drupal.behaviors.activeLinks - * - * @group system */ +#[Group('system')] class ActiveLinkTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/Batch/ProcessingTest.php b/core/modules/system/tests/src/FunctionalJavascript/Batch/ProcessingTest.php index 03952729d54..9fbe8a05be0 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/Batch/ProcessingTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/Batch/ProcessingTest.php @@ -5,10 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript\Batch; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** - * @group Batch + * Tests Processing. */ +#[Group('Batch')] class ProcessingTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/CopyFieldValueTest.php b/core/modules/system/tests/src/FunctionalJavascript/CopyFieldValueTest.php index 38728c38455..5ed27a9c42a 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/CopyFieldValueTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/CopyFieldValueTest.php @@ -5,14 +5,14 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests copy field value functionality. * * @see Drupal.behaviors.copyFieldValue. - * - * @group system */ +#[Group('system')] class CopyFieldValueTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/Form/ConfigTargetTest.php b/core/modules/system/tests/src/FunctionalJavascript/Form/ConfigTargetTest.php index e9e092f23ee..5bb3f54f7d5 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/Form/ConfigTargetTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/Form/ConfigTargetTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript\Form; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests forms using #config_target and #ajax together. - * - * @group Form */ +#[Group('Form')] class ConfigTargetTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/Form/DevelopmentSettingsFormTest.php b/core/modules/system/tests/src/FunctionalJavascript/Form/DevelopmentSettingsFormTest.php index 30f50b7bd0a..f2feffa7fa8 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/Form/DevelopmentSettingsFormTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/Form/DevelopmentSettingsFormTest.php @@ -7,13 +7,14 @@ namespace Drupal\Tests\system\FunctionalJavascript\Form; use Drupal\Core\Cache\NullBackend; use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\HttpFoundation\Request; /** * Tests development settings form items for expected behavior. - * - * @group Form */ +#[Group('Form')] class DevelopmentSettingsFormTest extends WebDriverTestBase { /** @@ -40,9 +41,8 @@ class DevelopmentSettingsFormTest extends WebDriverTestBase { /** * Tests turning on Twig development mode. - * - * @dataProvider twigDevelopmentData */ + #[DataProvider('twigDevelopmentData')] public function testTwigDevelopmentMode(bool $twig_development_mode, ?bool $twig_debug, ?bool $twig_cache_disable): void { $twig_debug = $twig_debug ?? $twig_development_mode; $twig_cache_disable = $twig_cache_disable ?? $twig_development_mode; diff --git a/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsTableSelectTest.php b/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsTableSelectTest.php index 379856a5a1b..10ffd2523d3 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsTableSelectTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsTableSelectTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript\Form; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the tableselect form element for expected behavior. - * - * @group Form */ +#[Group('Form')] class ElementsTableSelectTest extends WebDriverTestBase { /** @@ -51,7 +51,7 @@ class ElementsTableSelectTest extends WebDriverTestBase { $this->click($row); $this->assertSession()->assertWaitOnAjaxRequest(); $page->hasCheckedField($row); - // Check other rows are not checked + // Check other rows are not checked. for ($j = 1; $j <= 3; $j++) { if ($j == $i) { continue; diff --git a/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsVerticalTabsWithSummaryTest.php b/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsVerticalTabsWithSummaryTest.php index 376a9f96170..9fbe3c27995 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsVerticalTabsWithSummaryTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/Form/ElementsVerticalTabsWithSummaryTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript\Form; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests that titles and summaries in vertical-tabs form elements are set correctly. - * - * @group Form */ +#[Group('Form')] class ElementsVerticalTabsWithSummaryTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/Form/RebuildTest.php b/core/modules/system/tests/src/FunctionalJavascript/Form/RebuildTest.php index 3858006154c..1ae041121d0 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/Form/RebuildTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/Form/RebuildTest.php @@ -9,13 +9,14 @@ use Drupal\Core\Url; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests functionality of \Drupal\Core\Form\FormBuilderInterface::rebuildForm(). * - * @group Form * @todo Add tests for other aspects of form rebuilding. */ +#[Group('Form')] class RebuildTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/Form/TriggeringElementTest.php b/core/modules/system/tests/src/FunctionalJavascript/Form/TriggeringElementTest.php index d07aea57549..dec0ace7075 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/Form/TriggeringElementTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/Form/TriggeringElementTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript\Form; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests that FAPI correctly determines the triggering element. - * - * @group Form */ +#[Group('Form')] class TriggeringElementTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/FrameworkTest.php b/core/modules/system/tests/src/FunctionalJavascript/FrameworkTest.php index 0fd5f468fb1..780b772932f 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/FrameworkTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/FrameworkTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the off-canvas dialog functionality. - * - * @group system */ +#[Group('system')] class FrameworkTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/ModalRendererTest.php b/core/modules/system/tests/src/FunctionalJavascript/ModalRendererTest.php index ab5735efc57..5d6e8a469ba 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/ModalRendererTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/ModalRendererTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests that dialog links use different renderer services. - * - * @group system */ +#[Group('system')] class ModalRendererTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/ModuleFilterTest.php b/core/modules/system/tests/src/FunctionalJavascript/ModuleFilterTest.php index 96f877179e3..cb809502830 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/ModuleFilterTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/ModuleFilterTest.php @@ -5,13 +5,13 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript functionality of the module filter. - * - * @group system - * @group #slow */ +#[Group('system')] +#[Group('#slow')] class ModuleFilterTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/ModuleUninstallFilterTest.php b/core/modules/system/tests/src/FunctionalJavascript/ModuleUninstallFilterTest.php index 2652b322578..d8b3bf0eac1 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/ModuleUninstallFilterTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/ModuleUninstallFilterTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript functionality of the module uninstall filter. - * - * @group system */ +#[Group('system')] class ModuleUninstallFilterTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php index aef99eb3901..462bf5a9e67 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php @@ -4,11 +4,13 @@ declare(strict_types=1); namespace Drupal\Tests\system\FunctionalJavascript; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; + /** * Tests the off-canvas dialog functionality. - * - * @group system */ +#[Group('system')] class OffCanvasTest extends OffCanvasTestBase { /** @@ -34,9 +36,8 @@ class OffCanvasTest extends OffCanvasTestBase { /** * Tests that non-contextual links will work with the off-canvas dialog. - * - * @dataProvider themeDataProvider */ + #[DataProvider('themeDataProvider')] public function testOffCanvasLinks($theme): void { $this->enableTheme($theme); $this->drupalGet('/off-canvas-test-links'); @@ -95,7 +96,7 @@ class OffCanvasTest extends OffCanvasTestBase { $page->clickLink('Display more links!'); $this->waitForOffCanvasToOpen(); $web_assert->linkExists('Off_canvas link!'); - // Click off-canvas link inside off-canvas dialog + // Click off-canvas link inside off-canvas dialog. $page->clickLink('Off_canvas link!'); $this->waitForOffCanvasToOpen(); $web_assert->elementTextContains('css', '.ui-dialog[aria-describedby="drupal-off-canvas"]', 'Thing 2 says hello'); @@ -108,7 +109,7 @@ class OffCanvasTest extends OffCanvasTestBase { $page->clickLink('Display more links!'); $this->waitForOffCanvasToOpen(); $web_assert->linkExists('Off_canvas link!'); - // Click off-canvas link inside off-canvas dialog + // Click off-canvas link inside off-canvas dialog. $page->clickLink('Off_canvas link!'); $this->waitForOffCanvasToOpen(); $web_assert->elementTextContains('css', '.ui-dialog[aria-describedby="drupal-off-canvas"]', 'Thing 2 says hello'); diff --git a/core/modules/system/tests/src/FunctionalJavascript/System/DateFormatTest.php b/core/modules/system/tests/src/FunctionalJavascript/System/DateFormatTest.php index d2ab2f4a744..1bbb86d92e6 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/System/DateFormatTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/System/DateFormatTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\system\FunctionalJavascript\System; use Drupal\Core\Datetime\Entity\DateFormat; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests that date formats UI with JavaScript enabled. - * - * @group system */ +#[Group('system')] class DateFormatTest extends WebDriverTestBase { /** diff --git a/core/modules/system/tests/src/FunctionalJavascript/ThemeSettingsFormTest.php b/core/modules/system/tests/src/FunctionalJavascript/ThemeSettingsFormTest.php index 41fc94d4f1e..9c94c6040b0 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/ThemeSettingsFormTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/ThemeSettingsFormTest.php @@ -7,12 +7,13 @@ namespace Drupal\Tests\system\FunctionalJavascript; use Drupal\file\Entity\File; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\TestFileCreationTrait; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; /** * Tests that theme form settings works correctly. - * - * @group system */ +#[Group('system')] class ThemeSettingsFormTest extends WebDriverTestBase { use TestFileCreationTrait; @@ -39,9 +40,8 @@ class ThemeSettingsFormTest extends WebDriverTestBase { /** * Tests that submission handler works correctly. - * - * @dataProvider providerTestFormSettingsSubmissionHandler */ + #[DataProvider('providerTestFormSettingsSubmissionHandler')] public function testFormSettingsSubmissionHandler($theme): void { \Drupal::service('theme_installer')->install([$theme]); diff --git a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php index b8b18a5c9ba..9a69e563f01 100644 --- a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php +++ b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php @@ -27,8 +27,7 @@ class MigrateSystemConfigurationTest extends MigrateDrupal7TestBase { 'system.authorize' => [], 'system.cron' => [ 'threshold' => [ - // Auto-run is not handled by the migration. - // 'autorun' => 0, + // Auto-run is not handled by the migration, so ignore "'autorun' => 0". 'requirements_warning' => 172800, 'requirements_error' => 1209600, ], diff --git a/core/modules/system/tests/src/Unit/Pager/PreprocessPagerTest.php b/core/modules/system/tests/src/Unit/Pager/PreprocessPagerTest.php index ab42b418125..8a2cfe22f03 100644 --- a/core/modules/system/tests/src/Unit/Pager/PreprocessPagerTest.php +++ b/core/modules/system/tests/src/Unit/Pager/PreprocessPagerTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\system\Unit\Pager; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Pager\PagerPreprocess; use Drupal\Core\Template\AttributeString; use Drupal\Tests\UnitTestCase; @@ -12,10 +13,17 @@ use Drupal\Tests\UnitTestCase; * Tests pager preprocessing. * * @group system + * + * @coversDefaultClass \Drupal\Core\Pager\PagerPreprocess */ class PreprocessPagerTest extends UnitTestCase { /** + * Pager preprocess instance. + */ + protected PagerPreprocess $pagerPreprocess; + + /** * {@inheritdoc} */ protected function setUp(): void { @@ -39,21 +47,19 @@ class PreprocessPagerTest extends UnitTestCase { $pager_manager->method('getPager')->willReturn($pager); $pager_manager->method('getUpdatedParameters')->willReturn(''); + $this->pagerPreprocess = new PagerPreprocess($pager_manager); + $container = new ContainerBuilder(); - $container->set('pager.manager', $pager_manager); $container->set('url_generator', $url_generator); - // template_preprocess_pager() renders translatable attribute values. - $container->set('string_translation', $this->getStringTranslationStub()); \Drupal::setContainer($container); } /** - * Tests template_preprocess_pager() when an empty #quantity is passed. + * Tests when an empty #quantity is passed. * - * @covers ::template_preprocess_pager + * @covers ::preprocessPager */ public function testQuantityNotSet(): void { - require_once $this->root . '/core/includes/theme.inc'; $variables = [ 'pager' => [ '#element' => '', @@ -63,18 +69,17 @@ class PreprocessPagerTest extends UnitTestCase { '#tags' => '', ], ]; - template_preprocess_pager($variables); + $this->pagerPreprocess->preprocessPager($variables); $this->assertEquals(['first', 'previous'], array_keys($variables['items'])); } /** - * Tests template_preprocess_pager() when a #quantity value is passed. + * Tests when a #quantity value is passed. * - * @covers ::template_preprocess_pager + * @covers ::preprocessPager */ public function testQuantitySet(): void { - require_once $this->root . '/core/includes/theme.inc'; $variables = [ 'pager' => [ '#element' => '2', @@ -84,7 +89,7 @@ class PreprocessPagerTest extends UnitTestCase { '#tags' => '', ], ]; - template_preprocess_pager($variables); + $this->pagerPreprocess->preprocessPager($variables); $this->assertEquals(['first', 'previous', 'pages'], array_keys($variables['items'])); /** @var \Drupal\Core\Template\AttributeString $attribute */ @@ -94,12 +99,11 @@ class PreprocessPagerTest extends UnitTestCase { } /** - * Tests template_preprocess_pager() when an empty #pagination_heading_level value is passed. + * Tests when an empty #pagination_heading_level value is passed. * - * @covers ::template_preprocess_pager + * @covers ::preprocessPager */ public function testEmptyPaginationHeadingLevelSet(): void { - require_once $this->root . '/core/includes/theme.inc'; $variables = [ 'pager' => [ '#element' => '2', @@ -110,18 +114,17 @@ class PreprocessPagerTest extends UnitTestCase { '#tags' => '', ], ]; - template_preprocess_pager($variables); + $this->pagerPreprocess->preprocessPager($variables); $this->assertEquals('h4', $variables['pagination_heading_level']); } /** - * Tests template_preprocess_pager() when no #pagination_heading_level is passed. + * Tests when no #pagination_heading_level is passed. * - * @covers ::template_preprocess_pager + * @covers ::preprocessPager */ public function testPaginationHeadingLevelNotSet(): void { - require_once $this->root . '/core/includes/theme.inc'; $variables = [ 'pager' => [ '#element' => '', @@ -131,18 +134,17 @@ class PreprocessPagerTest extends UnitTestCase { '#tags' => '', ], ]; - template_preprocess_pager($variables); + $this->pagerPreprocess->preprocessPager($variables); $this->assertEquals('h4', $variables['pagination_heading_level']); } /** - * Tests template_preprocess_pager() when a #pagination_heading_level value is passed. + * Tests when a #pagination_heading_level value is passed. * - * @covers ::template_preprocess_pager + * @covers ::preprocessPager */ public function testPaginationHeadingLevelSet(): void { - require_once $this->root . '/core/includes/theme.inc'; $variables = [ 'pager' => [ '#element' => '2', @@ -153,18 +155,17 @@ class PreprocessPagerTest extends UnitTestCase { '#tags' => '', ], ]; - template_preprocess_pager($variables); + $this->pagerPreprocess->preprocessPager($variables); $this->assertEquals('h5', $variables['pagination_heading_level']); } /** - * Test template_preprocess_pager() with an invalid #pagination_heading_level. + * Test with an invalid #pagination_heading_level. * - * @covers ::template_preprocess_pager + * @covers ::preprocessPager */ public function testPaginationHeadingLevelInvalid(): void { - require_once $this->root . '/core/includes/theme.inc'; $variables = [ 'pager' => [ '#element' => '2', @@ -175,7 +176,7 @@ class PreprocessPagerTest extends UnitTestCase { '#tags' => '', ], ]; - template_preprocess_pager($variables); + $this->pagerPreprocess->preprocessPager($variables); $this->assertEquals('h4', $variables['pagination_heading_level']); } diff --git a/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php new file mode 100644 index 00000000000..89f177daf24 --- /dev/null +++ b/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php @@ -0,0 +1,25 @@ +<?php + +namespace Drupal\taxonomy\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for taxonomy. + */ +class TaxonomyThemeHooks { + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_taxonomy_term')] + public function themeSuggestionsTaxonomyTerm(array $variables): array { + $suggestions = []; + /** @var \Drupal\taxonomy\TermInterface $term */ + $term = $variables['elements']['#taxonomy_term']; + $suggestions[] = 'taxonomy_term__' . $term->bundle(); + $suggestions[] = 'taxonomy_term__' . $term->id(); + return $suggestions; + } + +} diff --git a/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php index 12c8f7dc885..7c7fbf63d98 100644 --- a/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php +++ b/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php @@ -65,7 +65,7 @@ class TaxonomyTokensHooks { 'name' => $this->t("Term count"), 'description' => $this->t("The number of terms belonging to the taxonomy vocabulary."), ]; - // Chained tokens for taxonomies + // Chained tokens for taxonomies. $term['vocabulary'] = [ 'name' => $this->t("Vocabulary"), 'description' => $this->t("The vocabulary the taxonomy term belongs to."), diff --git a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php index 7b7a57363bc..f4fd0c8641f 100644 --- a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php +++ b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php @@ -181,7 +181,7 @@ class Tid extends ArgumentDefaultPluginBase implements CacheableDependencyInterf } if (!empty($this->options['limit'])) { $tids = []; - // Filter by vocabulary + // Filter by vocabulary. foreach ($taxonomy as $tid => $vocab) { if (!empty($this->options['vids'][$vocab])) { $tids[] = $tid; diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php index 6d1b18c7647..e48931b5267 100644 --- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php +++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php @@ -289,7 +289,7 @@ class TaxonomyIndexTid extends ManyToOne { } if (!$form_state->get('exposed')) { - // Retain the helper option + // Retain the helper option. $this->helper->buildOptionsForm($form, $form_state); // Show help text if not exposed to end users. @@ -330,7 +330,7 @@ class TaxonomyIndexTid extends ManyToOne { } // If view is an attachment and is inheriting exposed filters, then assume - // exposed input has already been validated + // exposed input has already been validated. if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) { $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']]; } @@ -418,7 +418,7 @@ class TaxonomyIndexTid extends ManyToOne { * {@inheritdoc} */ public function adminSummary() { - // Set up $this->valueOptions for the parent summary + // Set up $this->valueOptions for the parent summary. $this->valueOptions = []; if ($this->value) { diff --git a/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php b/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php index 5987d9fe933..28c9cc8b06b 100644 --- a/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php +++ b/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php @@ -109,7 +109,7 @@ class NodeTermData extends RelationshipPluginBase { $def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER'; } else { - // If vocabularies are supplied join a subselect instead + // If vocabularies are supplied join a subselect instead. $def['left_table'] = $this->tableAlias; $def['left_field'] = 'nid'; $def['field'] = 'nid'; diff --git a/core/modules/taxonomy/src/TermViewsData.php b/core/modules/taxonomy/src/TermViewsData.php index 7ca182d9693..c6a1463fc25 100644 --- a/core/modules/taxonomy/src/TermViewsData.php +++ b/core/modules/taxonomy/src/TermViewsData.php @@ -139,12 +139,12 @@ class TermViewsData extends EntityViewsData { $data['taxonomy_index']['table']['join'] = [ 'taxonomy_term_field_data' => [ - // Links directly to taxonomy_term_field_data via tid + // Links directly to taxonomy_term_field_data via tid. 'left_field' => 'tid', 'field' => 'tid', ], 'node_field_data' => [ - // Links directly to node via nid + // Links directly to node via nid. 'left_field' => 'nid', 'field' => 'nid', ], diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 28e54b11f71..6694f62e563 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -10,21 +10,6 @@ use Drupal\Core\Render\Element; use Drupal\taxonomy\Entity\Term; /** - * Implements hook_theme_suggestions_HOOK(). - */ -function taxonomy_theme_suggestions_taxonomy_term(array $variables): array { - $suggestions = []; - - /** @var \Drupal\taxonomy\TermInterface $term */ - $term = $variables['elements']['#taxonomy_term']; - - $suggestions[] = 'taxonomy_term__' . $term->bundle(); - $suggestions[] = 'taxonomy_term__' . $term->id(); - - return $suggestions; -} - -/** * Prepares variables for taxonomy term templates. * * Default template: taxonomy-term.html.twig. diff --git a/core/modules/taxonomy/tests/src/Functional/TermLanguageTest.php b/core/modules/taxonomy/tests/src/Functional/TermLanguageTest.php index 4fa2b896c25..11ba24f7c06 100644 --- a/core/modules/taxonomy/tests/src/Functional/TermLanguageTest.php +++ b/core/modules/taxonomy/tests/src/Functional/TermLanguageTest.php @@ -164,7 +164,7 @@ class TermLanguageTest extends TaxonomyTestBase { ]); $term->save(); - // Overview page in the other language shows the translated term + // Overview page in the other language shows the translated term. $this->drupalGet('bb/admin/structure/taxonomy/manage/' . $this->vocabulary->id() . '/overview'); $this->assertSession()->responseMatches('|<a[^>]*>' . $translated_title . '</a>|'); } diff --git a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php index 83a3ce1521c..ccaab589e5c 100644 --- a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php @@ -133,7 +133,7 @@ abstract class TextItemBase extends FieldItemBase { $settings = $field_definition->getSettings(); if (empty($settings['max_length'])) { - // Textarea handling + // Textarea handling. $value = $random->paragraphs(); } else { diff --git a/core/modules/text/src/Plugin/Field/FieldWidget/TextfieldWidget.php b/core/modules/text/src/Plugin/Field/FieldWidget/TextfieldWidget.php index 16b7549681c..289fbbd332c 100644 --- a/core/modules/text/src/Plugin/Field/FieldWidget/TextfieldWidget.php +++ b/core/modules/text/src/Plugin/Field/FieldWidget/TextfieldWidget.php @@ -6,7 +6,10 @@ use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Widget; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\filter\Element\TextFormat; use Symfony\Component\Validator\ConstraintViolationInterface; /** @@ -22,20 +25,20 @@ class TextfieldWidget extends StringTextfieldWidget { /** * {@inheritdoc} */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $main_widget = parent::formElement($items, $delta, $element, $form, $form_state); + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface { + $widget = parent::singleElementObject($items, $delta, $widget, $form, $form_state); $allowed_formats = $this->getFieldSetting('allowed_formats'); - $element = $main_widget['value']; - $element['#type'] = 'text_format'; - $element['#format'] = $items[$delta]->format ?? NULL; - $element['#base_type'] = $main_widget['value']['#type']; - + $widget = $widget->getChild('value'); + $type = $widget->type; + $widget = $widget->changeType(TextFormat::class); + $widget->format = $items[$delta]->format ?? NULL; + $widget->base_type = $type; if ($allowed_formats && !$this->isDefaultValueWidget($form_state)) { - $element['#allowed_formats'] = $allowed_formats; + $widget->allowed_formats = $allowed_formats; } - return $element; + return $widget; } /** diff --git a/core/modules/text/tests/src/FunctionalJavascript/TextareaWithSummaryTest.php b/core/modules/text/tests/src/FunctionalJavascript/TextareaWithSummaryTest.php index 957b7ae5e9d..92576dcb95b 100644 --- a/core/modules/text/tests/src/FunctionalJavascript/TextareaWithSummaryTest.php +++ b/core/modules/text/tests/src/FunctionalJavascript/TextareaWithSummaryTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\text\FunctionalJavascript; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript functionality of the text_textarea_with_summary widget. - * - * @group text */ +#[Group('text')] class TextareaWithSummaryTest extends WebDriverTestBase { /** diff --git a/core/modules/toolbar/src/Hook/ToolbarThemeHooks.php b/core/modules/toolbar/src/Hook/ToolbarThemeHooks.php new file mode 100644 index 00000000000..fb08f0ffde6 --- /dev/null +++ b/core/modules/toolbar/src/Hook/ToolbarThemeHooks.php @@ -0,0 +1,23 @@ +<?php + +namespace Drupal\toolbar\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for toolbar. + */ +class ToolbarThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for HTML document templates. + */ + #[Hook('preprocess_html')] + public function preprocessHtml(&$variables): void { + if (!\Drupal::currentUser()->hasPermission('access toolbar')) { + return; + } + $variables['attributes']['class'][] = 'toolbar-loading'; + } + +} diff --git a/core/modules/toolbar/tests/modules/toolbar_test/src/Hook/ToolbarTestThemeHooks.php b/core/modules/toolbar/tests/modules/toolbar_test/src/Hook/ToolbarTestThemeHooks.php new file mode 100644 index 00000000000..e1d40c8219a --- /dev/null +++ b/core/modules/toolbar/tests/modules/toolbar_test/src/Hook/ToolbarTestThemeHooks.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\toolbar_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for toolbar_test. + */ +class ToolbarTestThemeHooks { + + /** + * Implements hook_preprocess_HOOK(). + */ + #[Hook('preprocess_menu')] + public function preprocessMenu(&$variables): void { + // All the standard hook_theme variables should be populated when the + // Toolbar module is rendering a menu. + foreach ([ + 'menu_name', + 'items', + 'attributes', + ] as $variable) { + $variables[$variable]; + } + } + +} diff --git a/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module b/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module deleted file mode 100644 index 8980eb4a6ef..00000000000 --- a/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module +++ /dev/null @@ -1,19 +0,0 @@ -<?php - -/** - * @file - * A dummy module to test API interaction with the Toolbar module. - */ - -declare(strict_types=1); - -/** - * Implements hook_preprocess_HOOK(). - */ -function toolbar_test_preprocess_menu(&$variables): void { - // All the standard hook_theme variables should be populated when the - // Toolbar module is rendering a menu. - foreach (['menu_name', 'items', 'attributes'] as $variable) { - $variables[$variable]; - } -} diff --git a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarActiveTrailTest.php b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarActiveTrailTest.php index b12ce9ef54f..73f495ae850 100644 --- a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarActiveTrailTest.php +++ b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarActiveTrailTest.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace Drupal\Tests\toolbar\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\TestWith; /** * Tests that the active trail is maintained in the toolbar. - * - * @group toolbar */ +#[Group('toolbar')] class ToolbarActiveTrailTest extends WebDriverTestBase { /** @@ -43,11 +44,10 @@ class ToolbarActiveTrailTest extends WebDriverTestBase { * @param string $orientation * The toolbar orientation. * - * @testWith ["vertical"] - * ["horizontal"] - * * @throws \Behat\Mink\Exception\ElementNotFoundException */ + #[TestWith(["vertical"])] + #[TestWith(["horizontal"])] public function testToolbarActiveTrail(string $orientation): void { $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); diff --git a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php index dcf0ff6d79c..639129786b3 100644 --- a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php +++ b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\toolbar\FunctionalJavascript; use Behat\Mink\Element\NodeElement; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript functionality of the toolbar. - * - * @group toolbar */ +#[Group('toolbar')] class ToolbarIntegrationTest extends WebDriverTestBase { /** diff --git a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarStoredStateTest.php b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarStoredStateTest.php index 432dd8c8522..9fc82f8f007 100644 --- a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarStoredStateTest.php +++ b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarStoredStateTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\toolbar\FunctionalJavascript; use Drupal\Component\Serialization\Json; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the sessionStorage state set by the toolbar. - * - * @group toolbar */ +#[Group('toolbar')] class ToolbarStoredStateTest extends WebDriverTestBase { /** diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index 51986310901..4bfcd89e187 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -118,17 +118,6 @@ function toolbar_menu_navigation_links(array $tree) { } /** - * Implements hook_preprocess_HOOK() for HTML document templates. - */ -function toolbar_preprocess_html(&$variables): void { - if (!\Drupal::currentUser()->hasPermission('access toolbar')) { - return; - } - - $variables['attributes']['class'][] = 'toolbar-loading'; -} - -/** * Returns the rendered subtree of each top-level toolbar link. * * @return array diff --git a/core/modules/update/src/Hook/UpdateHooks.php b/core/modules/update/src/Hook/UpdateHooks.php index 6c6c57b53e5..6577f7f1fc2 100644 --- a/core/modules/update/src/Hook/UpdateHooks.php +++ b/core/modules/update/src/Hook/UpdateHooks.php @@ -78,14 +78,6 @@ class UpdateHooks { $verbose = TRUE; break; } - // This loadInclude() is to ensure that the install API is available. - // Since we're loading an include of type 'install', this will also - // include core/includes/install.inc for us, which is where the - // REQUIREMENTS* constants are currently defined. - // @todo Remove this once those constants live in a better place. - // @see https://www.drupal.org/project/drupal/issues/2909480 - // @see https://www.drupal.org/project/drupal/issues/3410938 - \Drupal::moduleHandler()->loadInclude('update', 'install'); $status = \Drupal::moduleHandler()->invoke('update', 'runtime_requirements'); foreach (['core', 'contrib'] as $report_type) { $type = 'update_' . $report_type; diff --git a/core/modules/update/tests/src/Functional/UpdateSemverContribTestBase.php b/core/modules/update/tests/src/Functional/UpdateSemverContribTestBase.php index 2f5c7c038b9..bd554c0e850 100644 --- a/core/modules/update/tests/src/Functional/UpdateSemverContribTestBase.php +++ b/core/modules/update/tests/src/Functional/UpdateSemverContribTestBase.php @@ -10,7 +10,7 @@ namespace Drupal\Tests\update\Functional; * This wires up the protected data from UpdateSemverTestBase for a contrib * module with semantic version releases. */ -class UpdateSemverContribTestBase extends UpdateSemverTestBase { +abstract class UpdateSemverContribTestBase extends UpdateSemverTestBase { /** * {@inheritdoc} diff --git a/core/modules/update/tests/src/Functional/UpdateSemverCoreTestBase.php b/core/modules/update/tests/src/Functional/UpdateSemverCoreTestBase.php index b9d3a46a68e..f336562c479 100644 --- a/core/modules/update/tests/src/Functional/UpdateSemverCoreTestBase.php +++ b/core/modules/update/tests/src/Functional/UpdateSemverCoreTestBase.php @@ -10,7 +10,7 @@ namespace Drupal\Tests\update\Functional; * This wires up the protected data from UpdateSemverTestBase for Drupal core * with semantic version releases. */ -class UpdateSemverCoreTestBase extends UpdateSemverTestBase { +abstract class UpdateSemverCoreTestBase extends UpdateSemverTestBase { /** * {@inheritdoc} diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc index 05c6565ce19..800b10d0e50 100644 --- a/core/modules/update/update.compare.inc +++ b/core/modules/update/update.compare.inc @@ -32,7 +32,7 @@ function update_process_project_info(&$projects): void { $info = $project['info']; if (isset($info['version'])) { - // Check for development snapshots + // Check for development snapshots. if (preg_match('@(dev|HEAD)@', $info['version'])) { $install_type = 'dev'; } diff --git a/core/modules/update/update.fetch.inc b/core/modules/update/update.fetch.inc index c8e4990d385..12295b97d98 100644 --- a/core/modules/update/update.fetch.inc +++ b/core/modules/update/update.fetch.inc @@ -20,14 +20,6 @@ use Drupal\update\UpdateManagerInterface; #[ProceduralHookScanStop] function _update_cron_notify(): void { $update_config = \Drupal::config('update.settings'); - // This loadInclude() is to ensure that the install API is available. - // Since we're loading an include of type 'install', this will also - // include core/includes/install.inc for us, which is where the - // REQUIREMENTS* constants are currently defined. - // @todo Remove this once those constants live in a better place. - // @see https://www.drupal.org/project/drupal/issues/2909480 - // @see https://www.drupal.org/project/drupal/issues/3410938 - \Drupal::moduleHandler()->loadInclude('update', 'install'); $status = \Drupal::moduleHandler()->invoke('update', 'runtime_requirements'); $params = []; $notify_all = ($update_config->get('notification.threshold') == 'all'); diff --git a/core/modules/user/config/schema/user.schema.yml b/core/modules/user/config/schema/user.schema.yml index d58cb3dc4ea..54990b28497 100644 --- a/core/modules/user/config/schema/user.schema.yml +++ b/core/modules/user/config/schema/user.schema.yml @@ -115,27 +115,22 @@ user.flood: type: integer label: 'IP limit' constraints: - Range: - min: 0 + PositiveOrZero: ~ ip_window: type: integer label: 'IP window' constraints: - Range: - min: 0 + PositiveOrZero: ~ user_limit: type: integer label: 'User limit' constraints: - Range: - min: 0 + PositiveOrZero: ~ user_window: type: integer label: 'User window' constraints: - Range: - min: 0 - + PositiveOrZero: ~ user.role.*: type: config_entity label: 'User role settings' diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php index 881244f6f53..d0c5e8d2d9b 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -402,6 +402,7 @@ abstract class AccountForm extends ContentEntityForm implements TrustedCallbackI 'name', 'pass', 'mail', + 'roles', 'timezone', 'langcode', 'preferred_langcode', @@ -420,6 +421,7 @@ abstract class AccountForm extends ContentEntityForm implements TrustedCallbackI 'name', 'pass', 'mail', + 'roles', 'timezone', 'langcode', 'preferred_langcode', diff --git a/core/modules/user/src/Hook/UserRequirements.php b/core/modules/user/src/Hook/UserRequirements.php index f317ced58bc..46155e55e3c 100644 --- a/core/modules/user/src/Hook/UserRequirements.php +++ b/core/modules/user/src/Hook/UserRequirements.php @@ -49,6 +49,7 @@ class UserRequirements { $query->addExpression('LOWER(mail)', 'lower_mail'); $query->isNotNull('mail'); $query->groupBy('lower_mail'); + $query->groupBy('langcode'); $query->having('COUNT(uid) > :matches', [':matches' => 1]); $conflicts = $query->countQuery()->execute()->fetchField(); diff --git a/core/modules/user/src/Hook/UserThemeHooks.php b/core/modules/user/src/Hook/UserThemeHooks.php new file mode 100644 index 00000000000..9d334a68523 --- /dev/null +++ b/core/modules/user/src/Hook/UserThemeHooks.php @@ -0,0 +1,26 @@ +<?php + +namespace Drupal\user\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for user. + */ +class UserThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for block templates. + */ + #[Hook('preprocess_block')] + public function preprocessBlock(&$variables): void { + if ($variables['configuration']['provider'] == 'user') { + switch ($variables['elements']['#plugin_id']) { + case 'user_login_block': + $variables['attributes']['role'] = 'form'; + break; + } + } + } + +} diff --git a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php index 0310a62e453..12b46aee574 100644 --- a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php +++ b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php @@ -32,23 +32,23 @@ class UserNameConstraintValidator extends ConstraintValidator { } if (preg_match('/[^\x{80}-\x{F7} a-z0-9@+_.\'-]/i', $name) || preg_match( - // Non-printable ISO-8859-1 + NBSP + // Non-printable ISO-8859-1 + NBSP. '/[\x{80}-\x{A0}' . - // Soft-hyphen + // Soft-hyphen. '\x{AD}' . - // Various space characters + // Various space characters. '\x{2000}-\x{200F}' . - // Bidirectional text overrides + // Bidirectional text overrides. '\x{2028}-\x{202F}' . - // Various text hinting characters + // Various text hinting characters. '\x{205F}-\x{206F}' . - // Byte order mark + // Byte order mark. '\x{FEFF}' . - // Full-width latin + // Full-width latin. '\x{FF01}-\x{FF60}' . - // Replacement characters + // Replacement characters. '\x{FFF9}-\x{FFFD}' . - // NULL byte and control characters + // NULL byte and control characters. '\x{0}-\x{1F}]/u', $name) ) { diff --git a/core/modules/user/src/Plugin/views/access/Permission.php b/core/modules/user/src/Plugin/views/access/Permission.php index ac66b6229b8..aeeb52ca290 100644 --- a/core/modules/user/src/Plugin/views/access/Permission.php +++ b/core/modules/user/src/Plugin/views/access/Permission.php @@ -129,7 +129,7 @@ class Permission extends AccessPluginBase implements CacheableDependencyInterfac */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); - // Get list of permissions + // Get list of permissions. $perms = []; $permissions = $this->permissionHandler->getPermissions(); foreach ($permissions as $perm => $perm_item) { diff --git a/core/modules/user/src/Plugin/views/argument_validator/User.php b/core/modules/user/src/Plugin/views/argument_validator/User.php index 6133434644c..b8db861db79 100644 --- a/core/modules/user/src/Plugin/views/argument_validator/User.php +++ b/core/modules/user/src/Plugin/views/argument_validator/User.php @@ -83,7 +83,7 @@ class User extends Entity { */ public function submitOptionsForm(&$form, FormStateInterface $form_state, &$options = []) { // Filter trash out of the options so we don't store giant unnecessary - // arrays + // arrays. $options['roles'] = array_filter($options['roles']); } diff --git a/core/modules/user/src/Plugin/views/filter/Name.php b/core/modules/user/src/Plugin/views/filter/Name.php index 267ef6176df..3a03b09fddf 100644 --- a/core/modules/user/src/Plugin/views/filter/Name.php +++ b/core/modules/user/src/Plugin/views/filter/Name.php @@ -133,7 +133,7 @@ class Name extends InOperator { * {@inheritdoc} */ public function adminSummary() { - // Set up $this->valueOptions for the parent summary + // Set up $this->valueOptions for the parent summary. $this->valueOptions = []; if ($this->value) { diff --git a/core/modules/user/tests/modules/user_form_test/src/Hook/UserFormTestHooks.php b/core/modules/user/tests/modules/user_form_test/src/Hook/UserFormTestHooks.php index f49008597f1..106199f413f 100644 --- a/core/modules/user/tests/modules/user_form_test/src/Hook/UserFormTestHooks.php +++ b/core/modules/user/tests/modules/user_form_test/src/Hook/UserFormTestHooks.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\user_form_test\Hook; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Hook\Attribute\Hook; /** @@ -20,4 +21,14 @@ class UserFormTestHooks { $form['access']['#value'] = \Drupal::currentUser()->hasPermission('cancel other accounts'); } + /** + * Implements hook_entity_base_field_info_alter(). + */ + #[Hook('entity_base_field_info_alter')] + public function entityBaseFieldInfoAlter(&$fields, EntityTypeInterface $entity_type): void { + if ($entity_type->id() === 'user' && \Drupal::keyvalue('user_form_test')->get('user_form_test_constraint_roles_edit')) { + $fields['roles']->addConstraint('FieldWidgetConstraint'); + } + } + } diff --git a/core/modules/user/tests/src/Functional/UserAdminTest.php b/core/modules/user/tests/src/Functional/UserAdminTest.php index a400af7a7e0..fa159a757eb 100644 --- a/core/modules/user/tests/src/Functional/UserAdminTest.php +++ b/core/modules/user/tests/src/Functional/UserAdminTest.php @@ -138,14 +138,14 @@ class UserAdminTest extends BrowserTestBase { $account = $user_storage->load($user_c->id()); $this->assertTrue($account->isBlocked(), 'User C blocked'); - // Test filtering on admin page for blocked users + // Test filtering on admin page for blocked users. $this->drupalGet('admin/people', ['query' => ['status' => 2]]); $this->assertSession()->elementNotExists('xpath', static::getLinkSelectorForUser($user_a)); $this->assertSession()->elementNotExists('xpath', static::getLinkSelectorForUser($user_b)); $this->assertSession()->elementExists('xpath', static::getLinkSelectorForUser($user_c)); // Test unblocking of a user from /admin/people page and sending of - // activation mail + // activation mail. $edit_unblock = []; $edit_unblock['action'] = 'user_unblock_user_action'; $edit_unblock['user_bulk_form[4]'] = TRUE; diff --git a/core/modules/user/tests/src/Functional/UserCancelTest.php b/core/modules/user/tests/src/Functional/UserCancelTest.php index 65787a0729b..27ab5fc6142 100644 --- a/core/modules/user/tests/src/Functional/UserCancelTest.php +++ b/core/modules/user/tests/src/Functional/UserCancelTest.php @@ -279,7 +279,7 @@ class UserCancelTest extends BrowserTestBase { public function testUserBlockUnpublishNodeAccess(): void { \Drupal::service('module_installer')->install(['node_access_test', 'user_form_test']); - // Setup node access + // Setup node access. node_access_rebuild(); $this->addPrivateField(NodeType::load('page')); \Drupal::state()->set('node_access_test.private', TRUE); diff --git a/core/modules/user/tests/src/Functional/UserEditTest.php b/core/modules/user/tests/src/Functional/UserEditTest.php index f9a8dfb1a75..ff86fa24908 100644 --- a/core/modules/user/tests/src/Functional/UserEditTest.php +++ b/core/modules/user/tests/src/Functional/UserEditTest.php @@ -283,4 +283,21 @@ class UserEditTest extends BrowserTestBase { $this->assertSession()->fieldEnabled('edit-status-1'); } + /** + * Tests constraint violations are triggered on the user account form. + */ + public function testRolesValidation(): void { + $admin_user = $this->drupalCreateUser(['administer users']); + $this->drupalLogin($admin_user); + $this->drupalGet("user/" . $admin_user->id() . "/edit"); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('The changes have been saved.'); + \Drupal::keyvalue('user_form_test')->set('user_form_test_constraint_roles_edit', TRUE); + \Drupal::service('module_installer')->install(['entity_test', 'user_form_test']); + $this->drupalGet("user/" . $admin_user->id() . "/edit"); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Widget constraint has failed.'); + $this->assertSession()->pageTextNotContains('The changes have been saved.'); + } + } diff --git a/core/modules/user/tests/src/Functional/UserSearchTest.php b/core/modules/user/tests/src/Functional/UserSearchTest.php index dd957dfe88d..d69eee78341 100644 --- a/core/modules/user/tests/src/Functional/UserSearchTest.php +++ b/core/modules/user/tests/src/Functional/UserSearchTest.php @@ -93,7 +93,7 @@ class UserSearchTest extends BrowserTestBase { $this->assertSession()->pageTextContains($keys); $this->assertSession()->pageTextContains($user2->getAccountName()); - // Verify that wildcard search works for email + // Verify that wildcard search works for email. $subkey = substr($keys, 0, 2) . '*' . substr($keys, 4, 2); $edit = ['keys' => $subkey]; $this->drupalGet('search/user'); diff --git a/core/modules/user/tests/src/Functional/Views/BulkFormAccessTest.php b/core/modules/user/tests/src/Functional/Views/BulkFormAccessTest.php index 1841c4ab4d3..575af2625d2 100644 --- a/core/modules/user/tests/src/Functional/Views/BulkFormAccessTest.php +++ b/core/modules/user/tests/src/Functional/Views/BulkFormAccessTest.php @@ -65,7 +65,7 @@ class BulkFormAccessTest extends UserTestBase { $no_edit_user = User::load($no_edit_user->id()); $this->assertFalse($no_edit_user->isBlocked(), 'The user is not blocked.'); - // Create a normal user which can be edited by the admin user + // Create a normal user which can be edited by the admin user. $normal_user = $this->drupalCreateUser(); $this->assertTrue($normal_user->access('update', $admin_user)); diff --git a/core/modules/user/tests/src/FunctionalJavascript/PasswordConfirmWidgetTest.php b/core/modules/user/tests/src/FunctionalJavascript/PasswordConfirmWidgetTest.php index 7f63bec926e..c7441b55126 100644 --- a/core/modules/user/tests/src/FunctionalJavascript/PasswordConfirmWidgetTest.php +++ b/core/modules/user/tests/src/FunctionalJavascript/PasswordConfirmWidgetTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\user\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JS components added to the PasswordConfirm render element. - * - * @group user */ +#[Group('user')] class PasswordConfirmWidgetTest extends WebDriverTestBase { /** diff --git a/core/modules/user/tests/src/FunctionalJavascript/PermissionFilterTest.php b/core/modules/user/tests/src/FunctionalJavascript/PermissionFilterTest.php index f961061c2c4..2a56dcb2d7c 100644 --- a/core/modules/user/tests/src/FunctionalJavascript/PermissionFilterTest.php +++ b/core/modules/user/tests/src/FunctionalJavascript/PermissionFilterTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\user\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript functionality of the permission filter. - * - * @group user */ +#[Group('user')] class PermissionFilterTest extends WebDriverTestBase { /** diff --git a/core/modules/user/tests/src/FunctionalJavascript/RegistrationWithUserFieldsTest.php b/core/modules/user/tests/src/FunctionalJavascript/RegistrationWithUserFieldsTest.php index 7361ea0698c..120bf2e44f1 100644 --- a/core/modules/user/tests/src/FunctionalJavascript/RegistrationWithUserFieldsTest.php +++ b/core/modules/user/tests/src/FunctionalJavascript/RegistrationWithUserFieldsTest.php @@ -4,16 +4,16 @@ declare(strict_types=1); namespace Drupal\Tests\user\FunctionalJavascript; -use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests user registration forms with additional fields. - * - * @group user */ +#[Group('user')] class RegistrationWithUserFieldsTest extends WebDriverTestBase { /** diff --git a/core/modules/user/tests/src/FunctionalJavascript/UserPasswordResetTest.php b/core/modules/user/tests/src/FunctionalJavascript/UserPasswordResetTest.php index ab815c20cdb..394e82442f1 100644 --- a/core/modules/user/tests/src/FunctionalJavascript/UserPasswordResetTest.php +++ b/core/modules/user/tests/src/FunctionalJavascript/UserPasswordResetTest.php @@ -10,12 +10,12 @@ use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\Entity\User; +use PHPUnit\Framework\Attributes\Group; /** * Ensure that password reset methods work as expected. - * - * @group user */ +#[Group('user')] class UserPasswordResetTest extends WebDriverTestBase { use AssertMailTrait { @@ -93,7 +93,7 @@ class UserPasswordResetTest extends WebDriverTestBase { $resetURL = $this->getResetURL(); $this->drupalGet($resetURL); - // Login + // Login. $this->submitForm([], 'Log in'); // Generate file. diff --git a/core/modules/user/tests/src/FunctionalJavascript/UserPermissionsTest.php b/core/modules/user/tests/src/FunctionalJavascript/UserPermissionsTest.php index d467a2fbabf..6c2e80c438b 100644 --- a/core/modules/user/tests/src/FunctionalJavascript/UserPermissionsTest.php +++ b/core/modules/user/tests/src/FunctionalJavascript/UserPermissionsTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\user\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\user\RoleInterface; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JS components added to the user permissions page. - * - * @group user */ +#[Group('user')] class UserPermissionsTest extends WebDriverTestBase { /** diff --git a/core/modules/user/tests/src/FunctionalJavascript/UserRegisterFormTest.php b/core/modules/user/tests/src/FunctionalJavascript/UserRegisterFormTest.php index 6e0d600aa3b..e35ee0b19b1 100644 --- a/core/modules/user/tests/src/FunctionalJavascript/UserRegisterFormTest.php +++ b/core/modules/user/tests/src/FunctionalJavascript/UserRegisterFormTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\user\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests user registration forms via JS. - * - * @group user */ +#[Group('user')] class UserRegisterFormTest extends WebDriverTestBase { /** diff --git a/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserConfigsTest.php b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserConfigsTest.php index 538a26eabbd..cb34fc2ee84 100644 --- a/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserConfigsTest.php +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserConfigsTest.php @@ -59,7 +59,7 @@ class MigrateUserConfigsTest extends MigrateDrupal6TestBase { // Tests migration of user_register using the AccountSettingsForm. - // Map D6 value to D8 value + // Map D6 value to D8 value. $user_register_map = [ [0, UserInterface::REGISTER_ADMINISTRATORS_ONLY], [1, UserInterface::REGISTER_VISITORS], @@ -67,7 +67,7 @@ class MigrateUserConfigsTest extends MigrateDrupal6TestBase { ]; foreach ($user_register_map as $map) { - // Tests migration of user_register = 1 + // Tests migration of "user_register = 1". Database::getConnection('default', 'migrate') ->update('variable') ->fields(['value' => serialize($map[0])]) diff --git a/core/modules/user/tests/src/Kernel/UserDeleteTest.php b/core/modules/user/tests/src/Kernel/UserDeleteTest.php index 148e9f24139..d59e507d19f 100644 --- a/core/modules/user/tests/src/Kernel/UserDeleteTest.php +++ b/core/modules/user/tests/src/Kernel/UserDeleteTest.php @@ -40,7 +40,7 @@ class UserDeleteTest extends KernelTestBase { $uids = [$user_a->id(), $user_b->id(), $user_c->id()]; - // These users should have a role + // These users should have a role. $connection = Database::getConnection(); $query = $connection->select('user__roles', 'r'); $roles_created = $query diff --git a/core/modules/user/tests/src/Kernel/UserRequirementsTest.php b/core/modules/user/tests/src/Kernel/UserRequirementsTest.php index 146ab9c8b90..746370a15d6 100644 --- a/core/modules/user/tests/src/Kernel/UserRequirementsTest.php +++ b/core/modules/user/tests/src/Kernel/UserRequirementsTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\user\Kernel; use Drupal\KernelTests\KernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\user\Traits\UserCreationTrait; /** @@ -70,4 +71,21 @@ class UserRequirementsTest extends KernelTestBase { $this->assertArrayNotHasKey('conflicting emails', $output); } + /** + * Tests that the requirements check does not flag user translations. + */ + public function testTranslatedUserEmail(): void { + \Drupal::service('module_installer')->install(['language']); + ConfigurableLanguage::createFromLangcode('is')->save(); + + $output = $this->moduleHandler->invoke('user', 'runtime_requirements'); + $this->assertArrayNotHasKey('conflicting emails', $output); + + $user = $this->createUser([], 'User A', FALSE, ['mail' => 'unique@example.com']); + $user->addTranslation('is')->save(); + + $output = $this->moduleHandler->invoke('user', 'runtime_requirements'); + $this->assertArrayNotHasKey('conflicting emails', $output); + } + } diff --git a/core/modules/user/tests/src/Kernel/Views/ArgumentValidateTest.php b/core/modules/user/tests/src/Kernel/Views/ArgumentValidateTest.php index ed4049eded3..ab12bb8381c 100644 --- a/core/modules/user/tests/src/Kernel/Views/ArgumentValidateTest.php +++ b/core/modules/user/tests/src/Kernel/Views/ArgumentValidateTest.php @@ -62,7 +62,7 @@ class ArgumentValidateTest extends ViewsKernelTestBase { $this->assertTrue($view->argument['null']->validateArgument($this->account->id())); // Reset argument validation. $view->argument['null']->argument_validated = NULL; - // Fail for a valid numeric, but for a user that doesn't exist + // Fail for a valid numeric, but for a user that doesn't exist. $this->assertFalse($view->argument['null']->validateArgument(32)); $form = []; @@ -82,7 +82,7 @@ class ArgumentValidateTest extends ViewsKernelTestBase { $this->assertTrue($view->argument['null']->validateArgument($this->account->getAccountName())); // Reset argument validation. $view->argument['null']->argument_validated = NULL; - // Fail for a valid string, but for a user that doesn't exist + // Fail for a valid string, but for a user that doesn't exist. $this->assertFalse($view->argument['null']->validateArgument($this->randomMachineName())); } diff --git a/core/modules/user/tests/src/Unit/Views/Argument/RolesRidTest.php b/core/modules/user/tests/src/Unit/Views/Argument/RolesRidTest.php index 53538044b7e..a608dd81732 100644 --- a/core/modules/user/tests/src/Unit/Views/Argument/RolesRidTest.php +++ b/core/modules/user/tests/src/Unit/Views/Argument/RolesRidTest.php @@ -31,7 +31,7 @@ class RolesRidTest extends UnitTestCase { 'label' => 'test <strong>rid 2</strong>', ], 'user_role'); - // Creates a stub entity storage; + // Creates a stub entity storage. $role_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface'); $role_storage->expects($this->any()) ->method('loadMultiple') diff --git a/core/modules/user/user.module b/core/modules/user/user.module index b2bb0d9f833..39eb3c9e6e8 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -112,19 +112,6 @@ function user_is_blocked($name) { } /** - * Implements hook_preprocess_HOOK() for block templates. - */ -function user_preprocess_block(&$variables): void { - if ($variables['configuration']['provider'] == 'user') { - switch ($variables['elements']['#plugin_id']) { - case 'user_login_block': - $variables['attributes']['role'] = 'form'; - break; - } - } -} - -/** * Prepares variables for username templates. * * Default template: username.html.twig. diff --git a/core/modules/views/config/schema/views.data_types.schema.yml b/core/modules/views/config/schema/views.data_types.schema.yml index 0e011d73dc1..31c6b2f6479 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -633,29 +633,33 @@ views_pager: offset: type: integer label: 'Offset' + constraints: + PositiveOrZero: [] pagination_heading_level: + # Validating against a string, but the list is populated by a protected + # property of the plugin. This could be a callback in the future. type: string label: 'Pager header element' items_per_page: type: integer label: 'Items per page' + constraints: + PositiveOrZero: [] views_pager_sql: type: views_pager label: 'SQL pager' mapping: - items_per_page: - type: integer - label: 'Items per page' - pagination_heading_level: - type: string - label: 'Pager header element' total_pages: type: integer label: 'Number of pages' + constraints: + PositiveOrZero: [] id: type: integer label: 'Pager ID' + constraints: + PositiveOrZero: [] tags: type: mapping label: 'Pager link labels' @@ -669,6 +673,8 @@ views_pager_sql: quantity: type: integer label: 'Number of pager links visible' + constraints: + PositiveOrZero: [] expose: type: mapping label: 'Exposed options' @@ -682,6 +688,11 @@ views_pager_sql: items_per_page_options: type: string label: 'Exposed items per page options' + constraints: + # Comma separated list of integers, with optional space in between. + Regex: + pattern: '/^(\d+)(,\s*\d+)*$/' + message: 'Per page should be a valid list of integers.' items_per_page_options_all: type: boolean label: 'Include all items option' diff --git a/core/modules/views/config/schema/views.field.schema.yml b/core/modules/views/config/schema/views.field.schema.yml index 4f47cb6141e..1cddb405f7f 100644 --- a/core/modules/views/config/schema/views.field.schema.yml +++ b/core/modules/views/config/schema/views.field.schema.yml @@ -119,15 +119,6 @@ views.field.numeric: format_plural_string: type: plural_label label: 'Plural variants' - constraints: - Regex: - # Normally, labels cannot contain invisible control characters. In this particular - # case, an invisible character (ASCII 3, 0x03) is used to encode translation - # information, so carve out an exception for that only. - # @see \Drupal\views\Plugin\views\field\NumericField - pattern: '/([^\PC\x03])/u' - match: false - message: 'Labels are not allowed to span multiple lines or contain control characters.' prefix: type: label label: 'Prefix' diff --git a/core/modules/views/config/schema/views.pager.schema.yml b/core/modules/views/config/schema/views.pager.schema.yml index eb360227e02..072dbc5b87e 100644 --- a/core/modules/views/config/schema/views.pager.schema.yml +++ b/core/modules/views/config/schema/views.pager.schema.yml @@ -33,3 +33,5 @@ views.pager.full: quantity: type: integer label: 'Number of pager links visible' + constraints: + PositiveOrZero: [] diff --git a/core/modules/views/src/EventSubscriber/RouteSubscriber.php b/core/modules/views/src/EventSubscriber/RouteSubscriber.php index 6e6ca6e6544..23e4640de98 100644 --- a/core/modules/views/src/EventSubscriber/RouteSubscriber.php +++ b/core/modules/views/src/EventSubscriber/RouteSubscriber.php @@ -76,7 +76,7 @@ class RouteSubscriber extends RouteSubscriberBase { public static function getSubscribedEvents(): array { $events = parent::getSubscribedEvents(); $events[RoutingEvents::FINISHED] = ['routeRebuildFinished']; - // Ensure to run after the entity resolver subscriber + // Ensure to run after the entity resolver subscriber. // @see \Drupal\Core\EventSubscriber\EntityRouteAlterSubscriber $events[RoutingEvents::ALTER] = ['onAlterRoutes', -175]; diff --git a/core/modules/views/src/Form/ViewsExposedForm.php b/core/modules/views/src/Form/ViewsExposedForm.php index 9f90160ff55..e623618c5ae 100644 --- a/core/modules/views/src/Form/ViewsExposedForm.php +++ b/core/modules/views/src/Form/ViewsExposedForm.php @@ -79,7 +79,7 @@ class ViewsExposedForm extends FormBase implements WorkspaceSafeFormInterface { // Let form plugins know this is for exposed widgets. $form_state->set('exposed', TRUE); - // Check if the form was already created + // Check if the form was already created. if ($cache = $this->exposedFormCache->getForm($view->storage->id(), $view->current_display)) { return $cache; } diff --git a/core/modules/views/src/Hook/ViewsHooks.php b/core/modules/views/src/Hook/ViewsHooks.php index b309887723c..136714de6c9 100644 --- a/core/modules/views/src/Hook/ViewsHooks.php +++ b/core/modules/views/src/Hook/ViewsHooks.php @@ -96,7 +96,7 @@ class ViewsHooks { \Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme'); // Some quasi clever array merging here. $base = ['file' => 'views.theme.inc']; - // Our extra version of pager + // Our extra version of pager. $hooks['views_mini_pager'] = $base + [ 'variables' => [ 'tags' => [], @@ -151,7 +151,7 @@ class ViewsHooks { 'parameters' => [], ], ]; - // Default view themes + // Default view themes. $hooks['views_view_field'] = $base + ['variables' => ['view' => NULL, 'field' => NULL, 'row' => NULL]]; $hooks['views_view_grouping'] = $base + [ 'variables' => [ @@ -375,7 +375,7 @@ class ViewsHooks { #[Hook('view_presave')] public function viewPresave(ViewEntityInterface $view): void { /** @var \Drupal\views\ViewsConfigUpdater $config_updater */ - $config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $config_updater = \Drupal::service(ViewsConfigUpdater::class); $config_updater->updateAll($view); } diff --git a/core/modules/views/src/ManyToOneHelper.php b/core/modules/views/src/ManyToOneHelper.php index 56afc646c4b..62b3ead7021 100644 --- a/core/modules/views/src/ManyToOneHelper.php +++ b/core/modules/views/src/ManyToOneHelper.php @@ -92,7 +92,7 @@ class ManyToOneHelper { // need to create a new relationship to use. $relationship = $this->handler->relationship; - // Determine the primary table to seek + // Determine the primary table to seek. if (empty($this->handler->query->relationships[$relationship])) { $base_table = $this->handler->view->storage->get('base_table'); } @@ -140,7 +140,7 @@ class ManyToOneHelper { $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field; $join = $this->getJoin(); - // Shortcuts + // Shortcuts. $options = $this->handler->options; $view = $this->handler->view; $query = $this->handler->query; diff --git a/core/modules/views/src/Plugin/views/HandlerBase.php b/core/modules/views/src/Plugin/views/HandlerBase.php index b3503f44218..7c74d7244c5 100644 --- a/core/modules/views/src/Plugin/views/HandlerBase.php +++ b/core/modules/views/src/Plugin/views/HandlerBase.php @@ -703,7 +703,7 @@ abstract class HandlerBase extends PluginBase implements ViewsHandlerInterface { */ public function getJoin() { // Get the join from this table that links back to the base table. - // Determine the primary table to seek + // Determine the primary table to seek. if (empty($this->query->relationships[$this->relationship])) { $base_table = $this->view->storage->get('base_table'); } @@ -878,7 +878,7 @@ abstract class HandlerBase extends PluginBase implements ViewsHandlerInterface { */ public function displayExposedForm($form, FormStateInterface $form_state) { $item = &$this->options; - // Flip + // Flip. $item['exposed'] = empty($item['exposed']); // If necessary, set new defaults: @@ -958,7 +958,7 @@ abstract class HandlerBase extends PluginBase implements ViewsHandlerInterface { $form_state->get('rerender', TRUE); $form_state->setRebuild(); - // Write to cache + // Write to cache. $view->cacheSet(); } diff --git a/core/modules/views/src/Plugin/views/area/View.php b/core/modules/views/src/Plugin/views/area/View.php index b15e72c0864..d1949d79a8d 100644 --- a/core/modules/views/src/Plugin/views/area/View.php +++ b/core/modules/views/src/Plugin/views/area/View.php @@ -111,11 +111,11 @@ class View extends AreaPluginBase { } $view->setDisplay($display_id); - // Avoid recursion + // Avoid recursion. $view->parent_views += $this->view->parent_views; $view->parent_views[] = "$view_name:$display_id"; - // Check if the view is part of the parent views of this view + // Check if the view is part of the parent views of this view. $search = "$view_name:$display_id"; if (in_array($search, $this->view->parent_views)) { \Drupal::messenger()->addError($this->t("Recursion detected in view @view display @display.", ['@view' => $view_name, '@display' => $display_id])); diff --git a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php index 1caeead7b39..7746046b6bf 100644 --- a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php +++ b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php @@ -511,7 +511,7 @@ abstract class ArgumentPluginBase extends HandlerBase implements CacheableDepend $plugin->validateOptionsForm($form['argument_default'][$default_id], $form_state, $option_values['argument_default'][$default_id]); } - // Summary plugin + // Summary plugin. $summary_id = $option_values['summary']['format']; $plugin = $this->getPlugin('style', $summary_id); if ($plugin) { @@ -548,7 +548,7 @@ abstract class ArgumentPluginBase extends HandlerBase implements CacheableDepend $option_values['default_argument_options'] = $options; } - // Summary plugin + // Summary plugin. $summary_id = $option_values['summary']['format']; $plugin = $this->getPlugin('style', $summary_id); if ($plugin) { @@ -1000,7 +1000,7 @@ abstract class ArgumentPluginBase extends HandlerBase implements CacheableDepend * code that goes into summaryQuery() */ public function summaryBasics($count_field = TRUE) { - // Add the number of nodes counter + // Add the number of nodes counter. $distinct = ($this->view->display_handler->getOption('distinct') && empty($this->query->no_distinct)); $count_alias = $this->query->addField($this->view->storage->get('base_table'), $this->view->storage->get('base_field'), 'num_records', ['count' => TRUE, 'distinct' => $distinct]); diff --git a/core/modules/views/src/Plugin/views/argument/DayDate.php b/core/modules/views/src/Plugin/views/argument/DayDate.php index a4a94da0699..fa1a1ebfe48 100644 --- a/core/modules/views/src/Plugin/views/argument/DayDate.php +++ b/core/modules/views/src/Plugin/views/argument/DayDate.php @@ -28,7 +28,7 @@ class DayDate extends Date { public function summaryName($data) { $day = str_pad($data->{$this->name_alias}, 2, '0', STR_PAD_LEFT); // strtotime() respects server timezone, so we need to set the time fixed - // as utc time + // as utc time. return $this->dateFormatter->format(strtotime("200505" . $day . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); } diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index d3adc61de5a..5c7d9225f0b 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -763,7 +763,7 @@ abstract class DisplayPluginBase extends PluginBase implements DisplayPluginInte return $this->view->displayHandlers->get($display_id)->getRoutedDisplay(); } - // No routed display exists, so return NULL + // No routed display exists, so return NULL. return NULL; } diff --git a/core/modules/views/src/Plugin/views/display/PathPluginBase.php b/core/modules/views/src/Plugin/views/display/PathPluginBase.php index 65eaa6991e4..214b1e1026d 100644 --- a/core/modules/views/src/Plugin/views/display/PathPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/PathPluginBase.php @@ -213,7 +213,7 @@ abstract class PathPluginBase extends DisplayPluginBase implements DisplayRouter // Store whether the view will return a response. $route->setOption('returns_response', !empty($this->getPluginDefinition()['returns_response'])); - // Symfony 4 requires that UTF-8 route patterns have the "utf8" option set + // Symfony 4 requires that UTF-8 route patterns have the "utf8" option set. $route->setOption('utf8', TRUE); return $route; diff --git a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php index ec4808deae9..c6d3e39405a 100644 --- a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php +++ b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php @@ -201,7 +201,7 @@ abstract class ExposedFormPluginBase extends PluginBase implements CacheableDepe $form['actions']['submit']['#value'] = $this->options['submit_button']; } - // Check if there is exposed sorts for this view + // Check if there is exposed sorts for this view. $exposed_sorts = []; $exposed_sorts_options = []; foreach ($this->view->sort as $id => $handler) { diff --git a/core/modules/views/src/Plugin/views/field/Custom.php b/core/modules/views/src/Plugin/views/field/Custom.php index 29c840d2790..870983b557a 100644 --- a/core/modules/views/src/Plugin/views/field/Custom.php +++ b/core/modules/views/src/Plugin/views/field/Custom.php @@ -48,7 +48,7 @@ class Custom extends FieldPluginBase { public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); - // Remove the checkbox + // Remove the checkbox. unset($form['alter']['alter_text']); unset($form['alter']['text']['#states']); unset($form['alter']['help']['#states']); diff --git a/core/modules/views/src/Plugin/views/field/EntityField.php b/core/modules/views/src/Plugin/views/field/EntityField.php index ad9f935e160..9e1de3ee8e6 100644 --- a/core/modules/views/src/Plugin/views/field/EntityField.php +++ b/core/modules/views/src/Plugin/views/field/EntityField.php @@ -227,7 +227,7 @@ class EntityField extends FieldPluginBase implements CacheableDependencyInterfac $this->limit_values = TRUE; } - // If "First and last only" is chosen, limit the values + // If "First and last only" is chosen, limit the values. if (!empty($this->options['delta_first_last'])) { $this->limit_values = TRUE; } diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index b60cfbf199d..7fdd3dd024e 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -193,7 +193,7 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf */ protected function addAdditionalFields($fields = NULL) { if (!isset($fields)) { - // Notice check + // Notice check. if (empty($this->additional_fields)) { return; } @@ -1565,7 +1565,7 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf } $alt = $this->viewsTokenReplace($alter['alt'], $tokens); - // Set the title attribute of the link only if it improves accessibility + // Set the title attribute of the link only if it improves accessibility. if ($alt && $alt != $text) { $options['attributes']['title'] = Html::decodeEntities($alt); } @@ -1630,7 +1630,7 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf $final_url = CoreUrl::fromUri($path, $options); // Build the link based on our altered Url object, adding on the optional - // prefix and suffix + // prefix and suffix. $render = [ '#type' => 'link', '#title' => $text, @@ -1885,7 +1885,7 @@ abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterf $value = $matches[1]; } } - // Remove scraps of HTML entities from the end of a strings + // Remove scraps of HTML entities from the end of a strings. $value = rtrim(preg_replace('/(?:<(?!.+>)|&(?!.+;)).*$/us', '', $value)); if (!empty($alter['ellipsis'])) { diff --git a/core/modules/views/src/Plugin/views/filter/Combine.php b/core/modules/views/src/Plugin/views/filter/Combine.php index ffb13966844..4ab58875333 100644 --- a/core/modules/views/src/Plugin/views/filter/Combine.php +++ b/core/modules/views/src/Plugin/views/filter/Combine.php @@ -35,7 +35,7 @@ class Combine extends StringFilter { parent::buildOptionsForm($form, $form_state); $this->view->initStyle(); - // Allow to choose all fields as possible + // Allow to choose all fields as possible. if ($this->view->style_plugin->usesFields()) { $options = []; foreach ($this->view->display_handler->getHandlers('field') as $name => $field) { diff --git a/core/modules/views/src/Plugin/views/filter/StringFilter.php b/core/modules/views/src/Plugin/views/filter/StringFilter.php index 233c2067d07..54bf215b8d6 100644 --- a/core/modules/views/src/Plugin/views/filter/StringFilter.php +++ b/core/modules/views/src/Plugin/views/filter/StringFilter.php @@ -389,7 +389,7 @@ class StringFilter extends FilterPluginBase implements FilterOperatorsInterface $operator = $this->getConditionOperator('LIKE'); foreach ($matches as $match) { $phrase = FALSE; - // Strip off phrase quotes + // Strip off phrase quotes. if ($match[2][0] == '"') { $match[2] = substr($match[2], 1, -1); $phrase = TRUE; diff --git a/core/modules/views/src/Plugin/views/join/JoinPluginBase.php b/core/modules/views/src/Plugin/views/join/JoinPluginBase.php index db329710cb7..92b8ee4d1e4 100644 --- a/core/modules/views/src/Plugin/views/join/JoinPluginBase.php +++ b/core/modules/views/src/Plugin/views/join/JoinPluginBase.php @@ -398,7 +398,7 @@ class JoinPluginBase extends PluginBase implements JoinPluginInterface { } // Convert a single-valued array of values to the single-value case, - // and transform from IN() notation to = notation + // and transform from IN() notation to = notation. if (is_array($info['value']) && count($info['value']) == 1) { $info['value'] = array_shift($info['value']); } diff --git a/core/modules/views/src/Plugin/views/pager/Full.php b/core/modules/views/src/Plugin/views/pager/Full.php index 0176fc6e7f9..ed8f7d7566a 100644 --- a/core/modules/views/src/Plugin/views/pager/Full.php +++ b/core/modules/views/src/Plugin/views/pager/Full.php @@ -79,8 +79,8 @@ class Full extends SqlBase { * {@inheritdoc} */ public function render($input) { - // The 0, 1, 3, 4 indexes are correct. See the template_preprocess_pager() - // documentation. + // The 0, 1, 3, 4 indexes are correct. See the + // \Drupal\Core\Pager\PagerPreprocess::preprocessPager() documentation. $tags = [ 0 => $this->options['tags']['first'], 1 => $this->options['tags']['previous'], diff --git a/core/modules/views/src/Plugin/views/pager/Mini.php b/core/modules/views/src/Plugin/views/pager/Mini.php index 0f95f7a0d2f..e17aa7fabdd 100644 --- a/core/modules/views/src/Plugin/views/pager/Mini.php +++ b/core/modules/views/src/Plugin/views/pager/Mini.php @@ -90,7 +90,8 @@ class Mini extends SqlBase { * {@inheritdoc} */ public function render($input) { - // The 1, 3 indexes are correct, see template_preprocess_pager(). + // The 1, 3 indexes are correct, see + // \Drupal\Core\Pager\PagerPreprocess::preprocessPager(). $tags = [ 1 => $this->options['tags']['previous'], 3 => $this->options['tags']['next'], diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php index a3dd5d95cb7..597a4ee91a8 100644 --- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php +++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php @@ -190,7 +190,7 @@ abstract class QueryPluginBase extends PluginBase implements CacheableDependency $group = empty($groups) ? 1 : max(array_keys($groups)) + 1; } - // Create an empty group + // Create an empty group. if (empty($groups[$group])) { $groups[$group] = ['conditions' => [], 'args' => []]; } diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php index 815bce2b838..1e9620cecec 100644 --- a/core/modules/views/src/Plugin/views/query/Sql.php +++ b/core/modules/views/src/Plugin/views/query/Sql.php @@ -229,7 +229,7 @@ class Sql extends QueryPluginBase { 'join' => NULL, ]; - // Init the tables with our primary table + // Init the tables with our primary table. $this->tables[$base_table][$base_table] = [ 'count' => 1, 'alias' => $base_table, @@ -625,7 +625,7 @@ class Sql extends QueryPluginBase { * cannot be ensured. */ public function ensureTable($table, $relationship = NULL, ?JoinPluginBase $join = NULL) { - // Ensure a relationship + // Ensure a relationship. if (empty($relationship)) { $relationship = $this->view->storage->get('base_table'); } @@ -782,7 +782,7 @@ class Sql extends QueryPluginBase { } // First, if this is our link point/anchor table, just use the - // relationship + // relationship. if ($join->leftTable == $this->relationships[$relationship]['table']) { $join->leftTable = $relationship; } @@ -882,14 +882,14 @@ class Sql extends QueryPluginBase { $alias = $table . '_' . $field; } - // Make sure an alias is assigned + // Make sure an alias is assigned. $alias = $alias ?: $field; // PostgreSQL truncates aliases to 63 characters: // https://www.drupal.org/node/571548. // We limit the length of the original alias up to 60 characters - // to get a unique alias later if its have duplicates + // to get a unique alias later if its have duplicates. $alias = strtolower(substr($alias, 0, 60)); // Create a field info array. diff --git a/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php b/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php index 14225c3f445..96b6d33d1b2 100644 --- a/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php +++ b/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php @@ -191,7 +191,7 @@ class GroupwiseMax extends RelationshipPluginBase { // Either load another view, or create one on the fly. if ($options['subquery_view']) { $temp_view = Views::getView($options['subquery_view']); - // Remove all fields from default display + // Remove all fields from default display. unset($temp_view->display['default']['display_options']['fields']); } else { diff --git a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php index 79e537cfd67..9b5acc29d54 100644 --- a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php +++ b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php @@ -100,7 +100,7 @@ abstract class SortPluginBase extends HandlerBase implements CacheableDependency $form['expose_button'] = [ '#prefix' => '<div class="views-expose clearfix">', '#suffix' => '</div>', - // Should always come first + // Should always come first. '#weight' => -1000, ]; diff --git a/core/modules/views/src/Plugin/views/style/StylePluginBase.php b/core/modules/views/src/Plugin/views/style/StylePluginBase.php index ca8df3ba8fe..ffe742452c9 100644 --- a/core/modules/views/src/Plugin/views/style/StylePluginBase.php +++ b/core/modules/views/src/Plugin/views/style/StylePluginBase.php @@ -575,7 +575,7 @@ abstract class StylePluginBase extends PluginBase { $groupings = [['field' => $groupings, 'rendered' => $rendered]]; } - // Make sure fields are rendered + // Make sure fields are rendered. $this->renderFields($this->view->result); $sets = []; if ($groupings) { @@ -710,7 +710,7 @@ abstract class StylePluginBase extends PluginBase { // - HTML views are rendered inside a render context: then we want to // use ::render(), so that attachments and cacheability are bubbled. // - non-HTML views are rendered outside a render context: then we - // want to use ::renderInIsolation(), so that no bubbling happens + // want to use ::renderInIsolation(), so that no bubbling happens. if ($renderer->hasRenderContext()) { $renderer->render($data); } diff --git a/core/modules/views/src/Plugin/views/style/Table.php b/core/modules/views/src/Plugin/views/style/Table.php index 561628ac682..d14e433c2f3 100644 --- a/core/modules/views/src/Plugin/views/style/Table.php +++ b/core/modules/views/src/Plugin/views/style/Table.php @@ -191,7 +191,7 @@ class Table extends StylePluginBase implements CacheableDependencyInterface { } // If the field is the column, mark it so, or the column - // it's set to is a column, that's ok + // it's set to is a column, that's ok. if ($field == $column || $columns[$column] == $column && !empty($sanitized[$column])) { $sanitized[$field] = $column; } @@ -332,7 +332,7 @@ class Table extends StylePluginBase implements CacheableDependencyInterface { '#return_value' => $field, '#parents' => ['style_options', 'default'], '#id' => $radio_id, - // Because 'radio' doesn't fully support '#id' =( + // Because 'radio' doesn't fully support "'#id' =(". '#attributes' => ['id' => $radio_id], '#default_value' => $default, '#states' => [ @@ -395,13 +395,13 @@ class Table extends StylePluginBase implements CacheableDependencyInterface { ], ]; - // Markup for the field name + // Markup for the field name. $form['info'][$field]['name'] = [ '#markup' => $field_names[$field], ]; } - // Provide a radio for no default sort + // Provide a radio for no default sort. $form['default'][-1] = [ '#title' => $this->t('No default sort'), '#title_display' => 'invisible', diff --git a/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php b/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php index d53e93cecec..802c73144e6 100644 --- a/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php +++ b/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php @@ -722,7 +722,7 @@ abstract class WizardPluginBase extends PluginBase implements WizardInterface { * arrays of options for that display. */ protected function buildDisplayOptions($form, FormStateInterface $form_state) { - // Display: Default + // Display: Default. $display_options['default'] = $this->defaultDisplayOptions(); $display_options['default'] += [ 'filters' => [], @@ -731,17 +731,17 @@ abstract class WizardPluginBase extends PluginBase implements WizardInterface { $display_options['default']['filters'] += $this->defaultDisplayFilters($form, $form_state); $display_options['default']['sorts'] += $this->defaultDisplaySorts($form, $form_state); - // Display: Page + // Display: Page. if (!$form_state->isValueEmpty(['page', 'create'])) { $display_options['page'] = $this->pageDisplayOptions($form, $form_state); - // Display: Feed (attached to the page) + // Display: Feed (attached to the page). if (!$form_state->isValueEmpty(['page', 'feed'])) { $display_options['feed'] = $this->pageFeedDisplayOptions($form, $form_state); } } - // Display: Block + // Display: Block. if (!$form_state->isValueEmpty(['block', 'create'])) { $display_options['block'] = $this->blockDisplayOptions($form, $form_state); } @@ -773,13 +773,13 @@ abstract class WizardPluginBase extends PluginBase implements WizardInterface { // instances. $executable = $view->getExecutable(); - // Display: Default + // Display: Default. $default_display = $executable->newDisplay('default', 'Default', 'default'); foreach ($display_options['default'] as $option => $value) { $default_display->setOption($option, $value); } - // Display: Page + // Display: Page. if (isset($display_options['page'])) { $display = $executable->newDisplay('page', 'Page', 'page_1'); // The page display is usually the main one (from the user's point of diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php index 407260ed285..554c38cdaba 100644 --- a/core/modules/views/src/ViewExecutable.php +++ b/core/modules/views/src/ViewExecutable.php @@ -157,7 +157,7 @@ class ViewExecutable { // phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName, Drupal.Commenting.VariableComment.Missing public $feedIcons = []; - // Exposed widget input + // Exposed widget input. /** * All the form data from $form_state->getValues(). @@ -1177,7 +1177,7 @@ class ViewExecutable { // use whatever value the argument handler now has, not the raw value. $substitutions["{{ raw_arguments.$id }}"] = strip_tags(Html::decodeEntities($argument->getValue())); - // Test to see if we should use this argument's title + // Test to see if we should use this argument's title. if (!empty($argument->options['title_enable']) && !empty($argument->options['title'])) { $title = $argument->options['title']; } @@ -1385,7 +1385,7 @@ class ViewExecutable { $this->built = TRUE; $this->build_time = microtime(TRUE) - $start; - // Attach displays + // Attach displays. $this->attachDisplays(); // Let modules modify the view just after building it. @@ -1688,7 +1688,7 @@ class ViewExecutable { $this->preExecute($args); - // Execute the view + // Execute the view. $output = $this->display_handler->execute(); $this->postExecute(); @@ -1750,7 +1750,7 @@ class ViewExecutable { // Allow hook_views_pre_view() to set the dom_id, then ensure it is set. $this->dom_id = !empty($this->dom_id) ? $this->dom_id : hash('sha256', $this->storage->id() . \Drupal::time()->getRequestTime() . mt_rand()); - // Allow the display handler to set up for execution + // Allow the display handler to set up for execution. $this->display_handler->preExecute(); } @@ -2336,10 +2336,10 @@ class ViewExecutable { public function getHandler($display_id, $type, $id) { // Get info about the types so we can get the right data. $types = static::getHandlerTypes(); - // Initialize the display + // Initialize the display. $this->setDisplay($display_id); - // Get the existing configuration + // Get the existing configuration. $fields = $this->displayHandlers->get($display_id)->getOption($types[$type]['plural']); return $fields[$id] ?? NULL; diff --git a/core/modules/views/src/Views.php b/core/modules/views/src/Views.php index 87a2bc2f5d8..46393b68e0c 100644 --- a/core/modules/views/src/Views.php +++ b/core/modules/views/src/Views.php @@ -436,13 +436,13 @@ class Views { if (!isset(static::$handlerTypes)) { static::$handlerTypes = [ 'field' => [ - // Title + // Title. 'title' => static::t('Fields'), // Lowercase title for mid-sentence. 'ltitle' => static::t('fields'), // Singular title. 'stitle' => static::t('Field'), - // Singular lowercase title for mid sentence + // Singular lowercase title for mid sentence. 'lstitle' => static::t('field'), 'plural' => 'fields', ], diff --git a/core/modules/views/src/ViewsConfigUpdater.php b/core/modules/views/src/ViewsConfigUpdater.php index 9c5d38e10ac..c6a4fbc62d0 100644 --- a/core/modules/views/src/ViewsConfigUpdater.php +++ b/core/modules/views/src/ViewsConfigUpdater.php @@ -4,106 +4,38 @@ namespace Drupal\views; use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Config\TypedConfigManagerInterface; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * Provides a BC layer for modules providing old configurations. * * @internal */ -class ViewsConfigUpdater implements ContainerInjectionInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * The entity field manager. - * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface - */ - protected $entityFieldManager; - - /** - * The typed config manager. - * - * @var \Drupal\Core\Config\TypedConfigManagerInterface - */ - protected $typedConfigManager; - - /** - * The views data service. - * - * @var \Drupal\views\ViewsData - */ - protected $viewsData; - - /** - * The formatter plugin manager service. - * - * @var \Drupal\Component\Plugin\PluginManagerInterface - */ - protected $formatterPluginManager; +class ViewsConfigUpdater { /** * Flag determining whether deprecations should be triggered. - * - * @var bool */ - protected $deprecationsEnabled = TRUE; + protected bool $deprecationsEnabled = TRUE; /** * Stores which deprecations were triggered. - * - * @var bool */ - protected $triggeredDeprecations = []; + protected array $triggeredDeprecations = []; /** * ViewsConfigUpdater constructor. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager - * The entity field manager. - * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager - * The typed config manager. - * @param \Drupal\views\ViewsData $views_data - * The views data service. - * @param \Drupal\Component\Plugin\PluginManagerInterface $formatter_plugin_manager - * The formatter plugin manager service. */ public function __construct( - EntityTypeManagerInterface $entity_type_manager, - EntityFieldManagerInterface $entity_field_manager, - TypedConfigManagerInterface $typed_config_manager, - ViewsData $views_data, - PluginManagerInterface $formatter_plugin_manager, + private readonly EntityTypeManagerInterface $entityTypeManager, + private readonly EntityFieldManagerInterface $entityFieldManager, + private readonly TypedConfigManagerInterface $typedConfigManager, + private readonly ViewsData $viewsData, + #[Autowire(service: 'plugin.manager.field.formatter')] + private readonly PluginManagerInterface $formatterPluginManager, ) { - $this->entityTypeManager = $entity_type_manager; - $this->entityFieldManager = $entity_field_manager; - $this->typedConfigManager = $typed_config_manager; - $this->viewsData = $views_data; - $this->formatterPluginManager = $formatter_plugin_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager'), - $container->get('entity_field.manager'), - $container->get('config.typed'), - $container->get('views.views_data'), - $container->get('plugin.manager.field.formatter') - ); } /** @@ -112,11 +44,18 @@ class ViewsConfigUpdater implements ContainerInjectionInterface { * @param bool $enabled * Whether deprecations should be enabled. */ - public function setDeprecationsEnabled($enabled) { + public function setDeprecationsEnabled(bool $enabled): void { $this->deprecationsEnabled = $enabled; } /** + * Whether deprecations are enabled. + */ + public function areDeprecationsEnabled(): bool { + return $this->deprecationsEnabled; + } + + /** * Performs all required updates. * * @param \Drupal\views\ViewEntityInterface $view @@ -259,7 +198,7 @@ class ViewsConfigUpdater implements ContainerInjectionInterface { } $deprecations_triggered = &$this->triggeredDeprecations['2640994'][$view->id()]; - if ($this->deprecationsEnabled && $changed && !$deprecations_triggered) { + if ($this->areDeprecationsEnabled() && $changed && !$deprecations_triggered) { $deprecations_triggered = TRUE; @trigger_error(sprintf('The update to convert "numeric" arguments to "entity_target_id" for entity reference fields for view "%s" is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Profile, module and theme provided configuration should be updated. See https://www.drupal.org/node/3441945', $view->id()), E_USER_DEPRECATED); } @@ -351,7 +290,7 @@ class ViewsConfigUpdater implements ContainerInjectionInterface { } $deprecations_triggered = &$this->triggeredDeprecations['table_css_class'][$view->id()]; - if ($this->deprecationsEnabled && $changed && !$deprecations_triggered) { + if ($this->areDeprecationsEnabled() && $changed && !$deprecations_triggered) { $deprecations_triggered = TRUE; @trigger_error(sprintf('The update to add a default table CSS class for view "%s" is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Profile, module and theme provided configuration should be updated. See https://www.drupal.org/node/3499943', $view->id()), E_USER_DEPRECATED); } diff --git a/core/modules/views/src/ViewsDataHelper.php b/core/modules/views/src/ViewsDataHelper.php index a784843d018..2311ab90aa0 100644 --- a/core/modules/views/src/ViewsDataHelper.php +++ b/core/modules/views/src/ViewsDataHelper.php @@ -67,7 +67,7 @@ class ViewsDataHelper { $strings = []; $skip_bases = []; foreach ($table_data as $field => $info) { - // Collect table data from this table + // Collect table data from this table. if ($field == 'table') { // Calculate what tables this table can join to. if (!empty($info['join'])) { @@ -96,15 +96,15 @@ class ViewsDataHelper { } } foreach (['title', 'group', 'help', 'base', 'aliases'] as $string) { - // First, try the lowest possible level + // First, try the lowest possible level. if (!empty($info[$key][$string])) { $strings[$field][$key][$string] = $info[$key][$string]; } - // Then try the field level + // Then try the field level. elseif (!empty($info[$string])) { $strings[$field][$key][$string] = $info[$string]; } - // Finally, try the table level + // Finally, try the table level. elseif (!empty($table_data['table'][$string])) { $strings[$field][$key][$string] = $table_data['table'][$string]; } diff --git a/core/modules/views/tests/modules/views_test_config_updater/src/Hook/ViewsTestConfigUpdaterHooks.php b/core/modules/views/tests/modules/views_test_config_updater/src/Hook/ViewsTestConfigUpdaterHooks.php new file mode 100644 index 00000000000..77db1f9adc9 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config_updater/src/Hook/ViewsTestConfigUpdaterHooks.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\views_test_config_updater\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\views\ViewEntityInterface; +use Drupal\views\ViewsConfigUpdater; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * Hooks for the views_test_config_updater module. + */ +class ViewsTestConfigUpdaterHooks { + + public function __construct( + protected readonly ViewsConfigUpdater $viewsConfigUpdater, + #[Autowire(service: 'keyvalue')] + protected readonly KeyValueFactoryInterface $keyValueFactory, + ) { + + } + + /** + * Implements hook_ENTITY_TYPE_presave(). + */ + #[Hook('view_presave')] + public function viewPresave(ViewEntityInterface $view): void { + $this->keyValueFactory->get('views_test_config_updater')->set('deprecations_enabled', $this->viewsConfigUpdater->areDeprecationsEnabled()); + } + +} diff --git a/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.info.yml b/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.info.yml new file mode 100644 index 00000000000..1efb67ee48f --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.info.yml @@ -0,0 +1,6 @@ +name: 'Views Test Config Updater' +type: module +package: Testing +version: VERSION +dependencies: + - drupal:views diff --git a/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.post_update.php b/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.post_update.php new file mode 100644 index 00000000000..db4c780aa8a --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.post_update.php @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * Post update functions for Views Test Config Updater. + */ + +declare(strict_types=1); + +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\views\ViewEntityInterface; +use Drupal\views\ViewsConfigUpdater; + +/** + * Test post update to set deprecations disabled. + */ +function views_test_config_updater_post_update_set_deprecations_disabled(?array &$sandbox = NULL): void { + /** @var \Drupal\views\ViewsConfigUpdater $viewsConfigUpdater */ + $viewsConfigUpdater = \Drupal::service(ViewsConfigUpdater::class); + $viewsConfigUpdater->setDeprecationsEnabled(FALSE); + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', static fn (ViewEntityInterface $view): bool => TRUE); +} diff --git a/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataThemeHooks.php b/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataThemeHooks.php new file mode 100644 index 00000000000..c03ad9ba030 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataThemeHooks.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\views_test_data\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for views_test_data. + */ +class ViewsTestDataThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for views table templates. + */ + #[Hook('preprocess_views_view_table')] + public function preprocessViewsViewTable(&$variables): void { + if ($variables['view']->storage->id() == 'test_view_render') { + $views_render_test = \Drupal::state()->get('views_render.test'); + $views_render_test++; + \Drupal::state()->set('views_render.test', $views_render_test); + } + } + +} diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php index 0d1e494c705..9a35a3ca8b2 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php @@ -145,7 +145,7 @@ class QueryTest extends QueryPluginBase { } if ($match) { // If the query explicit defines fields to use, filter all others out. - // Filter out fields + // Filter out fields. if ($this->fields) { $element = array_intersect_key($element, $this->fields); } diff --git a/core/modules/views/tests/modules/views_test_data/views_test_data.module b/core/modules/views/tests/modules/views_test_data/views_test_data.module index 421dff3e88d..75259026c3d 100644 --- a/core/modules/views/tests/modules/views_test_data/views_test_data.module +++ b/core/modules/views/tests/modules/views_test_data/views_test_data.module @@ -44,17 +44,6 @@ function views_test_data_handler_test_access_callback_argument($argument = FALSE } /** - * Implements hook_preprocess_HOOK() for views table templates. - */ -function views_test_data_preprocess_views_view_table(&$variables): void { - if ($variables['view']->storage->id() == 'test_view_render') { - $views_render_test = \Drupal::state()->get('views_render.test'); - $views_render_test++; - \Drupal::state()->set('views_render.test', $views_render_test); - } -} - -/** * Prepares variables for the mapping row style test templates. * * Default template: views-view-mapping-test.html.twig. diff --git a/core/modules/views/tests/modules/views_test_rss/src/Hook/ViewsTestRssThemeHooks.php b/core/modules/views/tests/modules/views_test_rss/src/Hook/ViewsTestRssThemeHooks.php new file mode 100644 index 00000000000..1fe64d6aa8a --- /dev/null +++ b/core/modules/views/tests/modules/views_test_rss/src/Hook/ViewsTestRssThemeHooks.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\views_test_rss\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Hook implementations for views_test_rss. + */ +class ViewsTestRssThemeHooks { + use StringTranslationTrait; + + /** + * Implements hook_preprocess_HOOK(). + */ + #[Hook('preprocess_views_view_rss')] + public function preprocessViewsViewRss(&$variables): void { + $variables['channel_elements'][] = [ + '#type' => 'html_tag', + '#tag' => 'copyright', + '#value' => $this->t('Copyright 2019 Dries Buytaert'), + ]; + } + +} diff --git a/core/modules/views/tests/modules/views_test_rss/views_test_rss.module b/core/modules/views/tests/modules/views_test_rss/views_test_rss.module deleted file mode 100644 index 1b0dda574c4..00000000000 --- a/core/modules/views/tests/modules/views_test_rss/views_test_rss.module +++ /dev/null @@ -1,19 +0,0 @@ -<?php - -/** - * @file - * Hook implementations for this module. - */ - -declare(strict_types=1); - -/** - * Implements hook_preprocess_HOOK(). - */ -function views_test_rss_preprocess_views_view_rss(&$variables): void { - $variables['channel_elements'][] = [ - '#type' => 'html_tag', - '#tag' => 'copyright', - '#value' => t('Copyright 2019 Dries Buytaert'), - ]; -} diff --git a/core/modules/views/tests/src/Functional/GlossaryTest.php b/core/modules/views/tests/src/Functional/GlossaryTest.php index 25c08d5f159..317eda212e9 100644 --- a/core/modules/views/tests/src/Functional/GlossaryTest.php +++ b/core/modules/views/tests/src/Functional/GlossaryTest.php @@ -57,7 +57,7 @@ class GlossaryTest extends ViewTestBase { } } - // Execute glossary view + // Execute glossary view. $view = Views::getView('glossary'); $view->setDisplay('attachment_1'); $view->executeDisplay('attachment_1'); @@ -86,7 +86,7 @@ class GlossaryTest extends ViewTestBase { ], [ 'config:views.view.glossary', - // Listed for letter 'a' + // Listed for letter 'a'. 'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(), // Link for letter 'd'. 'node:1', diff --git a/core/modules/views/tests/src/Functional/Handler/HandlerTest.php b/core/modules/views/tests/src/Functional/Handler/HandlerTest.php index fa9af239ca3..b76b92a5b26 100644 --- a/core/modules/views/tests/src/Functional/Handler/HandlerTest.php +++ b/core/modules/views/tests/src/Functional/Handler/HandlerTest.php @@ -80,7 +80,7 @@ class HandlerTest extends ViewTestBase { // Check defaults. $this->assertEquals((object) ['value' => [], 'operator' => NULL], HandlerBase::breakString('')); - // Test ors + // Test ors. $handler = HandlerBase::breakString('word1 word2+word'); $this->assertEquals(['word1', 'word2', 'word'], $handler->value); $this->assertEquals('or', $handler->operator); @@ -114,13 +114,13 @@ class HandlerTest extends ViewTestBase { $this->assertEquals(['wõrd1', 'wõrd2', 'wõrd'], $handler->value); $this->assertEquals('and', $handler->operator); - // Test a single word + // Test a single word. $handler = HandlerBase::breakString('word'); $this->assertEquals(['word'], $handler->value); $this->assertEquals('and', $handler->operator); $s1 = $this->randomMachineName(); - // Generate three random numbers which can be used below; + // Generate three random numbers which can be used below. $n1 = rand(0, 100); $n2 = rand(0, 100); $n3 = rand(0, 100); @@ -168,7 +168,7 @@ class HandlerTest extends ViewTestBase { $this->assertEquals([(int) $s1, $n2, $n3], $handlerBase->value); $this->assertEquals('or', $handlerBase->operator); - // Generate three random decimals which can be used below; + // Generate three random decimals which can be used below. $d1 = rand(0, 10) / 10; $d2 = rand(0, 10) / 10; $d3 = rand(0, 10) / 10; diff --git a/core/modules/views/tests/src/Functional/Plugin/ArgumentDefaultTest.php b/core/modules/views/tests/src/Functional/Plugin/ArgumentDefaultTest.php index ebc7092dafb..cda2b3537bb 100644 --- a/core/modules/views/tests/src/Functional/Plugin/ArgumentDefaultTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/ArgumentDefaultTest.php @@ -127,7 +127,7 @@ class ArgumentDefaultTest extends ViewTestBase { $this->assertEquals($random, $view->argument['null']->getDefaultArgument(), 'Fixed argument should be used by default.'); - // Make sure that a normal argument provided is used + // Make sure that a normal argument provided is used. $random_string = $this->randomMachineName(); $view->executeDisplay('default', [$random_string]); diff --git a/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php b/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php index 5aecbea3e36..f3a824e48c6 100644 --- a/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php @@ -392,7 +392,7 @@ class DisplayTest extends ViewTestBase { // Remove the relationship used by other handlers. $view->removeHandler('default', 'relationship', 'uid'); - // Validate display + // Validate display. $errors = $view->validate(); // Check that the error messages are shown. $this->assertCount(2, $errors['default'], 'Error messages found for required relationship'); diff --git a/core/modules/views/tests/src/Functional/Plugin/MiniPagerTest.php b/core/modules/views/tests/src/Functional/Plugin/MiniPagerTest.php index 8852c9e7be4..d81ddc46562 100644 --- a/core/modules/views/tests/src/Functional/Plugin/MiniPagerTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/MiniPagerTest.php @@ -84,7 +84,7 @@ class MiniPagerTest extends ViewTestBase { $this->assertSession()->pageTextContains($this->nodes[18]->label()); $this->assertSession()->pageTextContains($this->nodes[19]->label()); - // Test @total value in result summary + // Test @total value in result summary. $view = Views::getView('test_mini_pager'); $view->setDisplay('page_4'); $this->executeView($view); diff --git a/core/modules/views/tests/src/Functional/Plugin/PagerTest.php b/core/modules/views/tests/src/Functional/Plugin/PagerTest.php index cf381282443..b981440107c 100644 --- a/core/modules/views/tests/src/Functional/Plugin/PagerTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/PagerTest.php @@ -334,7 +334,7 @@ class PagerTest extends ViewTestBase { $this->executeView($view); $this->assertCount(3, $view->result, 'Make sure that only a certain count of items is returned'); - // Test items per page = 0 + // Test items per page = 0. $view = Views::getView('test_view_pager_full_zero_items_per_page'); $this->executeView($view); diff --git a/core/modules/views/tests/src/Functional/ViewsConfigUpdaterTest.php b/core/modules/views/tests/src/Functional/ViewsConfigUpdaterTest.php new file mode 100644 index 00000000000..dc5b47eece7 --- /dev/null +++ b/core/modules/views/tests/src/Functional/ViewsConfigUpdaterTest.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\views\Functional; + +use Drupal\Core\Database\Database; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\UpdatePathTestTrait; + +/** + * Tests the views config updater service. + * + * @group views + */ +class ViewsConfigUpdaterTest extends BrowserTestBase { + + use UpdatePathTestTrait; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'views', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $connection = Database::getConnection(); + + // Enable views_test_config_updater via the database so post_update hooks + // can run. + $extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); + $extensions = unserialize($extensions); + $extensions['module']['views_test_config_updater'] = 0; + $connection->update('config') + ->fields([ + 'data' => serialize($extensions), + ]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); + } + + /** + * Tests the deprecationsEnabled flag persists from post_update to presave. + * + * @see views_test_config_updater_post_update_set_deprecations_disabled + * @see \Drupal\views_test_config_updater\Hook\ViewsTestConfigUpdaterHooks::viewPresave() + */ + public function testDeprecationsFlagPersists(): void { + $this->assertNull(\Drupal::keyValue('views_test_config_updater')->get('deprecations_enabled')); + + $this->runUpdates(); + + $this->assertFalse(\Drupal::keyValue('views_test_config_updater')->get('deprecations_enabled')); + } + +} diff --git a/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php index 76b782fdf19..b645ceaef11 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php @@ -8,12 +8,12 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the exposed filter ajax functionality in a block. - * - * @group views */ +#[Group('views')] class BlockExposedFilterAJAXTest extends WebDriverTestBase { use ContentTypeCreationTrait; diff --git a/core/modules/views/tests/src/FunctionalJavascript/ClickSortingAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/ClickSortingAJAXTest.php index 858c5ee2365..cbdbb5a242b 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/ClickSortingAJAXTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/ClickSortingAJAXTest.php @@ -8,12 +8,12 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the click sorting AJAX functionality of Views exposed forms. - * - * @group views */ +#[Group('views')] class ClickSortingAJAXTest extends WebDriverTestBase { use ContentTypeCreationTrait; diff --git a/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php index af7cad9e287..1ddbcdba9fc 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php @@ -8,12 +8,12 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the basic AJAX functionality of Views exposed forms. - * - * @group views */ +#[Group('views')] class ExposedFilterAJAXTest extends WebDriverTestBase { use ContentTypeCreationTrait; diff --git a/core/modules/views/tests/src/FunctionalJavascript/GlossaryViewTest.php b/core/modules/views/tests/src/FunctionalJavascript/GlossaryViewTest.php index 390046be412..f80bcc95619 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/GlossaryViewTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/GlossaryViewTest.php @@ -10,12 +10,12 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the basic AJAX functionality of the Glossary View. - * - * @group node */ +#[Group('node')] class GlossaryViewTest extends WebDriverTestBase { use ContentTypeCreationTrait; diff --git a/core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php index feba85b6172..f6da5d52dbe 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php @@ -8,12 +8,12 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the click sorting AJAX functionality of Views exposed forms. - * - * @group views */ +#[Group('views')] class PaginationAJAXTest extends WebDriverTestBase { use ContentTypeCreationTrait; diff --git a/core/modules/views/tests/src/FunctionalJavascript/Plugin/BulkOperationsTest.php b/core/modules/views/tests/src/FunctionalJavascript/Plugin/BulkOperationsTest.php index fb6bcab6de7..46885027d7d 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/Plugin/BulkOperationsTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/Plugin/BulkOperationsTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\views\FunctionalJavascript\Plugin; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the bulk operations. - * - * @group views */ +#[Group('views')] class BulkOperationsTest extends WebDriverTestBase { /** diff --git a/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/ContextualFilterTest.php b/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/ContextualFilterTest.php index 1987e585431..91baf920074 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/ContextualFilterTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/ContextualFilterTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\views\FunctionalJavascript\Plugin\views\Handler; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the contextual filter handler UI. - * - * @group views */ +#[Group('views')] class ContextualFilterTest extends WebDriverTestBase { /** diff --git a/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/FieldTest.php b/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/FieldTest.php index 1224c578594..6808086cac0 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/FieldTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/FieldTest.php @@ -10,12 +10,12 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\SchemaCheckTestTrait; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the field handler UI. - * - * @group views */ +#[Group('views')] class FieldTest extends WebDriverTestBase { use SchemaCheckTestTrait; diff --git a/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/FilterTest.php b/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/FilterTest.php index 6ecbeeff788..ce5c8eb52db 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/FilterTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/FilterTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\views\FunctionalJavascript\Plugin\views\Handler; use Drupal\field\Entity\FieldConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\node\Entity\NodeType; +use PHPUnit\Framework\Attributes\Group; /** * Tests the add filter handler UI. - * - * @group views */ +#[Group('views')] class FilterTest extends WebDriverTestBase { /** diff --git a/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/GroupedExposedFilterTest.php b/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/GroupedExposedFilterTest.php index 458d9053167..f1ae1c5a23e 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/GroupedExposedFilterTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/Plugin/views/Handler/GroupedExposedFilterTest.php @@ -8,12 +8,12 @@ use Drupal\field\Entity\FieldConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\node\Entity\NodeType; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the grouped exposed filter admin UI. - * - * @group views */ +#[Group('views')] class GroupedExposedFilterTest extends WebDriverTestBase { /** diff --git a/core/modules/views/tests/src/FunctionalJavascript/RedirectAjaxTest.php b/core/modules/views/tests/src/FunctionalJavascript/RedirectAjaxTest.php index 2cee37e5c95..8cfd23b2b8e 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/RedirectAjaxTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/RedirectAjaxTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\views\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests that the redirects work with Ajax enabled views. - * - * @group views */ +#[Group('views')] class RedirectAjaxTest extends WebDriverTestBase { use ContentTypeCreationTrait; diff --git a/core/modules/views/tests/src/Kernel/BasicTest.php b/core/modules/views/tests/src/Kernel/BasicTest.php index ba766c1e72c..dd0f5e452fc 100644 --- a/core/modules/views/tests/src/Kernel/BasicTest.php +++ b/core/modules/views/tests/src/Kernel/BasicTest.php @@ -101,7 +101,7 @@ class BasicTest extends ViewsKernelTestBase { * Tests simple argument. */ public function testSimpleArgument(): void { - // Execute with a view + // Execute with a view. $view = Views::getView('test_simple_argument'); $view->setArguments([27]); $this->executeView($view); diff --git a/core/modules/views/tests/src/Kernel/Entity/EntityViewsDataTest.php b/core/modules/views/tests/src/Kernel/Entity/EntityViewsDataTest.php index b53d7314047..0d62a0186ae 100644 --- a/core/modules/views/tests/src/Kernel/Entity/EntityViewsDataTest.php +++ b/core/modules/views/tests/src/Kernel/Entity/EntityViewsDataTest.php @@ -123,7 +123,7 @@ class EntityViewsDataTest extends KernelTestBase { ->setTranslatable(TRUE) ->setSetting('max_length', 255); - // A base field with cardinality > 1 + // A base field with cardinality > 1. $this->commonBaseFields['string'] = BaseFieldDefinition::create('string') ->setLabel('Strong') ->setTranslatable(TRUE) diff --git a/core/modules/views/tests/src/Kernel/EventSubscriber/ViewsEntitySchemaSubscriberIntegrationTest.php b/core/modules/views/tests/src/Kernel/EventSubscriber/ViewsEntitySchemaSubscriberIntegrationTest.php index a3bcc66be58..cc839de62cd 100644 --- a/core/modules/views/tests/src/Kernel/EventSubscriber/ViewsEntitySchemaSubscriberIntegrationTest.php +++ b/core/modules/views/tests/src/Kernel/EventSubscriber/ViewsEntitySchemaSubscriberIntegrationTest.php @@ -332,9 +332,9 @@ class ViewsEntitySchemaSubscriberIntegrationTest extends ViewsKernelTestBase { // base + translation <-> base + translation + revision // base + revision <-> base + translation + revision // base <-> base + revision - // base <-> base + translation + revision + // base <-> base + translation + revision. - // Base <-> base + translation + // Base <-> base + translation. $this->updateEntityTypeToTranslatable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -351,7 +351,7 @@ class ViewsEntitySchemaSubscriberIntegrationTest extends ViewsKernelTestBase { $this->resetEntityType(); - // Base + translation <-> base + translation + revision + // Base + translation <-> base + translation + revision. $this->updateEntityTypeToTranslatable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -375,7 +375,7 @@ class ViewsEntitySchemaSubscriberIntegrationTest extends ViewsKernelTestBase { $this->resetEntityType(); - // Base + revision <-> base + translation + revision + // Base + revision <-> base + translation + revision. $this->updateEntityTypeToRevisionable(); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -399,7 +399,7 @@ class ViewsEntitySchemaSubscriberIntegrationTest extends ViewsKernelTestBase { $this->resetEntityType(); - // Base <-> base + revision + // Base <-> base + revision. $this->updateEntityTypeToRevisionable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -416,7 +416,7 @@ class ViewsEntitySchemaSubscriberIntegrationTest extends ViewsKernelTestBase { $this->resetEntityType(); - // Base <-> base + translation + revision + // Base <-> base + translation + revision. $this->updateEntityTypeToRevisionable(TRUE); $this->updateEntityTypeToTranslatable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -445,7 +445,7 @@ class ViewsEntitySchemaSubscriberIntegrationTest extends ViewsKernelTestBase { * Tests some possible entity table updates for a revision view. */ public function testVariousTableUpdatesForRevisionView(): void { - // Base + revision <-> base + translation + revision + // Base + revision <-> base + translation + revision. $this->updateEntityTypeToRevisionable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(TRUE); diff --git a/core/modules/views/tests/src/Kernel/Handler/AreaTextTest.php b/core/modules/views/tests/src/Kernel/Handler/AreaTextTest.php index a96bb66e652..e8da8768519 100644 --- a/core/modules/views/tests/src/Kernel/Handler/AreaTextTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/AreaTextTest.php @@ -46,7 +46,7 @@ class AreaTextTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Add a text header + // Add a text header. $string = $this->randomMachineName(); $view->displayHandlers->get('default')->overrideOption('header', [ 'area' => [ diff --git a/core/modules/views/tests/src/Kernel/Handler/ArgumentNullTest.php b/core/modules/views/tests/src/Kernel/Handler/ArgumentNullTest.php index f48cbf89b40..9d2c8111146 100644 --- a/core/modules/views/tests/src/Kernel/Handler/ArgumentNullTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/ArgumentNullTest.php @@ -35,7 +35,7 @@ class ArgumentNullTest extends ViewsKernelTestBase { * Tests the NullArgument handler for text areas. */ public function testAreaText(): void { - // Test validation + // Test validation. $view = Views::getView('test_view'); $view->setDisplay(); diff --git a/core/modules/views/tests/src/Kernel/Handler/FieldBooleanTest.php b/core/modules/views/tests/src/Kernel/Handler/FieldBooleanTest.php index 2d1c79b71c3..cc3d66716d1 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FieldBooleanTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FieldBooleanTest.php @@ -25,7 +25,7 @@ class FieldBooleanTest extends ViewsKernelTestBase { * Modifies the default dataset by removing the age for specific entries. */ public function dataSet() { - // Use default dataset but remove the age from john and paul + // Use default dataset but remove the age from john and paul. $data = parent::dataSet(); $data[0]['age'] = 0; $data[3]['age'] = 0; diff --git a/core/modules/views/tests/src/Kernel/Handler/FieldKernelTest.php b/core/modules/views/tests/src/Kernel/Handler/FieldKernelTest.php index b8a0d79bee5..5aaf400fb0f 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FieldKernelTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FieldKernelTest.php @@ -808,7 +808,7 @@ class FieldKernelTest extends ViewsKernelTestBase { $this->assertEquals($expect[$key], $result_text); } - // Test also word_boundary + // Test also word_boundary. $alter['word_boundary'] = TRUE; $expect = [ 'Tuy nhiên', diff --git a/core/modules/views/tests/src/Kernel/Handler/FilterEqualityTest.php b/core/modules/views/tests/src/Kernel/Handler/FilterEqualityTest.php index aeb4d145c1e..fe00082e063 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FilterEqualityTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FilterEqualityTest.php @@ -51,7 +51,7 @@ class FilterEqualityTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'name' => [ 'id' => 'name', @@ -80,7 +80,7 @@ class FilterEqualityTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->newDisplay('page', 'Page', 'page_1'); - // Filter: Name, Operator: =, Value: Ringo + // Filter: Name, Operator: =, Value: Ringo. $filters['name']['group_info']['default_group'] = 1; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -102,7 +102,7 @@ class FilterEqualityTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'name' => [ 'id' => 'name', @@ -140,7 +140,7 @@ class FilterEqualityTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->newDisplay('page', 'Page', 'page_1'); - // Filter: Name, Operator: !=, Value: Ringo + // Filter: Name, Operator: !=, Value: Ringo. $filters['name']['group_info']['default_group'] = 2; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); diff --git a/core/modules/views/tests/src/Kernel/Handler/FilterInOperatorTest.php b/core/modules/views/tests/src/Kernel/Handler/FilterInOperatorTest.php index e812d2fd417..2bfbfcd63b2 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FilterInOperatorTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FilterInOperatorTest.php @@ -126,7 +126,7 @@ class FilterInOperatorTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = Views::getView('test_view'); - // Filter: Age, Operator: in, Value: 26, 30 + // Filter: Age, Operator: in, Value: 26, 30. $filters['age']['group_info']['default_group'] = 1; $view->setDisplay(); $view->displayHandlers->get('default')->overrideOption('filters', $filters); @@ -155,7 +155,7 @@ class FilterInOperatorTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = Views::getView('test_view'); - // Filter: Age, Operator: in, Value: 26, 30 + // Filter: Age, Operator: in, Value: 26, 30. $filters['age']['group_info']['default_group'] = 2; $view->setDisplay(); $view->displayHandlers->get('default')->overrideOption('filters', $filters); diff --git a/core/modules/views/tests/src/Kernel/Handler/FilterNumericTest.php b/core/modules/views/tests/src/Kernel/Handler/FilterNumericTest.php index b0a355f8dbd..3e049815fc7 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FilterNumericTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FilterNumericTest.php @@ -55,7 +55,7 @@ class FilterNumericTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'age' => [ 'id' => 'age', @@ -85,7 +85,7 @@ class FilterNumericTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->newDisplay('page', 'Page', 'page_1'); - // Filter: Age, Operator: =, Value: 28 + // Filter: Age, Operator: =, Value: 28. $filters['age']['group_info']['default_group'] = 1; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -212,7 +212,7 @@ class FilterNumericTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->newDisplay('page', 'Page', 'page_1'); - // Filter: Age, Operator: between, Value: 26 and 29 + // Filter: Age, Operator: between, Value: 26 and 29. $filters['age']['group_info']['default_group'] = 2; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -244,7 +244,7 @@ class FilterNumericTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->newDisplay('page', 'Page', 'page_1'); - // Filter: Age, Operator: between, Value: 26 and 29 + // Filter: Age, Operator: between, Value: 26 and 29. $filters['age']['group_info']['default_group'] = 3; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -348,7 +348,7 @@ class FilterNumericTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->newDisplay('page', 'Page', 'page_1'); - // Filter: Age, Operator: regular_expression, Value: 2[7-8] + // Filter: Age, Operator: regular_expression, Value: 2[7-8]. $filters['age']['group_info']['default_group'] = 6; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -379,7 +379,7 @@ class FilterNumericTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->newDisplay('page', 'Page', 'page_1'); - // Filter: Age, Operator: not_regular_expression, Value: 2[7-8] + // Filter: Age, Operator: not_regular_expression, Value: 2[7-8]. $filters['age']['group_info']['default_group'] = 7; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -410,7 +410,7 @@ class FilterNumericTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'age' => [ 'id' => 'age', @@ -428,7 +428,7 @@ class FilterNumericTest extends ViewsKernelTestBase { $view->destroy(); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'age' => [ 'id' => 'age', diff --git a/core/modules/views/tests/src/Kernel/Handler/FilterStringTest.php b/core/modules/views/tests/src/Kernel/Handler/FilterStringTest.php index 96d7f91c6fc..43434d16743 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FilterStringTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FilterStringTest.php @@ -102,7 +102,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'name' => [ 'id' => 'name', @@ -123,7 +123,7 @@ class FilterStringTest extends ViewsKernelTestBase { $this->assertIdenticalResultset($view, $resultset, $this->columnMap); $view->destroy(); - // Get the original dataset + // Get the original dataset. $data_set = $this->dataSet(); // Adds a new data point in the views_test_data table. $query = Database::getConnection()->insert('views_test_data') @@ -141,7 +141,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'name' => [ 'id' => 'name', @@ -169,7 +169,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: =, Value: Ringo + // Filter: Name, Operator: =, Value: Ringo. $filters['name']['group_info']['default_group'] = 1; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -193,7 +193,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'name' => [ 'id' => 'name', @@ -230,7 +230,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: !=, Value: Ringo + // Filter: Name, Operator: !=, Value: Ringo. $filters['name']['group_info']['default_group'] = '2'; $view->setDisplay('page_1'); @@ -264,7 +264,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'name' => [ 'id' => 'name', @@ -292,7 +292,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: contains, Value: ing + // Filter: Name, Operator: contains, Value: ing. $filters['name']['group_info']['default_group'] = '3'; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -316,7 +316,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'description' => [ 'id' => 'description', @@ -343,7 +343,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'description' => [ 'id' => 'description', @@ -407,7 +407,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: contains, Value: ing + // Filter: Name, Operator: contains, Value: ing. $filters['name']['group_info']['default_group'] = '3'; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -427,7 +427,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Description, Operator: contains, Value: actor + // Filter: Description, Operator: contains, Value: actor. $filters['description']['group_info']['default_group'] = '1'; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -451,7 +451,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'description' => [ 'id' => 'description', @@ -479,7 +479,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: starts, Value: George + // Filter: Name, Operator: starts, Value: George. $filters['description']['group_info']['default_group'] = 2; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -502,7 +502,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: not_regular_expression, Value: ^Rin + // Filter: Name, Operator: not_regular_expression, Value: ^Rin. $filters['name']['group_info']['default_group'] = 6; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -535,7 +535,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'description' => [ 'id' => 'description', @@ -558,7 +558,7 @@ class FilterStringTest extends ViewsKernelTestBase { [ 'name' => 'Paul', ], - // There is no Meredith returned because their description is empty + // There is no Meredith returned because their description is empty. ]; $this->assertIdenticalResultset($view, $resultset, $this->columnMap); } @@ -570,7 +570,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: not_starts, Value: George + // Filter: Name, Operator: not_starts, Value: George. $filters['description']['group_info']['default_group'] = 3; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -588,7 +588,7 @@ class FilterStringTest extends ViewsKernelTestBase { [ 'name' => 'Paul', ], - // There is no Meredith returned because their description is empty + // There is no Meredith returned because their description is empty. ]; $this->assertIdenticalResultset($view, $resultset, $this->columnMap); } @@ -600,7 +600,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'description' => [ 'id' => 'description', @@ -631,7 +631,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Description, Operator: ends, Value: Beatles + // Filter: Description, Operator: ends, Value: Beatles. $filters['description']['group_info']['default_group'] = 4; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -657,7 +657,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'description' => [ 'id' => 'description', @@ -677,7 +677,7 @@ class FilterStringTest extends ViewsKernelTestBase { [ 'name' => 'Paul', ], - // There is no Meredith returned because their description is empty + // There is no Meredith returned because their description is empty. ]; $this->assertIdenticalResultset($view, $resultset, $this->columnMap); } @@ -689,7 +689,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Description, Operator: not_ends, Value: Beatles + // Filter: Description, Operator: not_ends, Value: Beatles. $filters['description']['group_info']['default_group'] = 5; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -704,7 +704,7 @@ class FilterStringTest extends ViewsKernelTestBase { [ 'name' => 'Paul', ], - // There is no Meredith returned because their description is empty + // There is no Meredith returned because their description is empty. ]; $this->assertIdenticalResultset($view, $resultset, $this->columnMap); } @@ -716,7 +716,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'description' => [ 'id' => 'description', @@ -736,7 +736,7 @@ class FilterStringTest extends ViewsKernelTestBase { [ 'name' => 'Paul', ], - // There is no Meredith returned because their description is empty + // There is no Meredith returned because their description is empty. ]; $this->assertIdenticalResultset($view, $resultset, $this->columnMap); } @@ -748,7 +748,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Description, Operator: not (does not contains), Value: Beatles + // Filter: Description, Operator: not (does not contains), Value: Beatles. $filters['description']['group_info']['default_group'] = 6; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -763,7 +763,7 @@ class FilterStringTest extends ViewsKernelTestBase { [ 'name' => 'Paul', ], - // There is no Meredith returned because their description is empty + // There is no Meredith returned because their description is empty. ]; $this->assertIdenticalResultset($view, $resultset, $this->columnMap); @@ -776,7 +776,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'name' => [ 'id' => 'name', @@ -807,7 +807,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: shorterthan, Value: 5 + // Filter: Name, Operator: shorterthan, Value: 5. $filters['name']['group_info']['default_group'] = 4; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -832,7 +832,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'name' => [ 'id' => 'name', @@ -860,7 +860,7 @@ class FilterStringTest extends ViewsKernelTestBase { $filters = $this->getGroupedExposedFilters(); $view = $this->getBasicPageView(); - // Filter: Name, Operator: longerthan, Value: 4 + // Filter: Name, Operator: longerthan, Value: 4. $filters['name']['group_info']['default_group'] = 5; $view->setDisplay('page_1'); $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); @@ -882,7 +882,7 @@ class FilterStringTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the filtering + // Change the filtering. $view->displayHandlers->get('default')->overrideOption('filters', [ 'description' => [ 'id' => 'description', diff --git a/core/modules/views/tests/src/Kernel/Handler/SortDateTest.php b/core/modules/views/tests/src/Kernel/Handler/SortDateTest.php index a1d72775a75..e6f65f2bc5f 100644 --- a/core/modules/views/tests/src/Kernel/Handler/SortDateTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/SortDateTest.php @@ -201,7 +201,7 @@ class SortDateTest extends ViewsKernelTestBase { ], ]); - // Change the ordering + // Change the ordering. $view->displayHandlers->get('default')->overrideOption('sorts', [ 'created' => [ 'id' => 'created', diff --git a/core/modules/views/tests/src/Kernel/Handler/SortTest.php b/core/modules/views/tests/src/Kernel/Handler/SortTest.php index a27e46ebac5..444ed8c7ce5 100644 --- a/core/modules/views/tests/src/Kernel/Handler/SortTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/SortTest.php @@ -28,7 +28,7 @@ class SortTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the ordering + // Change the ordering. $view->displayHandlers->get('default')->overrideOption('sorts', [ 'age' => [ 'order' => 'ASC', @@ -52,7 +52,7 @@ class SortTest extends ViewsKernelTestBase { $view->destroy(); $view->setDisplay(); - // Reverse the ordering + // Reverse the ordering. $view->displayHandlers->get('default')->overrideOption('sorts', [ 'age' => [ 'order' => 'DESC', @@ -81,7 +81,7 @@ class SortTest extends ViewsKernelTestBase { $view = Views::getView('test_view'); $view->setDisplay(); - // Change the ordering + // Change the ordering. $view->displayHandlers->get('default')->overrideOption('sorts', [ 'name' => [ 'order' => 'ASC', @@ -105,7 +105,7 @@ class SortTest extends ViewsKernelTestBase { $view->destroy(); $view->setDisplay(); - // Reverse the ordering + // Reverse the ordering. $view->displayHandlers->get('default')->overrideOption('sorts', [ 'name' => [ 'order' => 'DESC', diff --git a/core/modules/views/tests/src/Kernel/Plugin/SqlQueryTest.php b/core/modules/views/tests/src/Kernel/Plugin/SqlQueryTest.php index 271447fc8b1..3b36afd04ff 100644 --- a/core/modules/views/tests/src/Kernel/Plugin/SqlQueryTest.php +++ b/core/modules/views/tests/src/Kernel/Plugin/SqlQueryTest.php @@ -109,7 +109,7 @@ class SqlQueryTest extends ViewsKernelTestBase { $this->assertSame('default', $view->getQuery()->getConnection()->getKey()); $this->assertSame('default', $view->getQuery()->getConnection()->getTarget()); - // Test the database connection with the option 'replica' set to TRUE; + // Test the database connection with the option 'replica' set to TRUE. $view->getQuery()->options['replica'] = TRUE; $this->assertSame('default', $view->getQuery()->getConnection()->getKey()); $this->assertSame('replica', $view->getQuery()->getConnection()->getTarget()); diff --git a/core/modules/views/tests/src/Kernel/ViewStorageTest.php b/core/modules/views/tests/src/Kernel/ViewStorageTest.php index 4f7eed048a8..0ea14bf8032 100644 --- a/core/modules/views/tests/src/Kernel/ViewStorageTest.php +++ b/core/modules/views/tests/src/Kernel/ViewStorageTest.php @@ -71,7 +71,7 @@ class ViewStorageTest extends ViewsKernelTestBase { $this->createTests(); $this->displayTests(); - // Helper method tests + // Helper method tests. $this->displayMethodTests(); } diff --git a/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php b/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php index 79a3ff64888..ae182e2189a 100644 --- a/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php @@ -181,7 +181,7 @@ class FieldPluginBaseTest extends UnitTestCase { ->willReturnCallback( // Pretend to do a render. function (&$elements, $is_root_call = FALSE) { - // Mock the ability to theme links + // Mock the ability to theme links. $link = $this->linkGenerator->generate($elements['#title'], $elements['#url']); if (isset($elements['#prefix'])) { $link = $elements['#prefix'] . $link; @@ -367,7 +367,7 @@ class FieldPluginBaseTest extends UnitTestCase { // entity_type flag. $entity_type_id = 'node'; $data[] = ['test-path', ['entity_type' => $entity_type_id], '<a href="/test-path">value</a>']; - // Prefix + // Prefix. $data[] = ['test-path', ['prefix' => 'test_prefix'], 'test_prefix<a href="/test-path">value</a>']; // suffix. $data[] = ['test-path', ['suffix' => 'test_suffix'], '<a href="/test-path">value</a>test_suffix']; diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php index 6579f93199b..9d2f23cf4c9 100644 --- a/core/modules/views/views.api.php +++ b/core/modules/views/views.api.php @@ -13,6 +13,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Language\LanguageInterface; use Drupal\views\Plugin\views\cache\CachePluginBase; use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\Plugin\views\query\QueryPluginBase; use Drupal\views\ViewExecutable; /** diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index 48623fc593e..2dcc15e73e1 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -59,7 +59,7 @@ function views_removed_post_updates(): array { */ function views_post_update_views_data_argument_plugin_id(?array &$sandbox = NULL): void { /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */ - $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $view_config_updater = \Drupal::service(ViewsConfigUpdater::class); $view_config_updater->setDeprecationsEnabled(FALSE); \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool { return $view_config_updater->needsEntityArgumentUpdate($view); @@ -71,7 +71,7 @@ function views_post_update_views_data_argument_plugin_id(?array &$sandbox = NULL */ function views_post_update_update_remember_role_empty(?array &$sandbox = NULL): void { /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */ - $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $view_config_updater = \Drupal::service(ViewsConfigUpdater::class); $view_config_updater->setDeprecationsEnabled(FALSE); \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool { return $view_config_updater->needsRememberRolesUpdate($view); @@ -83,7 +83,7 @@ function views_post_update_update_remember_role_empty(?array &$sandbox = NULL): */ function views_post_update_table_css_class(?array &$sandbox = NULL): void { /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */ - $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $view_config_updater = \Drupal::service(ViewsConfigUpdater::class); $view_config_updater->setDeprecationsEnabled(FALSE); \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool { return $view_config_updater->needsTableCssClassUpdate($view); diff --git a/core/modules/views/views.services.yml b/core/modules/views/views.services.yml index 2a3aa1a16fa..19c5caf92b7 100644 --- a/core/modules/views/views.services.yml +++ b/core/modules/views/views.services.yml @@ -107,3 +107,5 @@ services: tags: - { name: backend_overridable } Drupal\views\Plugin\views\query\CastSqlInterface: '@views.cast_sql' + Drupal\views\ViewsConfigUpdater: + autowire: true diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc index 04c5de5a535..e6e85343e9c 100644 --- a/core/modules/views/views.theme.inc +++ b/core/modules/views/views.theme.inc @@ -159,7 +159,7 @@ function template_preprocess_views_view_fields(&$variables): void { $object->wrapper_attributes = new Attribute($attributes); } - // Set up field label + // Set up field label. $object->label = $view->field[$id]->label(); // Set up field label wrapper and its attributes. @@ -300,7 +300,7 @@ function template_preprocess_views_view_summary(&$variables): void { $url->setRouteParameters($parameters); } catch (Exception) { - // If the given route doesn't exist, default to <front> + // If the given route doesn't exist, default to "<front>". $url = Url::fromRoute('<front>'); } } @@ -387,7 +387,7 @@ function template_preprocess_views_view_summary_unformatted(&$variables): void { $url->setRouteParameters($parameters); } catch (Exception) { - // If the given route doesn't exist, default to <front> + // If the given route doesn't exist, default to <front>. $url = Url::fromRoute('<front>'); } } diff --git a/core/modules/views_ui/src/Form/Ajax/AddHandler.php b/core/modules/views_ui/src/Form/Ajax/AddHandler.php index 1934593779e..171a87ac7cb 100644 --- a/core/modules/views_ui/src/Form/Ajax/AddHandler.php +++ b/core/modules/views_ui/src/Form/Ajax/AddHandler.php @@ -166,7 +166,7 @@ class AddHandler extends ViewsFormBase { '#markup' => '<div class="js-form-item form-item">' . $this->t('There are no @types available to add.', ['@types' => $ltitle]) . '</div>', ]; } - // Add a div to show the selected items + // Add a div to show the selected items. $form['selected'] = [ '#type' => 'item', '#markup' => '<span class="views-ui-view-title">' . $this->t('Selected:') . '</span><div class="views-selected-options"></div>', diff --git a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php index e0ac4d9e92b..94de2c7a8eb 100644 --- a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php +++ b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php @@ -244,7 +244,7 @@ class ConfigHandler extends ViewsFormBase { // extra stuff on the form is not sent through. $handler->unpackOptions($handler->options, $options, NULL, FALSE); - // Store the item back on the view + // Store the item back on the view. $executable->setHandler($display_id, $type, $id, $handler->options); // Ensure any temporary options are removed. @@ -252,7 +252,7 @@ class ConfigHandler extends ViewsFormBase { unset($view->temporary_options[$type][$id]); } - // Write to cache + // Write to cache. $view->cacheSet(); } @@ -264,7 +264,7 @@ class ConfigHandler extends ViewsFormBase { $display_id = $form_state->get('display_id'); $type = $form_state->get('type'); $id = $form_state->get('id'); - // Store the item back on the view + // Store the item back on the view. [$was_defaulted, $is_defaulted] = $view->getOverrideValues($form, $form_state); $executable = $view->getExecutable(); // If the display selection was changed toggle the override value. @@ -274,7 +274,7 @@ class ConfigHandler extends ViewsFormBase { } $executable->removeHandler($display_id, $type, $id); - // Write to cache + // Write to cache. $view->cacheSet(); } diff --git a/core/modules/views_ui/src/Form/Ajax/ConfigHandlerExtra.php b/core/modules/views_ui/src/Form/Ajax/ConfigHandlerExtra.php index 8e711233899..a6f1bcdc236 100644 --- a/core/modules/views_ui/src/Form/Ajax/ConfigHandlerExtra.php +++ b/core/modules/views_ui/src/Form/Ajax/ConfigHandlerExtra.php @@ -112,10 +112,10 @@ class ConfigHandlerExtra extends ViewsFormBase { $item[$key] = $value; } - // Store the item back on the view + // Store the item back on the view. $view->getExecutable()->setHandler($form_state->get('display_id'), $form_state->get('type'), $form_state->get('id'), $item); - // Write to cache + // Write to cache. $view->cacheSet(); } diff --git a/core/modules/views_ui/src/Form/Ajax/ConfigHandlerGroup.php b/core/modules/views_ui/src/Form/Ajax/ConfigHandlerGroup.php index b208139f5ee..5e8a8851908 100644 --- a/core/modules/views_ui/src/Form/Ajax/ConfigHandlerGroup.php +++ b/core/modules/views_ui/src/Form/Ajax/ConfigHandlerGroup.php @@ -105,10 +105,10 @@ class ConfigHandlerGroup extends ViewsFormBase { $handler->submitGroupByForm($form, $form_state); - // Store the item back on the view + // Store the item back on the view. $executable->setHandler($form_state->get('display_id'), $form_state->get('type'), $form_state->get('id'), $item); - // Write to cache + // Write to cache. $view->cacheSet(); } diff --git a/core/modules/views_ui/src/Form/Ajax/Rearrange.php b/core/modules/views_ui/src/Form/Ajax/Rearrange.php index 52be994b276..57911220fae 100644 --- a/core/modules/views_ui/src/Form/Ajax/Rearrange.php +++ b/core/modules/views_ui/src/Form/Ajax/Rearrange.php @@ -72,7 +72,7 @@ class Rearrange extends ViewsFormBase { $count = 0; - // Get relationship labels + // Get relationship labels. $relationships = []; foreach ($display->getHandlers('relationship') as $id => $handler) { $relationships[$id] = $handler->adminLabel(); @@ -161,7 +161,7 @@ class Rearrange extends ViewsFormBase { $old_fields = $display->getOption($types[$type]['plural']); $new_fields = $order = []; - // Make an array with the weights + // Make an array with the weights. foreach ($form_state->getValue('fields') as $field => $info) { // Add each value that is a field with a weight to our list, but only if // it has had its 'removed' checkbox checked. @@ -170,7 +170,7 @@ class Rearrange extends ViewsFormBase { } } - // Sort the array + // Sort the array. asort($order); // Create a new list of fields in the new order. @@ -179,7 +179,7 @@ class Rearrange extends ViewsFormBase { } $display->setOption($types[$type]['plural'], $new_fields); - // Store in cache + // Store in cache. $view->cacheSet(); } diff --git a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php index b0a3ad6b942..9bb23d40831 100644 --- a/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php +++ b/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php @@ -69,7 +69,7 @@ class RearrangeFilter extends ViewsFormBase { } $count = 0; - // Get relationship labels + // Get relationship labels. $relationships = []; foreach ($display->getHandlers('relationship') as $id => $handler) { $relationships[$id] = $handler->adminLabel(); @@ -242,7 +242,7 @@ class RearrangeFilter extends ViewsFormBase { // Whatever button was clicked, re-calculate field information. $new_fields = $order = []; - // Make an array with the weights + // Make an array with the weights. foreach ($form_state->getValue('filters') as $field => $info) { // Add each value that is a field with a weight to our list, but only if // it has had its 'removed' checkbox checked. @@ -258,7 +258,7 @@ class RearrangeFilter extends ViewsFormBase { } } - // Sort the array + // Sort the array. asort($order); // Create a new list of fields in the new order. @@ -271,7 +271,7 @@ class RearrangeFilter extends ViewsFormBase { $triggering_element = $form_state->getTriggeringElement(); if (!empty($triggering_element['#group'])) { if ($triggering_element['#group'] == 'add') { - // Add a new group + // Add a new group. $groups['groups'][] = 'AND'; } else { diff --git a/core/modules/views_ui/src/Hook/ViewsUiHooks.php b/core/modules/views_ui/src/Hook/ViewsUiHooks.php index 2d076aca439..45d5bafa096 100644 --- a/core/modules/views_ui/src/Hook/ViewsUiHooks.php +++ b/core/modules/views_ui/src/Hook/ViewsUiHooks.php @@ -75,7 +75,7 @@ class ViewsUiHooks { #[Hook('theme')] public function theme() : array { return [ - // Edit a view + // Edit a view. 'views_ui_display_tab_setting' => [ 'variables' => [ 'description' => '', @@ -127,7 +127,7 @@ class ViewsUiHooks { 'render element' => 'form', 'file' => 'views_ui.theme.inc', ], - // On behalf of a plugin + // On behalf of a plugin. 'views_ui_style_plugin_table' => [ 'render element' => 'form', 'file' => 'views_ui.theme.inc', diff --git a/core/modules/views_ui/src/Hook/ViewsUiThemeHooks.php b/core/modules/views_ui/src/Hook/ViewsUiThemeHooks.php new file mode 100644 index 00000000000..8284cbf1afa --- /dev/null +++ b/core/modules/views_ui/src/Hook/ViewsUiThemeHooks.php @@ -0,0 +1,70 @@ +<?php + +namespace Drupal\views_ui\Hook; + +use Drupal\Component\Utility\Xss; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for views_ui. + */ +class ViewsUiThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for views templates. + */ + #[Hook('preprocess_views_view')] + public function preprocessViewsView(&$variables): void { + $view = $variables['view']; + // Render title for the admin preview. + if (!empty($view->live_preview)) { + $variables['title'] = [ + '#markup' => $view->getTitle(), + '#allowed_tags' => Xss::getHtmlTagList(), + ]; + } + if (!empty($view->live_preview) && \Drupal::moduleHandler()->moduleExists('contextual')) { + $view->setShowAdminLinks(FALSE); + foreach ([ + 'title', + 'header', + 'exposed', + 'rows', + 'pager', + 'more', + 'footer', + 'empty', + 'attachment_after', + 'attachment_before', + ] as $section) { + if (!empty($variables[$section])) { + $variables[$section] = [ + '#theme' => 'views_ui_view_preview_section', + '#view' => $view, + '#section' => $section, + '#content' => $variables[$section], + '#theme_wrappers' => [ + 'views_ui_container', + ], + '#attributes' => [ + 'class' => [ + 'contextual-region', + ], + ], + ]; + } + } + } + } + + /** + * Implements hook_theme_suggestions_HOOK(). + */ + #[Hook('theme_suggestions_views_ui_view_preview_section')] + public function themeSuggestionsViewsUiViewPreviewSection(array $variables): array { + return [ + 'views_ui_view_preview_section__' . $variables['section'], + ]; + } + +} diff --git a/core/modules/views_ui/src/ViewEditForm.php b/core/modules/views_ui/src/ViewEditForm.php index e112480de1d..2c57b1eedd3 100644 --- a/core/modules/views_ui/src/ViewEditForm.php +++ b/core/modules/views_ui/src/ViewEditForm.php @@ -210,7 +210,7 @@ class ViewEditForm extends ViewFormBase { } } - // Add the edit display content + // Add the edit display content. $tab_content = $this->getDisplayTab($view); $tab_content['#theme_wrappers'] = ['container']; $tab_content['#attributes'] = ['class' => ['views-display-tab']]; @@ -647,13 +647,13 @@ class ViewEditForm extends ViewFormBase { */ public function submitDisplayUndoDelete($form, FormStateInterface $form_state) { $view = $this->entity; - // Create the new display + // Create the new display. $id = $form_state->get('display_id'); $displays = $view->get('display'); $displays[$id]['deleted'] = FALSE; $view->set('display', $displays); - // Store in cache + // Store in cache. $view->cacheSet(); // Redirect to the top-level edit page. @@ -669,10 +669,10 @@ class ViewEditForm extends ViewFormBase { public function submitDisplayEnable($form, FormStateInterface $form_state) { $view = $this->entity; $id = $form_state->get('display_id'); - // setOption doesn't work because this would might affect upper displays + // setOption doesn't work because this would might affect upper displays. $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', TRUE); - // Store in cache + // Store in cache. $view->cacheSet(); // Redirect to the top-level edit page. @@ -690,7 +690,7 @@ class ViewEditForm extends ViewFormBase { $id = $form_state->get('display_id'); $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', FALSE); - // Store in cache + // Store in cache. $view->cacheSet(); // Redirect to the top-level edit page. @@ -753,7 +753,7 @@ class ViewEditForm extends ViewFormBase { $element['#attributes']['class'] = ['views-display-top', 'clearfix']; $element['#attributes']['id'] = ['views-display-top']; - // Extra actions for the display + // Extra actions for the display. $element['extra_actions'] = [ '#type' => 'dropbutton', '#attributes' => [ @@ -1084,7 +1084,7 @@ class ViewEditForm extends ViewFormBase { ]; } - // Render the array of links + // Render the array of links. $build['#actions'] = [ '#type' => 'dropbutton', '#links' => $actions, diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index c8606eb098f..2db8de84b04 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -477,7 +477,7 @@ class ViewUI implements ViewEntityInterface { } $id = $this->getExecutable()->addHandler($display_id, $type, $table, $field); - // Check to see if we have group by settings + // Check to see if we have group by settings. $key = $type; // Footer,header and empty text have a different internal handler // type(area). @@ -494,11 +494,11 @@ class ViewUI implements ViewEntityInterface { } // Check to see if this type has settings, if so add the settings form - // first + // first. if ($handler && $handler->hasExtraOptions()) { $this->addFormToStack('handler-extra', $display_id, $type, $id); } - // Then add the form to the stack + // Then add the form to the stack. $this->addFormToStack('handler', $display_id, $type, $id); } } @@ -507,7 +507,7 @@ class ViewUI implements ViewEntityInterface { unset($this->form_cache); } - // Store in cache + // Store in cache. $this->cacheSet(); } diff --git a/core/modules/views_ui/tests/src/Functional/DisplayTest.php b/core/modules/views_ui/tests/src/Functional/DisplayTest.php index eebd1861d59..cf603f6c8df 100644 --- a/core/modules/views_ui/tests/src/Functional/DisplayTest.php +++ b/core/modules/views_ui/tests/src/Functional/DisplayTest.php @@ -161,7 +161,7 @@ class DisplayTest extends UITestBase { $path = 'admin/structure/views/view/test_display/edit/block_1'; $link_display_path = 'admin/structure/views/nojs/display/test_display/block_1/link_display'; - // Test the link text displays 'None' and not 'Block 1' + // Test the link text displays 'None' and not 'Block 1'. $this->drupalGet($path); $this->assertSession()->elementTextEquals('xpath', "//a[contains(@href, '{$link_display_path}')]", 'None'); @@ -187,7 +187,7 @@ class DisplayTest extends UITestBase { $this->assertSession()->linkExists('Custom URL', 0, 'The link option has custom URL as summary.'); - // Test the default link_url value for new display + // Test the default link_url value for new display. $this->submitForm([], 'Add Block'); $this->assertSession()->addressEquals('admin/structure/views/view/test_display/edit/block_2'); $this->clickLink('Custom URL'); diff --git a/core/modules/views_ui/tests/src/Functional/HandlerTest.php b/core/modules/views_ui/tests/src/Functional/HandlerTest.php index 93d3a3006d4..4d24e830979 100644 --- a/core/modules/views_ui/tests/src/Functional/HandlerTest.php +++ b/core/modules/views_ui/tests/src/Functional/HandlerTest.php @@ -146,7 +146,7 @@ class HandlerTest extends UITestBase { $display = $view->getDisplay('default'); $this->assertTrue(isset($display['display_options'][$type_info['plural']][$id]), 'Ensure the field was added to the view itself.'); - // Remove the item and check that it's removed + // Remove the item and check that it's removed. $this->drupalGet($edit_handler_url); $this->submitForm([], 'Remove'); $this->assertSession()->linkByHrefNotExists($edit_handler_url, 0, 'The handler edit link does not appears in the UI after removing.'); diff --git a/core/modules/views_ui/tests/src/Functional/ViewEditTest.php b/core/modules/views_ui/tests/src/Functional/ViewEditTest.php index dbe0c0c3e3f..9e16c59ecbb 100644 --- a/core/modules/views_ui/tests/src/Functional/ViewEditTest.php +++ b/core/modules/views_ui/tests/src/Functional/ViewEditTest.php @@ -85,7 +85,7 @@ class ViewEditTest extends UITestBase { $machine_name_edit_url = 'admin/structure/views/nojs/display/test_view/test_1/display_id'; $error_text = 'Display machine name must contain only lowercase letters, numbers, or underscores.'; - // Test that potential invalid display ID requests are detected + // Test that potential invalid display ID requests are detected. $this->drupalGet('admin/structure/views/ajax/handler/test_view/fake_display_name/filter/title'); $arguments = [ '@display_id' => 'fake_display_name', diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/AdminAjaxTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/AdminAjaxTest.php index 7b6f8495264..de762b018b1 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/AdminAjaxTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/AdminAjaxTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\views_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the admin UI AJAX interactions. - * - * @group views_ui */ +#[Group('views_ui')] class AdminAjaxTest extends WebDriverTestBase { /** diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php index 03ad843b1ad..24e12e2c460 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php @@ -7,17 +7,16 @@ namespace Drupal\Tests\views_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\locale\SourceString; +use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\views\Entity\View; use Drupal\views\Tests\ViewTestData; -use Drupal\Tests\node\Traits\NodeCreationTrait; +use PHPUnit\Framework\Attributes\Group; // cSpell:ignore Blokk hozzáadása - /** * Tests the display UI. - * - * @group views_ui */ +#[Group('views_ui')] class DisplayTest extends WebDriverTestBase { use NodeCreationTrait; diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/FieldDialogsTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/FieldDialogsTest.php index 7864b7286f7..6e5e13712e0 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/FieldDialogsTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/FieldDialogsTest.php @@ -6,12 +6,12 @@ namespace Drupal\Tests\views_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the fields dialogs. - * - * @group views_ui */ +#[Group('views_ui')] class FieldDialogsTest extends WebDriverTestBase { /** diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php index f2580e7457e..a336d3cf780 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterCriteriaTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\views_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the View UI filter criteria group dialog. - * - * @group views_ui */ +#[Group('views_ui')] class FilterCriteriaTest extends WebDriverTestBase { /** diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterEntityReferenceTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterEntityReferenceTest.php index b6021c6580e..9458b59985a 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterEntityReferenceTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterEntityReferenceTest.php @@ -7,13 +7,14 @@ namespace Drupal\Tests\views_ui\FunctionalJavascript; use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\views_ui\Traits\FilterEntityReferenceTrait; +use PHPUnit\Framework\Attributes\Group; /** * Tests views creation wizard. * - * @group views_ui * @see \Drupal\views\Plugin\views\filter\EntityReference */ +#[Group('views_ui')] class FilterEntityReferenceTest extends WebDriverTestBase { use FilterEntityReferenceTrait; @@ -91,7 +92,7 @@ class FilterEntityReferenceTest extends WebDriverTestBase { ->isVisible()); $this->assertTrue($page->findField('options[widget]')->isVisible()); - // Ensure that disabled form elements from selection handler do not show up + // Ensure that disabled form elements from selection handler do not show up. // @see \Drupal\views\Plugin\views\filter\EntityReference method // buildExtraOptionsForm. $this->assertFalse($page->hasField('options[reference_default:node][target_bundles_update]')); diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterOptionsTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterOptionsTest.php index 3a5dc110040..fbfd5141ffe 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterOptionsTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterOptionsTest.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Drupal\Tests\views_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript filtering of options in add handler form. - * - * @group views_ui */ +#[Group('views_ui')] class FilterOptionsTest extends WebDriverTestBase { /** diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/PreviewTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/PreviewTest.php index c1b53b9765e..d8ab9632e17 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/PreviewTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/PreviewTest.php @@ -8,12 +8,12 @@ use Behat\Mink\Element\NodeElement; use Drupal\Core\Database\Database; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\views\Tests\ViewTestData; +use PHPUnit\Framework\Attributes\Group; /** * Tests the UI preview functionality. - * - * @group views_ui */ +#[Group('views_ui')] class PreviewTest extends WebDriverTestBase { /** diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsListingTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsListingTest.php index b6f442e6265..d0d6d4725e8 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsListingTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsListingTest.php @@ -5,13 +5,14 @@ declare(strict_types=1); namespace Drupal\Tests\views_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests the JavaScript filtering on the Views listing page. * * @see core/modules/views_ui/js/views_ui.listing.js - * @group views_ui */ +#[Group('views_ui')] class ViewsListingTest extends WebDriverTestBase { /** diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsWizardTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsWizardTest.php index b1026d78584..5526fb7bff3 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsWizardTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/ViewsWizardTest.php @@ -5,13 +5,14 @@ declare(strict_types=1); namespace Drupal\Tests\views_ui\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use PHPUnit\Framework\Attributes\Group; /** * Tests views creation wizard. * * @see core/modules/views_ui/js/views-admin.js - * @group views_ui */ +#[Group('views_ui')] class ViewsWizardTest extends WebDriverTestBase { /** diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module index d0e00a1ca7b..ab752170442 100644 --- a/core/modules/views_ui/views_ui.module +++ b/core/modules/views_ui/views_ui.module @@ -4,49 +4,10 @@ * @file */ -use Drupal\Component\Utility\Xss; use Drupal\Core\Url; use Drupal\views\ViewExecutable; /** - * Implements hook_preprocess_HOOK() for views templates. - */ -function views_ui_preprocess_views_view(&$variables): void { - $view = $variables['view']; - - // Render title for the admin preview. - if (!empty($view->live_preview)) { - $variables['title'] = [ - '#markup' => $view->getTitle(), - '#allowed_tags' => Xss::getHtmlTagList(), - ]; - } - - if (!empty($view->live_preview) && \Drupal::moduleHandler()->moduleExists('contextual')) { - $view->setShowAdminLinks(FALSE); - foreach (['title', 'header', 'exposed', 'rows', 'pager', 'more', 'footer', 'empty', 'attachment_after', 'attachment_before'] as $section) { - if (!empty($variables[$section])) { - $variables[$section] = [ - '#theme' => 'views_ui_view_preview_section', - '#view' => $view, - '#section' => $section, - '#content' => $variables[$section], - '#theme_wrappers' => ['views_ui_container'], - '#attributes' => ['class' => ['contextual-region']], - ]; - } - } - } -} - -/** - * Implements hook_theme_suggestions_HOOK(). - */ -function views_ui_theme_suggestions_views_ui_view_preview_section(array $variables): array { - return ['views_ui_view_preview_section__' . $variables['section']]; -} - -/** * Returns contextual links for each handler of a certain section. * * @todo Bring in relationships. diff --git a/core/modules/workflows/tests/src/Unit/WorkflowTest.php b/core/modules/workflows/tests/src/Unit/WorkflowTest.php index a17b8f7bf0b..a2eb0bcbde9 100644 --- a/core/modules/workflows/tests/src/Unit/WorkflowTest.php +++ b/core/modules/workflows/tests/src/Unit/WorkflowTest.php @@ -166,7 +166,7 @@ class WorkflowTest extends UnitTestCase { ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft') ->addTransition('publish', 'Publish', ['draft'], 'published'); - // Ensure we're returning state objects and they are set up correctly + // Ensure we're returning state objects and they are set up correctly. $this->assertInstanceOf(State::class, $workflow->getTypePlugin()->getState('draft')); $this->assertEquals('archived', $workflow->getTypePlugin()->getState('archived')->id()); $this->assertEquals('Archived', $workflow->getTypePlugin()->getState('archived')->label()); @@ -447,7 +447,7 @@ class WorkflowTest extends UnitTestCase { ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft') ->addTransition('publish', 'Publish', ['draft'], 'published'); - // Ensure we're returning state objects and they are set up correctly + // Ensure we're returning state objects and they are set up correctly. $this->assertInstanceOf(Transition::class, $workflow->getTypePlugin()->getTransition('create_new_draft')); $this->assertEquals('publish', $workflow->getTypePlugin()->getTransition('publish')->id()); $this->assertEquals('Publish', $workflow->getTypePlugin()->getTransition('publish')->label()); diff --git a/core/modules/workspaces/src/Hook/FormOperations.php b/core/modules/workspaces/src/Hook/FormOperations.php index 61b775ea3cd..6f91618e71a 100644 --- a/core/modules/workspaces/src/Hook/FormOperations.php +++ b/core/modules/workspaces/src/Hook/FormOperations.php @@ -8,7 +8,10 @@ use Drupal\Core\Form\WorkspaceSafeFormInterface; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Render\Element; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\workspaces\Entity\Workspace; +use Drupal\workspaces\Negotiator\QueryParameterWorkspaceNegotiator; use Drupal\workspaces\WorkspaceManagerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * Defines a class for reacting to form operations. @@ -17,6 +20,8 @@ class FormOperations { public function __construct( protected WorkspaceManagerInterface $workspaceManager, + #[Autowire('@workspaces.negotiator.query_parameter')] + protected QueryParameterWorkspaceNegotiator $queryParameterNegotiator, ) {} /** @@ -24,8 +29,21 @@ class FormOperations { */ #[Hook('form_alter')] public function formAlter(array &$form, FormStateInterface $form_state, $form_id): void { + $active_workspace = $this->workspaceManager->getActiveWorkspace(); + + // Ensure that the form's initial workspace (if any) is used for the current + // request. + $form_workspace_id = $form_state->getUserInput()['active_workspace_id'] ?? NULL; + $form_workspace = $form_workspace_id + ? Workspace::load($form_workspace_id) + : NULL; + if ($form_workspace && (!$active_workspace || $active_workspace->id() != $form_workspace->id())) { + $this->workspaceManager->setActiveWorkspace($form_workspace, FALSE); + $active_workspace = $form_workspace; + } + // No alterations are needed if we're not in a workspace context. - if (!$this->workspaceManager->hasActiveWorkspace()) { + if (!$active_workspace) { return; } @@ -47,6 +65,17 @@ class FormOperations { ]; $this->addWorkspaceValidation($form); } + else { + // Persist the active workspace for the entire lifecycle of the form, + // including AJAX requests. + $form['active_workspace_id'] = [ + '#type' => 'hidden', + '#value' => $active_workspace->id(), + ]; + + $url_query_options = $this->queryParameterNegotiator->getQueryOptions($active_workspace->id()); + $this->setAjaxWorkspace($form, $url_query_options + ['persist' => FALSE]); + } } /** @@ -83,4 +112,29 @@ class FormOperations { } } + /** + * Ensures that the current workspace is persisted across AJAX interactions. + * + * @param array &$element + * An associative array containing the structure of the form. + * @param array $url_query_options + * An array of URL query options used by the query parameter workspace + * negotiator. + */ + protected function setAjaxWorkspace(array &$element, array $url_query_options): void { + // Recurse through all children if needed. + foreach (Element::children($element) as $key) { + if (isset($element[$key]) && $element[$key]) { + $this->setAjaxWorkspace($element[$key], $url_query_options); + } + } + + if (isset($element['#ajax']) && !isset($element['#ajax']['options']['query']['workspace'])) { + $element['#ajax']['options']['query'] = array_merge_recursive( + $url_query_options, + $element['#ajax']['options']['query'] ?? [], + ); + } + } + } diff --git a/core/modules/workspaces/src/Negotiator/QueryParameterWorkspaceNegotiator.php b/core/modules/workspaces/src/Negotiator/QueryParameterWorkspaceNegotiator.php index 1f0b688541f..04efac2c3b1 100644 --- a/core/modules/workspaces/src/Negotiator/QueryParameterWorkspaceNegotiator.php +++ b/core/modules/workspaces/src/Negotiator/QueryParameterWorkspaceNegotiator.php @@ -4,6 +4,7 @@ namespace Drupal\workspaces\Negotiator; use Drupal\Component\Utility\Crypt; use Drupal\Core\Site\Settings; +use Drupal\workspaces\WorkspaceInterface; use Symfony\Component\HttpFoundation\Request; /** @@ -12,6 +13,11 @@ use Symfony\Component\HttpFoundation\Request; class QueryParameterWorkspaceNegotiator extends SessionWorkspaceNegotiator { /** + * Whether the negotiated workspace should be persisted. + */ + protected bool $persist = TRUE; + + /** * {@inheritdoc} */ public function applies(Request $request) { @@ -24,6 +30,8 @@ class QueryParameterWorkspaceNegotiator extends SessionWorkspaceNegotiator { * {@inheritdoc} */ public function getActiveWorkspaceId(Request $request): ?string { + $this->persist = (bool) $request->query->get('persist', TRUE); + $workspace_id = (string) $request->query->get('workspace'); $token = (string) $request->query->get('token'); $is_valid_token = hash_equals($this->getQueryToken($workspace_id), $token); @@ -36,6 +44,40 @@ class QueryParameterWorkspaceNegotiator extends SessionWorkspaceNegotiator { } /** + * {@inheritdoc} + */ + public function setActiveWorkspace(WorkspaceInterface $workspace) { + if ($this->persist) { + parent::setActiveWorkspace($workspace); + } + } + + /** + * {@inheritdoc} + */ + public function unsetActiveWorkspace() { + if ($this->persist) { + parent::unsetActiveWorkspace(); + } + } + + /** + * Returns the query options used by this negotiator. + * + * @param string $workspace_id + * A workspace ID. + * + * @return array + * An array of query options that can be used for a \Drupal\Core\Url object. + */ + public function getQueryOptions(string $workspace_id): array { + return [ + 'workspace' => $workspace_id, + 'token' => $this->getQueryToken($workspace_id), + ]; + } + + /** * Calculates a token based on a workspace ID. * * @param string $workspace_id diff --git a/core/modules/workspaces/src/WorkspaceManager.php b/core/modules/workspaces/src/WorkspaceManager.php index 5bb2dc454c3..b0e9b8ac959 100644 --- a/core/modules/workspaces/src/WorkspaceManager.php +++ b/core/modules/workspaces/src/WorkspaceManager.php @@ -9,24 +9,53 @@ use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Site\Settings; use Drupal\Core\State\StateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\workspaces\Negotiator\WorkspaceNegotiatorInterface; use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; use Symfony\Component\HttpFoundation\RequestStack; /** * Provides the workspace manager. + * + * @property iterable $negotiators */ class WorkspaceManager implements WorkspaceManagerInterface { use StringTranslationTrait; /** - * The current active workspace or FALSE if there is no active workspace. + * The current active workspace. * - * @var \Drupal\workspaces\WorkspaceInterface|false + * The value is either a workspace object, FALSE if there is no active + * workspace, or NULL if the active workspace hasn't been determined yet. */ - protected $activeWorkspace; + protected WorkspaceInterface|false|null $activeWorkspace = NULL; - public function __construct(protected RequestStack $requestStack, protected EntityTypeManagerInterface $entityTypeManager, protected MemoryCacheInterface $entityMemoryCache, protected AccountProxyInterface $currentUser, protected StateInterface $state, protected LoggerInterface $logger, protected ClassResolverInterface $classResolver, protected WorkspaceAssociationInterface $workspaceAssociation, protected WorkspaceInformationInterface $workspaceInfo, protected array $negotiatorIds = []) { + /** + * An array of workspace negotiator services. + * + * @todo Remove in drupal:12.0.0. + */ + private array $collectedNegotiators = []; + + public function __construct( + protected RequestStack $requestStack, + protected EntityTypeManagerInterface $entityTypeManager, + protected MemoryCacheInterface $entityMemoryCache, + protected AccountProxyInterface $currentUser, + protected StateInterface $state, + #[Autowire(service: 'logger.channel.workspaces')] + protected LoggerInterface $logger, + #[AutowireIterator(tag: 'workspace_negotiator')] + protected $negotiators, + protected WorkspaceAssociationInterface $workspaceAssociation, + protected WorkspaceInformationInterface $workspaceInfo, + ) { + if ($negotiators instanceof ClassResolverInterface) { + @trigger_error('Passing the \'class_resolver\' service as the 7th argument to ' . __METHOD__ . ' is deprecated in drupal:11.3.0 and is unsupported in drupal:12.0.0. Use autowiring for the \'workspaces.manager\' service instead. See https://www.drupal.org/node/3532939', E_USER_DEPRECATED); + $this->negotiators = $this->collectedNegotiators; + } } /** @@ -43,10 +72,7 @@ class WorkspaceManager implements WorkspaceManagerInterface { if (!isset($this->activeWorkspace)) { $request = $this->requestStack->getCurrentRequest(); - foreach ($this->negotiatorIds as $negotiator_id) { - /** @var \Drupal\workspaces\Negotiator\WorkspaceIdNegotiatorInterface $negotiator */ - $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id); - + foreach ($this->negotiators as $negotiator) { if ($negotiator->applies($request)) { if ($workspace_id = $negotiator->getActiveWorkspaceId($request)) { /** @var \Drupal\workspaces\WorkspaceInterface $negotiated_workspace */ @@ -79,16 +105,19 @@ class WorkspaceManager implements WorkspaceManagerInterface { /** * {@inheritdoc} */ - public function setActiveWorkspace(WorkspaceInterface $workspace) { + public function setActiveWorkspace(WorkspaceInterface $workspace, /* bool $persist = TRUE */) { + $persist = func_num_args() < 2 || func_get_arg(1); + $this->doSwitchWorkspace($workspace); - // Set the workspace on the proper negotiator. - $request = $this->requestStack->getCurrentRequest(); - foreach ($this->negotiatorIds as $negotiator_id) { - $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id); - if ($negotiator->applies($request)) { - $negotiator->setActiveWorkspace($workspace); - break; + // Set the workspace on the first applicable negotiator. + if ($persist) { + $request = $this->requestStack->getCurrentRequest(); + foreach ($this->negotiators as $negotiator) { + if ($negotiator->applies($request)) { + $negotiator->setActiveWorkspace($workspace); + break; + } } } @@ -102,8 +131,7 @@ class WorkspaceManager implements WorkspaceManagerInterface { $this->doSwitchWorkspace(NULL); // Unset the active workspace on all negotiators. - foreach ($this->negotiatorIds as $negotiator_id) { - $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id); + foreach ($this->negotiators as $negotiator) { $negotiator->unsetActiveWorkspace(); } @@ -253,4 +281,18 @@ class WorkspaceManager implements WorkspaceManagerInterface { } } + /** + * Adds a workspace negotiator service. + * + * @param \Drupal\workspaces\Negotiator\WorkspaceNegotiatorInterface $negotiator + * The negotiator to be added. + * + * @todo Remove in drupal:12.0.0. + * + * @internal + */ + public function addNegotiator(WorkspaceNegotiatorInterface $negotiator): void { + $this->collectedNegotiators[] = $negotiator; + } + } diff --git a/core/modules/workspaces/src/WorkspaceManagerInterface.php b/core/modules/workspaces/src/WorkspaceManagerInterface.php index a61d29f50d5..8d8024c6620 100644 --- a/core/modules/workspaces/src/WorkspaceManagerInterface.php +++ b/core/modules/workspaces/src/WorkspaceManagerInterface.php @@ -24,20 +24,24 @@ interface WorkspaceManagerInterface { public function getActiveWorkspace(); /** - * Sets the active workspace via the workspace negotiators. + * Sets the active workspace. * * @param \Drupal\workspaces\WorkspaceInterface $workspace * The workspace to set as active. + * phpcs:ignore + * @param bool $persist + * (optional) Whether to persist this workspace in the first applicable + * negotiator. Defaults to TRUE. * * @return $this * * @throws \Drupal\workspaces\WorkspaceAccessException * Thrown when the current user doesn't have access to view the workspace. */ - public function setActiveWorkspace(WorkspaceInterface $workspace); + public function setActiveWorkspace(WorkspaceInterface $workspace, /* bool $persist = TRUE */); /** - * Unsets the active workspace via the workspace negotiators. + * Unsets the active workspace. * * @return $this */ diff --git a/core/modules/workspaces/tests/modules/workspaces_test/src/Form/ActiveWorkspaceTestForm.php b/core/modules/workspaces/tests/modules/workspaces_test/src/Form/ActiveWorkspaceTestForm.php new file mode 100644 index 00000000000..955cf472038 --- /dev/null +++ b/core/modules/workspaces/tests/modules/workspaces_test/src/Form/ActiveWorkspaceTestForm.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\workspaces_test\Form; + +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\WorkspaceSafeFormInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreInterface; +use Drupal\Core\Url; +use Drupal\workspaces\WorkspaceManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Form for testing the active workspace. + * + * @internal + */ +class ActiveWorkspaceTestForm extends FormBase implements WorkspaceSafeFormInterface { + + /** + * The workspace manager. + */ + protected WorkspaceManagerInterface $workspaceManager; + + /** + * The test key-value store. + */ + protected KeyValueStoreInterface $keyValue; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container): static { + $instance = parent::create($container); + $instance->workspaceManager = $container->get('workspaces.manager'); + $instance->keyValue = $container->get('keyvalue')->get('ws_test'); + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'active_workspace_test_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['test'] = [ + '#type' => 'textfield', + '#ajax' => [ + 'url' => Url::fromRoute('workspaces_test.get_form'), + 'callback' => function () { + $this->keyValue->set('ajax_test_active_workspace', $this->workspaceManager->getActiveWorkspace()->id()); + return new AjaxResponse(); + }, + ], + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->keyValue->set('form_test_active_workspace', $this->workspaceManager->getActiveWorkspace()->id()); + } + +} diff --git a/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.routing.yml b/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.routing.yml new file mode 100644 index 00000000000..bdf7648db9c --- /dev/null +++ b/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.routing.yml @@ -0,0 +1,7 @@ +workspaces_test.get_form: + path: '/active-workspace-test-form' + defaults: + _title: 'Active Workspace Test Form' + _form: '\Drupal\workspaces_test\Form\ActiveWorkspaceTestForm' + requirements: + _access: 'TRUE' diff --git a/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesLayoutBuilderIntegrationTest.php b/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesLayoutBuilderIntegrationTest.php index f59aba78bb5..632ec73f122 100644 --- a/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesLayoutBuilderIntegrationTest.php +++ b/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesLayoutBuilderIntegrationTest.php @@ -9,14 +9,14 @@ use Drupal\Tests\system\Traits\OffCanvasTestTrait; use Drupal\Tests\workspaces\Functional\WorkspaceTestUtilities; use Drupal\user\UserInterface; use Drupal\workspaces\Entity\Workspace; +use PHPUnit\Framework\Attributes\Group; /** * Tests for layout editing in workspaces. - * - * @group layout_builder - * @group workspaces - * @group #slow */ +#[Group('layout_builder')] +#[Group('workspaces')] +#[Group('#slow')] class WorkspacesLayoutBuilderIntegrationTest extends InlineBlockTestBase { use OffCanvasTestTrait; @@ -191,7 +191,7 @@ class WorkspacesLayoutBuilderIntegrationTest extends InlineBlockTestBase { /** * Tests workspace specific layout tempstore data. * - * @covers \Drupal\workspaces\WorkspacesLayoutTempstoreRepository::getKey + * @legacy-covers \Drupal\workspaces\WorkspacesLayoutTempstoreRepository::getKey */ public function testWorkspacesLayoutTempstore(): void { $assert_session = $this->assertSession(); diff --git a/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesMediaLibraryIntegrationTest.php b/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesMediaLibraryIntegrationTest.php index 49cac2860ae..673aed12a73 100644 --- a/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesMediaLibraryIntegrationTest.php +++ b/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesMediaLibraryIntegrationTest.php @@ -7,12 +7,12 @@ namespace Drupal\Tests\workspaces\FunctionalJavascript; use Drupal\Tests\media_library\FunctionalJavascript\EntityReferenceWidgetTest; use Drupal\user\UserInterface; use Drupal\workspaces\Entity\Workspace; +use PHPUnit\Framework\Attributes\Group; /** * Tests the Media library entity reference widget in a workspace. - * - * @group workspaces */ +#[Group('workspaces')] class WorkspacesMediaLibraryIntegrationTest extends EntityReferenceWidgetTest { /** diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php index 6c4ac5ef105..9703526116d 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php @@ -231,7 +231,7 @@ class WorkspaceCRUDTest extends KernelTestBase { $workspace->save(); $this->workspaceManager->setActiveWorkspace($workspace); - // Create a new node in the 'stage' workspace + // Create a new node in the 'stage' workspace. $node = $this->createNode(['status' => TRUE]); // Create an additional workspace-specific revision for the node. diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php new file mode 100644 index 00000000000..ac3e7b3a51c --- /dev/null +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php @@ -0,0 +1,120 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\workspaces\Kernel; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; +use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Form\FormState; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\user\Traits\UserCreationTrait; +use Drupal\workspaces\Entity\Workspace; +use Drupal\workspaces_test\Form\ActiveWorkspaceTestForm; +use Symfony\Component\HttpFoundation\Request; + +/** + * Tests form persistence for the active workspace. + * + * @group workspaces + */ +class WorkspaceFormPersistenceTest extends KernelTestBase { + + use UserCreationTrait; + use WorkspaceTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'system', + 'user', + 'workspaces', + 'workspaces_test', + ]; + + /** + * The entity type manager. + */ + protected EntityTypeManagerInterface $entityTypeManager; + + /** + * The form builder. + */ + protected FormBuilderInterface $formBuilder; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->entityTypeManager = \Drupal::entityTypeManager(); + $this->formBuilder = \Drupal::formBuilder(); + + $this->installEntitySchema('user'); + $this->installEntitySchema('workspace'); + + Workspace::create(['id' => 'ham', 'label' => 'Ham'])->save(); + Workspace::create(['id' => 'cheese', 'label' => 'Cheese'])->save(); + + $this->setCurrentUser($this->createUser([ + 'view any workspace', + ])); + } + + /** + * Tests that the active workspace is persisted throughout a form's lifecycle. + */ + public function testFormPersistence(): void { + $form_arg = ActiveWorkspaceTestForm::class; + + $this->switchToWorkspace('ham'); + $form_state_1 = new FormState(); + $form_1 = $this->formBuilder->buildForm($form_arg, $form_state_1); + + $this->switchToWorkspace('cheese'); + $form_state_2 = new FormState(); + $this->formBuilder->buildForm($form_arg, $form_state_2); + + // Submit the second form and check the workspace in which it was submitted. + $this->formBuilder->submitForm($form_arg, $form_state_2); + $this->assertSame('cheese', $this->keyValue->get('ws_test')->get('form_test_active_workspace')); + + // Submit the first form and check the workspace in which it was submitted. + $this->formBuilder->submitForm($form_arg, $form_state_1); + $this->assertSame('ham', $this->keyValue->get('ws_test')->get('form_test_active_workspace')); + + // Reset the workspace manager service to simulate a new request and check + // that the second workspace is still active. + \Drupal::getContainer()->set('workspaces.manager', NULL); + $this->assertSame('cheese', \Drupal::service('workspaces.manager')->getActiveWorkspace()->id()); + + // Reset the workspace manager service again to prepare for a new request. + \Drupal::getContainer()->set('workspaces.manager', NULL); + + $request = Request::create( + $form_1['test']['#ajax']['url']->toString(), + 'POST', + [ + MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax', + ] + $form_1['test']['#attached']['drupalSettings']['ajax'][$form_1['test']['#id']]['submit'], + ); + \Drupal::service('http_kernel')->handle($request); + + $form_state_1->setTriggeringElement($form_1['test']); + \Drupal::service('form_ajax_response_builder')->buildResponse($request, $form_1, $form_state_1, []); + + // Check that the AJAX callback is executed in the initial workspace of its + // parent form. + $this->assertSame('ham', $this->keyValue->get('ws_test')->get('ajax_test_active_workspace')); + + // Reset the workspace manager service again and check that the AJAX request + // didn't change the persisted workspace. + \Drupal::getContainer()->set('workspaces.manager', NULL); + \Drupal::requestStack()->pop(); + $this->assertSame('cheese', \Drupal::service('workspaces.manager')->getActiveWorkspace()->id()); + } + +} diff --git a/core/modules/workspaces/workspaces.services.yml b/core/modules/workspaces/workspaces.services.yml index 1fd07d36d77..4abb1d1cae4 100644 --- a/core/modules/workspaces/workspaces.services.yml +++ b/core/modules/workspaces/workspaces.services.yml @@ -3,9 +3,9 @@ services: autoconfigure: true workspaces.manager: class: Drupal\workspaces\WorkspaceManager - arguments: ['@request_stack', '@entity_type.manager', '@entity.memory_cache', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver', '@workspaces.association', '@workspaces.information'] + autowire: true tags: - - { name: service_id_collector, tag: workspace_negotiator } + - { name: service_collector, call: addNegotiator, tag: workspace_negotiator } Drupal\workspaces\WorkspaceManagerInterface: '@workspaces.manager' workspaces.information: class: Drupal\workspaces\WorkspaceInformation diff --git a/core/modules/workspaces_ui/tests/src/FunctionalJavascript/WorkspaceToolbarIntegrationTest.php b/core/modules/workspaces_ui/tests/src/FunctionalJavascript/WorkspaceToolbarIntegrationTest.php index b83953de37e..5d500fce8fa 100644 --- a/core/modules/workspaces_ui/tests/src/FunctionalJavascript/WorkspaceToolbarIntegrationTest.php +++ b/core/modules/workspaces_ui/tests/src/FunctionalJavascript/WorkspaceToolbarIntegrationTest.php @@ -6,13 +6,13 @@ namespace Drupal\Tests\workspaces_ui\FunctionalJavascript; use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase; use Drupal\workspaces\Entity\Workspace; +use PHPUnit\Framework\Attributes\Group; /** * Tests workspace settings stray integration. - * - * @group workspaces - * @group workspaces_ui */ +#[Group('workspaces')] +#[Group('workspaces_ui')] class WorkspaceToolbarIntegrationTest extends OffCanvasTestBase { /** |