diff --git a/entity_mesh.install b/entity_mesh.install index de482d89ab375a528be5d8ef627e91b0a00cb66e..85482611dae3450807de56cf8728838f109bbd7e 100644 --- a/entity_mesh.install +++ b/entity_mesh.install @@ -224,3 +224,9 @@ function entity_mesh_update_10008() { \Drupal::service('config.installer')->installOptionalConfig($config_source); return (string) new TranslatableMarkup("Views data export installed and configured on Entity Mesh report."); } + +/** + * Empty update to clear caches for service Entity render definition. + */ +function entity_mesh_update_10009() { +} diff --git a/entity_mesh.services.yml b/entity_mesh.services.yml index d9142d0fa2ebc67ce7411d6b7960f561ea9ac8c0..88157c20e0a3e573acf2e6c7394e224b6a10d155 100644 --- a/entity_mesh.services.yml +++ b/entity_mesh.services.yml @@ -18,6 +18,12 @@ services: - '@entity_type.manager' - '@entity_field.manager' - '@config.factory' + entity_mesh.theme_switcher: + class: Drupal\entity_mesh\ThemeSwitcher + arguments: + - '@theme.manager' + - '@theme.initialization' + - '@config.factory' entity_mesh.entity_render: class: Drupal\entity_mesh\EntityRender arguments: @@ -30,6 +36,7 @@ services: - '@entity_mesh.language_negotiator_switcher' - '@module_handler' - '@access_manager' + - '@entity_mesh.theme_switcher' entity_mesh.menu: class: Drupal\entity_mesh\Menu arguments: [ '@entity_mesh.repository', '@entity_type.manager', '@language_manager', '@config.factory' ] diff --git a/src/EntityRender.php b/src/EntityRender.php index f5fb06bf367bcbe787d8efd04c045958555b69dc..e85a2f950c29ea2a00ffb58bdd17b958a9070107 100644 --- a/src/EntityRender.php +++ b/src/EntityRender.php @@ -44,7 +44,6 @@ class EntityRender extends Entity { */ protected LanguageNegotiatorSwitcher $languageNegotiatorSwitcher; - /** * Module handler. * @@ -59,6 +58,13 @@ class EntityRender extends Entity { */ protected AccessManager $accessManager; + /** + * Theme switcher. + * + * @var \Drupal\entity_mesh\ThemeSwitcherInterface + */ + protected ThemeSwitcherInterface $themeSwitcher; + /** * Constructs a Menu object. * @@ -80,6 +86,8 @@ class EntityRender extends Entity { * Module handler. * @param \Drupal\Core\Access\AccessManager $access_manager * Access manager. + * @param \Drupal\entity_mesh\ThemeSwitcher $theme_switcher + * The theme switcher service. */ public function __construct( RepositoryInterface $entity_mesh_repository, @@ -91,6 +99,7 @@ class EntityRender extends Entity { LanguageNegotiatorSwitcher $language_negotiator_switcher, ModuleHandlerInterface $module_handler, AccessManager $access_manager, + ThemeSwitcher $theme_switcher, ) { parent::__construct($entity_mesh_repository, $entity_type_manager, $language_manager, $config_factory); $this->renderer = $renderer; @@ -99,6 +108,7 @@ class EntityRender extends Entity { $this->languageNegotiatorSwitcher = $language_negotiator_switcher; $this->moduleHandler = $module_handler; $this->accessManager = $access_manager; + $this->themeSwitcher = $theme_switcher; } /** @@ -200,6 +210,9 @@ class EntityRender extends Entity { * @SuppressWarnings(PHPMD.ErrorControlOperator) */ protected function getFullEntityDom(EntityInterface $entity, string $langcode) { + // Switch to the default theme in case the admin theme is enabled. + $previous_theme = $this->themeSwitcher->switchToDefault(); + $this->accountSwitcher->switchTo(new AnonymousUserSession()); // Find LINKs from rendered entity page output. // Switch to the anonymous user: @@ -208,36 +221,46 @@ class EntityRender extends Entity { // in the specific language. $this->languageNegotiatorSwitcher->switchLanguage($entity->language()); - // Render entity HTML output: - $view_mode = 'full'; - $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId()); - - $pre_render = $view_builder->view($entity, $view_mode, $langcode); - // Load all modules before rendering. if (!$this->moduleHandler->isLoaded()) { $this->moduleHandler->loadAll(); } - $render_output = DeprecationHelper::backwardsCompatibleCall( - currentVersion: \Drupal::VERSION, - deprecatedVersion: '10.3', - currentCallable: fn() => $this->renderer->renderInIsolation($pre_render), - deprecatedCallable: fn() => $this->renderer->renderPlain($pre_render), - ); - - // Switches back to the current language: - $this->languageNegotiatorSwitcher->switchBack(); - // Switch back to the current user: - $this->accountSwitcher->switchBack(); - - // Parse the HTML. - $dom = new \DOMDocument(); - // The @ is used to suppress any parsing errors that will be thrown - // if the $html string isn't valid XHTML. - @$dom->loadHTML($render_output); - - return $dom; + try { + // Render entity HTML output: + $view_mode = 'full'; + $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId()); + $pre_render = $view_builder->view($entity, $view_mode, $langcode); + + $render_output = DeprecationHelper::backwardsCompatibleCall( + currentVersion: \Drupal::VERSION, + deprecatedVersion: '10.3', + currentCallable: fn() => $this->renderer->renderInIsolation($pre_render), + deprecatedCallable: fn() => $this->renderer->renderPlain($pre_render), + ); + + // Switches back to the current language: + $this->languageNegotiatorSwitcher->switchBack(); + // Switch back to the current user: + $this->accountSwitcher->switchBack(); + // Restore the original theme. + $this->themeSwitcher->switchBack($previous_theme); + + // Parse the HTML. + $dom = new \DOMDocument(); + // The @ is used to suppress any parsing errors that will be thrown + // if the $html string isn't valid XHTML. + @$dom->loadHTML($render_output); + + return $dom; + } + catch (\Exception $e) { + // Ensure we switch everything back in case of an error. + $this->languageNegotiatorSwitcher->switchBack(); + $this->accountSwitcher->switchBack(); + $this->themeSwitcher->switchBack($previous_theme); + throw $e; + } } /** diff --git a/src/ThemeSwitcher.php b/src/ThemeSwitcher.php new file mode 100644 index 0000000000000000000000000000000000000000..bd41e24a65389d64dac13944607f14931576268d --- /dev/null +++ b/src/ThemeSwitcher.php @@ -0,0 +1,107 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\entity_mesh; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Theme\ActiveTheme; +use Drupal\Core\Theme\MissingThemeDependencyException; +use Drupal\Core\Theme\ThemeInitializationInterface; +use Drupal\Core\Theme\ThemeManagerInterface; + +/** + * Provides simple theme switching for use during indexing. + * + * Service copied from search api. + * + * @see Issue https://www.drupal.org/i/3517383 + */ +class ThemeSwitcher implements ThemeSwitcherInterface { + + /** + * The theme manager service. + * + * @var \Drupal\Core\Theme\ThemeManagerInterface + */ + protected $themeManager; + + /** + * The theme initializer service. + * + * @var \Drupal\Core\Theme\ThemeInitializationInterface + */ + protected $themeInitializer; + + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Constructs a new class instance. + * + * @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager + * The theme manager service. + * @param \Drupal\Core\Theme\ThemeInitializationInterface $themeInitializer + * The theme initializer service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory + * The config factory service. + */ + public function __construct( + ThemeManagerInterface $themeManager, + ThemeInitializationInterface $themeInitializer, + ConfigFactoryInterface $configFactory, + ) { + $this->themeManager = $themeManager; + $this->themeInitializer = $themeInitializer; + $this->configFactory = $configFactory; + } + + /** + * {@inheritdoc} + */ + public function switchToDefault(): ?ActiveTheme { + // Switch to the default theme in case the admin theme (or any other theme) + // is enabled. + $activeTheme = $this->themeManager->getActiveTheme(); + $defaultTheme = $this->configFactory + ->get('system.theme') + ->get('default'); + try { + $defaultTheme = $this->themeInitializer + ->getActiveThemeByName($defaultTheme); + } + catch (MissingThemeDependencyException) { + // It is highly unlikely that the default theme cannot be initialized, but + // in this case the site will have far larger problems than incorrect + // indexing. Just act like all is fine. + return NULL; + } + if ($defaultTheme->getName() === $activeTheme->getName()) { + return NULL; + } + + $this->themeManager->setActiveTheme($defaultTheme); + // Ensure that statically cached default variables are reset correctly, + // especially the directory variable. + drupal_static_reset('template_preprocess'); + // Return the previously active theme, for switching back. + return $activeTheme; + } + + /** + * {@inheritdoc} + */ + public function switchBack(?ActiveTheme $previousTheme): void { + if ($previousTheme === NULL + || $previousTheme === $this->themeManager->getActiveTheme()) { + return; + } + $this->themeManager->setActiveTheme($previousTheme); + drupal_static_reset('template_preprocess'); + } + +} diff --git a/src/ThemeSwitcherInterface.php b/src/ThemeSwitcherInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a14ede96b0ec2ebd19a8751942ecef2f8a94beba --- /dev/null +++ b/src/ThemeSwitcherInterface.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\entity_mesh; + +use Drupal\Core\Theme\ActiveTheme; + +/** + * Provides an interface for the theme switcher service. + */ +interface ThemeSwitcherInterface { + + /** + * Switches to the default theme, in case another theme is active. + * + * @return \Drupal\Core\Theme\ActiveTheme|null + * The previously active theme, or NULL in case the default theme was + * already active. + */ + public function switchToDefault(): ?ActiveTheme; + + /** + * Switches back to the specified theme. + * + * @param \Drupal\Core\Theme\ActiveTheme|null $previousTheme + * The theme to switch to, as returned by switchToDefault(). For the sake of + * simplicity, NULL can be passed which will do nothing. + */ + public function switchBack(?ActiveTheme $previousTheme): void; + +}