diff --git a/core/modules/system/system.tokens.inc b/core/modules/system/system.tokens.inc index 388796ecca..89ee2e4cc1 100644 --- a/core/modules/system/system.tokens.inc +++ b/core/modules/system/system.tokens.inc @@ -19,6 +19,16 @@ function system_token_info() { 'name' => t("Site information"), 'description' => t("Tokens for site-wide settings and other global information."), ]; + $types['site-logo'] = [ + 'name' => t('Site logo'), + 'description' => t('Tokens related to the site logo.'), + 'needs-data' => 'site', + ]; + $types['site-logo-properties'] = [ + 'name' => t('Site logo properties'), + 'description' => t('Tokens for site logo properties.'), + 'needs-data' => 'site-logo', + ]; $types['date'] = [ 'name' => t("Dates"), 'description' => t("Tokens related to times and dates."), @@ -61,6 +71,45 @@ function system_token_info() { 'description' => t("The URL of the site's login page."), ]; + // The [site:logo] token renders the active theme logo. Chained tokens can be + // used for rendering specific theme logos or logo properties (URLs) instead. + $site['logo'] = [ + 'name' => t('Logo'), + 'description' => t('The logo of the active theme. Note that the theme may be different when this is used in the administrative interface.'), + 'type' => 'site-logo', + ]; + + // Tokens for a specific theme logo. + $site_logo['active-theme'] = [ + 'name' => t('Active theme'), + 'description' => t('The logo of the active theme. Note that the theme may be different when this is used in the administrative interface.'), + 'type' => 'site-logo-properties', + ]; + $site_logo['default-theme'] = [ + 'name' => t('Default theme'), + 'description' => t('The logo that is configured for the default theme.'), + 'type' => 'site-logo-properties', + ]; + // Obtain a list of installed themes and make tokens from them. + $themes = \Drupal::service('theme_handler')->listInfo(); + foreach ($themes as $theme => $info) { + if (empty($info->info['hidden'])) { + $site_logo['theme-' . $theme] = [ + 'name' => t('@theme', ['@theme' => $info->info['name']]), + 'description' => t('The logo that is configured for the %theme theme.', [ + '%theme' => $info->info['name'], + ]), + 'type' => 'site-logo-properties', + ]; + } + } + + // Tokens for individual properties for logos. + $site_logo_properties['url'] = [ + 'name' => t('URL'), + 'description' => t('The URL of the logo.'), + ]; + /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */ $date_formatter = \Drupal::service('date.formatter'); @@ -95,6 +144,8 @@ function system_token_info() { 'types' => $types, 'tokens' => [ 'site' => $site, + 'site-logo' => $site_logo, + 'site-logo-properties' => $site_logo_properties, 'date' => $date, ], ]; @@ -173,10 +224,65 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe $bubbleable_metadata->addCacheableDependency($result); $replacements[$original] = $result->getGeneratedUrl(); break; + + case 'logo': + $replacements[$original] = _system_tokens_get_logo_rendered('active-theme', $bubbleable_metadata); + break; } } + + // Detect chained tokens ([site:logo:?]). + if ($logo_tokens = $token_service->findWithPrefix($tokens, 'logo')) { + $replacements += $token_service->generate('site-logo', $logo_tokens, [], $options, $bubbleable_metadata); + } } + elseif ($type == 'site-logo') { + foreach ($tokens as $name => $original) { + switch ($name) { + case 'active-theme': + case 'default-theme': + $replacements[$original] = _system_tokens_get_logo_rendered($name, $bubbleable_metadata); + break; + } + } + + if ($logo_tokens = $token_service->findWithPrefix($tokens, 'active-theme')) { + $replacements += $token_service->generate('site-logo-properties', $logo_tokens, ['theme' => 'active-theme'], $options, $bubbleable_metadata); + } + if ($logo_tokens = $token_service->findWithPrefix($tokens, 'default-theme')) { + $replacements += $token_service->generate('site-logo-properties', $logo_tokens, ['theme' => 'default-theme'], $options, $bubbleable_metadata); + } + + // List installed themes. + $themes = \Drupal::service('theme_handler')->listInfo(); + $installed_themes = array_keys($themes); + // Detect direct tokens ([site:logo:theme-bartik]). + foreach ($tokens as $name => $original) { + // Strip 'theme-' to get the theme name, but do not strip 'active-theme'. + $name = str_starts_with($name, 'theme-') ? substr($name, 6) : $name; + if (in_array($name, $installed_themes)) { + $replacements[$original] = _system_tokens_get_logo_rendered($name, $bubbleable_metadata); + } + } + + // Detect chained tokens ([site:logo:theme-bartik:?]). + foreach ($installed_themes as $installed_theme) { + if ($created_tokens = $token_service->findWithPrefix($tokens, 'theme-' . $installed_theme)) { + $replacements += $token_service->generate('site-logo-properties', $created_tokens, ['theme' => $installed_theme], $options, $bubbleable_metadata); + } + } + } + elseif ($type == 'site-logo-properties' && !empty($data['theme'])) { + foreach ($tokens as $name => $original) { + switch ($name) { + case 'url': + $logo_path = _system_tokens_get_logo_path($data['theme'], $bubbleable_metadata); + $replacements[$original] = Url::fromUserInput($logo_path, ['absolute' => TRUE])->toString(); + break; + } + } + } elseif ($type == 'date') { if (empty($data['date'])) { $date = \Drupal::time()->getRequestTime(); @@ -218,3 +324,62 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe return $replacements; } + +/** + * Returns the path of the logo for the given theme. + * + * @see _system_tokens_get_logo_path() + * + * @param string $theme + * The theme to load the logo for. + * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata + * The bubbleable metadata to alter in order to cache the token. + * + * @return mixed + * The path to the logo, NULL if the setting does not exist. + */ +function _system_tokens_get_logo_path(string $theme, BubbleableMetadata &$bubbleable_metadata) { + if ($theme === 'default-theme') { + // Add the cache dependency. + $system_theme_config = \Drupal::config('system.theme'); + $theme = $system_theme_config->get('default'); + $bubbleable_metadata->addCacheableDependency($system_theme_config); + } + elseif ($theme === 'active-theme') { + $theme = \Drupal::service('theme.manager')->getActiveTheme()->getName(); + // Needs caching per theme, because it can change. + $bubbleable_metadata->addCacheContexts(['theme']); + } + + // Because theme_get_setting() can return either the global theme logo or a + // theme specific logo, this token depends on both configurations. + $global_theme_config = \Drupal::config('system.theme.global'); + $theme_config = \Drupal::config($theme . '.settings'); + $bubbleable_metadata->addCacheableDependency($global_theme_config); + $bubbleable_metadata->addCacheableDependency($theme_config); + + return theme_get_setting('logo.url', $theme); +} + +/** + * Returns an tag for the logo of the given theme. + * + * @see _system_tokens_get_logo_path() + * + * @param string $theme + * The theme to load the logo for. + * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata + * The bubbleable metadata to alter in order to cache the token. + * + * @return \Drupal\Component\Render\MarkupInterface + * An HTML tag for the logo. + */ +function _system_tokens_get_logo_rendered(string $theme, BubbleableMetadata &$bubbleable_metadata) { + $logo_path = _system_tokens_get_logo_path($theme, $bubbleable_metadata); + $build = [ + '#theme' => 'image', + '#uri' => $logo_path, + '#alt' => t('The site logo'), + ]; + return \Drupal::service('renderer')->renderPlain($build); +} diff --git a/core/modules/system/tests/src/Kernel/Token/TokenReplaceKernelTest.php b/core/modules/system/tests/src/Kernel/Token/TokenReplaceKernelTest.php index 2ecff8dcbe..30196a0c81 100644 --- a/core/modules/system/tests/src/Kernel/Token/TokenReplaceKernelTest.php +++ b/core/modules/system/tests/src/Kernel/Token/TokenReplaceKernelTest.php @@ -2,11 +2,11 @@ namespace Drupal\Tests\system\Kernel\Token; -use Drupal\Core\Url; use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Xss; use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Url; use Symfony\Component\HttpFoundation\Request; /** @@ -149,6 +149,148 @@ public function testSystemSiteTokenReplacement() { $this->assertEquals((new BubbleableMetadata())->addCacheContexts(['url.site']), $bubbleable_metadata); } + /** + * Tests the logo tokens for the active and default theme. + */ + public function testSystemSiteLogoTokenReplacement() { + // Install Olivero and Claro. + \Drupal::service('theme_installer')->install(['olivero', 'claro']); + // Prepare instances that we need only once. + $theme_initialization = \Drupal::service('theme.initialization'); + $theme_manager = \Drupal::service('theme.manager'); + $renderer = \Drupal::service('renderer'); + + // Prepare configurations. + $global_theme_config = $this->config('system.theme.global'); + $global_theme_config + ->set('logo.path', '/path/to/global_logo.svg') + ->set('logo.use_default', FALSE) + ->save(); + $olivero_config = $this->config('olivero.settings'); + $olivero_config + ->set('logo.path', '/path/to/olivero_logo.svg') + ->set('logo.use_default', FALSE) + ->save(); + $claro_config = $this->config('claro.settings'); + $claro_config + ->set('logo.path', '/path/to/claro_logo.svg') + ->set('logo.use_default', FALSE) + ->save(); + + // Set Olivero as the default theme. + $system_theme_config = $this->config('system.theme'); + $system_theme_config + ->set('default', 'olivero') + ->save(); + + // And set Claro as the active theme. + $active_theme = $theme_initialization->initTheme('claro'); + $theme_manager->setActiveTheme($active_theme); + + // Render both theme logos. + $build = [ + '#theme' => 'image', + '#uri' => theme_get_setting('logo.url', 'olivero'), + '#alt' => 'The site logo', + ]; + $olivero_logo = $renderer->renderPlain($build); + $build = [ + '#theme' => 'image', + '#uri' => theme_get_setting('logo.url', 'claro'), + '#alt' => 'The site logo', + ]; + $claro_logo = $renderer->renderPlain($build); + + $tests = []; + $tests['[site:logo]'] = $claro_logo; + $tests['[site:logo:active-theme]'] = $claro_logo; + $tests['[site:logo:active-theme:url]'] = Url::fromUserInput($claro_config->get('logo.path'), ['absolute' => TRUE])->toString(); + $tests['[site:logo:default-theme]'] = $olivero_logo; + $tests['[site:logo:default-theme:url]'] = Url::fromUserInput($olivero_config->get('logo.path'), ['absolute' => TRUE])->toString(); + + $bubbleable_metadata_claro = BubbleableMetadata::createFromObject($global_theme_config) + ->addCacheableDependency($claro_config) + ->addCacheContexts(['theme']); + $bubbleable_metadata_default = BubbleableMetadata::createFromObject($system_theme_config) + ->addCacheableDependency($global_theme_config) + ->addCacheableDependency($olivero_config); + + $metadata_tests = []; + $metadata_tests['[site:logo]'] = $bubbleable_metadata_claro; + $metadata_tests['[site:logo:active-theme]'] = $bubbleable_metadata_claro; + $metadata_tests['[site:logo:active-theme:url]'] = $bubbleable_metadata_claro; + $metadata_tests['[site:logo:default-theme]'] = $bubbleable_metadata_default; + $metadata_tests['[site:logo:default-theme:url]'] = $bubbleable_metadata_default; + + // Test to make sure that we generated something for each token. + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); + + foreach ($tests as $input => $expected) { + $bubbleable_metadata = new BubbleableMetadata(); + $output = $this->tokenService->replace($input, [], [], $bubbleable_metadata); + $this->assertEquals($expected, $output, new FormattableMarkup('System site information token %token replaced.', ['%token' => $input])); + $this->assertEquals($metadata_tests[$input], $bubbleable_metadata, new FormattableMarkup('Asserting metadata for token %token.', ['%token' => $input])); + } + } + + /** + * Tests the logo tokens for a specific theme. + */ + public function testSystemSiteLogoThemeTokenReplacement() { + // Install Olivero and Claro. + \Drupal::service('theme_installer')->install(['olivero', 'claro']); + // Prepare instances that we need only once. + $renderer = \Drupal::service('renderer'); + // Prepare configurations. + // Do not set a logo for Claro, it should fall back to the global logo. + $global_theme_config = $this->config('system.theme.global'); + $global_theme_config + ->set('logo.path', '/path/to/global_logo.svg') + ->set('logo.use_default', FALSE) + ->save(); + $olivero_config = $this->config('olivero.settings'); + $olivero_config + ->set('logo.path', '/path/to/olivero_logo.svg') + ->set('logo.use_default', FALSE) + ->save(); + // Render the Olivero logo. + $build = [ + '#theme' => 'image', + '#uri' => theme_get_setting('logo.url', 'olivero'), + '#alt' => 'The site logo', + ]; + $olivero_logo = $renderer->renderPlain($build); + // Generate and test tokens. + $tests = []; + $tests['[site:logo:theme-olivero]'] = $olivero_logo; + $tests['[site:logo:theme-olivero:url]'] = Url::fromUserInput($olivero_config->get('logo.path'), ['absolute' => TRUE])->toString(); + $tests['[site:logo:theme-olivero:foo]'] = '[site:logo:theme-olivero:foo]'; + $tests['[site:logo:theme-not-enabled-theme]'] = '[site:logo:theme-not-enabled-theme]'; + $tests['[site:logo:theme-not-enabled-theme:url]'] = '[site:logo:theme-not-enabled-theme:url]'; + $tests['[site:logo:theme-not-enabled-theme:foo]'] = '[site:logo:theme-not-enabled-theme:foo]'; + $bubbleable_metadata_olivero = BubbleableMetadata::createFromObject($global_theme_config) + ->addCacheableDependency($olivero_config); + $metadata_tests = []; + $metadata_tests['[site:logo:theme-olivero]'] = $bubbleable_metadata_olivero; + $metadata_tests['[site:logo:theme-olivero:url]'] = $bubbleable_metadata_olivero; + $metadata_tests['[site:logo:theme-olivero:foo]'] = new BubbleableMetadata(); + $metadata_tests['[site:logo:theme-not-enabled-theme]'] = new BubbleableMetadata(); + $metadata_tests['[site:logo:theme-not-enabled-theme:url]'] = new BubbleableMetadata(); + $metadata_tests['[site:logo:theme-not-enabled-theme:foo]'] = new BubbleableMetadata(); + // Test to make sure that we generated something for each token. + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); + // Test that the Claro logo has the path to the global logo. + $bubbleable_metadata = new BubbleableMetadata(); + $output = $this->tokenService->replace('[site:logo:theme-claro:url]', [], [], $bubbleable_metadata); + $this->assertStringContainsString($global_theme_config->get('logo.path'), $output, 'Token for undefined theme logo falls back to global logo.'); + foreach ($tests as $input => $expected) { + $bubbleable_metadata = new BubbleableMetadata(); + $output = $this->tokenService->replace($input, [], [], $bubbleable_metadata); + $this->assertEquals($expected, $output, new FormattableMarkup('System site information token %token replaced.', ['%token' => $input])); + $this->assertEquals($metadata_tests[$input], $bubbleable_metadata, new FormattableMarkup('Asserting metadata for token %token.', ['%token' => $input])); + } + } + /** * Tests the generation of all system date tokens. */