From ba827d468c40731d3c4fcebfb7e29e765df7a6ea Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Sat, 23 Dec 2023 21:49:48 +0100 Subject: [PATCH] Issue #3103962 by godotislate, smustgrave, alexpott, larowlan: ConfigInstaller::isSyncing() is not reset before module preinstall --- .../Drupal/Core/Extension/ModuleInstaller.php | 30 ++++++++----- core/lib/Drupal/Core/Extension/module.api.php | 17 ++++++- .../modules/system_test/system_test.module | 30 +++++++++++-- .../Core/Config/ConfigImporterTest.php | 44 +++++++++++++++++++ 4 files changed, 104 insertions(+), 17 deletions(-) diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index 4e5734bb6bca..289ecffe41fb 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -179,9 +179,6 @@ public function install(array $module_list, $enable_dependencies = TRUE) { /** @var \Drupal\Core\Config\ConfigInstaller $config_installer */ $config_installer = \Drupal::service('config.installer'); $sync_status = $config_installer->isSyncing(); - if ($sync_status) { - $source_storage = $config_installer->getSourceStorage(); - } $modules_installed = []; foreach ($module_list as $module) { $enabled = $extension_config->get("module.$module") !== NULL; @@ -268,7 +265,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) { } // Allow modules to react prior to the installation of a module. - $this->moduleHandler->invokeAll('module_preinstall', [$module]); + $this->moduleHandler->invokeAll('module_preinstall', [$module, $sync_status]); // Now install the module's schema if necessary. $this->installSchema($module); @@ -326,12 +323,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // Install default configuration of the module. $config_installer = \Drupal::service('config.installer'); - if ($sync_status) { - $config_installer - ->setSyncing(TRUE) - ->setSourceStorage($source_storage); - } - \Drupal::service('config.installer')->installDefaultConfig('module', $module); + $config_installer->installDefaultConfig('module', $module); // If the module has no current updates, but has some that were // previously removed, set the version to the value of @@ -467,7 +459,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { } // Allow modules to react prior to the uninstallation of a module. - $this->moduleHandler->invokeAll('module_preuninstall', [$module]); + $this->moduleHandler->invokeAll('module_preuninstall', [$module, $sync_status]); // Uninstall the module. $this->moduleHandler->loadInclude($module, 'install'); @@ -604,10 +596,17 @@ function ($definition) { /** * Updates the kernel module list. * - * @param string $module_filenames + * @param string[] $module_filenames * The list of installed modules. */ protected function updateKernel($module_filenames) { + // Save current state of config installer, so it can be restored after the + // container is rebuilt. + /** @var \Drupal\Core\Config\ConfigInstallerInterface $config_installer */ + $config_installer = $this->kernel->getContainer()->get('config.installer'); + $sync_status = $config_installer->isSyncing(); + $source_storage = $config_installer->getSourceStorage(); + // This reboots the kernel to register the module's bundle and its services // in the service container. The $module_filenames argument is taken over as // %container.modules% parameter, which is passed to a fresh ModuleHandler @@ -619,6 +618,13 @@ protected function updateKernel($module_filenames) { $this->moduleHandler = $container->get('module_handler'); $this->connection = $container->get('database'); $this->updateRegistry = $container->get('update.update_hook_registry'); + + // Restore state of config installer. + if ($sync_status) { + $container->get('config.installer') + ->setSyncing(TRUE) + ->setSourceStorage($source_storage); + } } /** diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 62f0bd65f937..ed735b833928 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -161,8 +161,15 @@ function hook_system_info_alter(array &$info, \Drupal\Core\Extension\Extension $ * * @param string $module * The name of the module about to be installed. + * @param bool $is_syncing + * TRUE if the module is being installed as part of a configuration import. In + * these cases, your hook implementation needs to carefully consider what + * changes, if any, it should make. For example, it should not make any + * changes to configuration objects or configuration entities. Those changes + * should be made earlier and exported so during import there's no need to + * do them again. */ -function hook_module_preinstall($module) { +function hook_module_preinstall($module, bool $is_syncing) { mymodule_cache_clear(); } @@ -252,8 +259,14 @@ function hook_install($is_syncing) { * * @param string $module * The name of the module about to be uninstalled. + * @param bool $is_syncing + * TRUE if the module is being uninstalled as part of a configuration import. + * In these cases, your hook implementation needs to carefully consider what + * changes to configuration objects or configuration entities. Those changes + * should be made earlier and exported so during import there's no need to + * do them again. */ -function hook_module_preuninstall($module) { +function hook_module_preuninstall($module, bool $is_syncing) { mymodule_cache_clear(); } diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module index e52b030abb1d..c30a87e81f46 100644 --- a/core/modules/system/tests/modules/system_test/system_test.module +++ b/core/modules/system/tests/modules/system_test/system_test.module @@ -35,12 +35,20 @@ function system_test_modules_installed($modules) { /** * Implements hook_modules_uninstalled(). */ -function system_test_modules_uninstalled($modules) { +function system_test_modules_uninstalled($modules, $is_syncing) { if (\Drupal::state()->get('system_test.verbose_module_hooks')) { foreach ($modules as $module) { \Drupal::messenger()->addStatus(t('hook_modules_uninstalled fired for @module', ['@module' => $module])); } } + + // Save the config.installer isSyncing() value to state to check that it is + // correctly set when installing module during config import. + \Drupal::state()->set('system_test_modules_uninstalled_config_installer_syncing', \Drupal::service('config.installer')->isSyncing()); + + // Save the $is_syncing parameter value to state to check that it is correctly + // set when installing module during config import. + \Drupal::state()->set('system_test_modules_uninstalled_syncing_param', $is_syncing); } /** @@ -132,14 +140,30 @@ function system_test_filetransfer_info() { /** * Implements hook_module_preinstall(). */ -function system_test_module_preinstall($module) { +function system_test_module_preinstall($module, bool $is_syncing) { \Drupal::messenger()->addStatus('system_test_preinstall_module called'); \Drupal::state()->set('system_test_preinstall_module', $module); + + // Save the config.installer isSyncing() value to state to check that it is + // correctly set when installing module during config import. + \Drupal::state()->set('system_test_preinstall_module_config_installer_syncing', \Drupal::service('config.installer')->isSyncing()); + + // Save the $is_syncing parameter value to state to check that it is correctly + // set when installing module during config import. + \Drupal::state()->set('system_test_preinstall_module_syncing_param', $is_syncing); } /** * Implements hook_module_preuninstall(). */ -function system_test_module_preuninstall($module) { +function system_test_module_preuninstall($module, bool $is_syncing) { \Drupal::state()->set('system_test_preuninstall_module', $module); + + // Save the config.installer isSyncing() value to state to check that it is + // correctly set when uninstalling module during config import. + \Drupal::state()->set('system_test_preuninstall_module_config_installer_syncing', \Drupal::service('config.installer')->isSyncing()); + + // Save the $is_syncing parameter value to state to check that it is correctly + // set when installing module during config import. + \Drupal::state()->set('system_test_preuninstall_module_syncing_param', $is_syncing); } diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php index bf17339b8b69..9f3e9fd9a294 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php @@ -829,6 +829,50 @@ public function testIsSyncingInHooks() { $this->assertTrue($state['entity_state::predelete'], 'ConfigEntity::isSyncing() returns TRUE'); $this->assertTrue($state['global_state::delete'], '\Drupal::isConfigSyncing() returns TRUE'); $this->assertTrue($state['entity_state::delete'], 'ConfigEntity::isSyncing() returns TRUE'); + + // Test that isSyncing is TRUE in hook_module_preinstall() when installing + // module via config import. + $extensions = $sync->read('core.extension'); + // First, install system_test so that its hook_module_preinstall() will run + // when module_test is installed. + $this->container->get('module_installer')->install(['system_test']); + // Add module_test and system_test to the enabled modules to be imported, + // so that module_test gets installed on import and system_test does not get + // uninstalled. + $extensions['module']['module_test'] = 0; + $extensions['module']['system_test'] = 0; + $extensions['module'] = module_config_sort($extensions['module']); + $sync->write('core.extension', $extensions); + $this->configImporter->reset()->import(); + + // Syncing values stored in state by hook_module_preinstall should be TRUE + // when module is installed via config import. + $this->assertTrue(\Drupal::state()->get('system_test_preinstall_module_config_installer_syncing'), '\Drupal::isConfigSyncing() in system_test_module_preinstall() returns TRUE'); + $this->assertTrue(\Drupal::state()->get('system_test_preinstall_module_syncing_param'), 'system_test_module_preinstall() $is_syncing value is TRUE'); + + // Syncing value stored in state by uninstall hooks should be FALSE + // when uninstalling outside of config import. + $this->container->get('module_installer')->uninstall(['module_test']); + $this->assertFalse(\Drupal::state()->get('system_test_preuninstall_module_config_installer_syncing'), '\Drupal::isConfigSyncing() in system_test_module_preuninstall() returns FALSE'); + $this->assertFalse(\Drupal::state()->get('system_test_preuninstall_module_syncing_param'), 'system_test_module_preuninstall() $is_syncing value is FALSE'); + $this->assertFalse(\Drupal::state()->get('system_test_modules_uninstalled_config_installer_syncing'), '\Drupal::isConfigSyncing() in system_test_modules_uninstalled returns FALSE'); + $this->assertFalse(\Drupal::state()->get('system_test_modules_uninstalled_syncing_param'), 'system_test_modules_uninstalled $is_syncing value is FALSE'); + + // Syncing value stored in state by hook_module_preinstall should be FALSE + // when installing outside of config import. + $this->container->get('module_installer')->install(['module_test']); + $this->assertFalse(\Drupal::state()->get('system_test_preinstall_module_config_installer_syncing'), '\Drupal::isConfigSyncing() in system_test_module_preinstall() returns TRUE'); + $this->assertFalse(\Drupal::state()->get('system_test_preinstall_module_syncing_param'), 'system_test_module_preinstall() $is_syncing value is TRUE'); + + // Uninstall module_test via config import. Syncing value stored in state + // by uninstall hooks should be TRUE. + unset($extensions['module']['module_test']); + $sync->write('core.extension', $extensions); + $this->configImporter->reset()->import(); + $this->assertTrue(\Drupal::state()->get('system_test_preuninstall_module_config_installer_syncing'), '\Drupal::isConfigSyncing() in system_test_module_preuninstall() returns TRUE'); + $this->assertTrue(\Drupal::state()->get('system_test_preuninstall_module_syncing_param'), 'system_test_module_preuninstall() $is_syncing value is TRUE'); + $this->assertTrue(\Drupal::state()->get('system_test_modules_uninstalled_config_installer_syncing'), '\Drupal::isConfigSyncing() in system_test_modules_uninstalled returns TRUE'); + $this->assertTrue(\Drupal::state()->get('system_test_modules_uninstalled_syncing_param'), 'system_test_modules_uninstalled $is_syncing value is TRUE'); } /** -- GitLab