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.
*/