From d3fdc93bc57e4349bebf4c6127cabdbf1eb63904 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Sat, 30 Mar 2024 14:59:24 +0000 Subject: [PATCH] Issue #3432602 by alexpott, phenaproxima: Allow a site to be installed from configuration with no profile --- core/includes/install.core.inc | 56 ++++++++++++------- core/includes/install.inc | 17 +++++- .../Core/Installer/Form/SelectProfileForm.php | 35 ++++++++---- .../Core/Test/FunctionalTestSetupTrait.php | 20 ++++--- .../InstallerExistingConfigNoProfileTest.php | 19 +++++++ .../InstallerExistingConfigTestBase.php | 52 +++++++++++------ 6 files changed, 138 insertions(+), 61 deletions(-) create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoProfileTest.php diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index ebb746cb5c11..0672b37bbf8f 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -303,8 +303,16 @@ function install_begin_request($class_loader, &$install_state) { } // Validate certain core settings that are used throughout the installation. - if (!empty($install_state['parameters']['profile'])) { + if (array_key_exists('profile', $install_state['parameters'])) { $install_state['parameters']['profile'] = preg_replace('/[^a-zA-Z_0-9]/', '', $install_state['parameters']['profile']); + // If the site is not using an install profile then profile will be set to + // an empty string in the query parameters. Convert this to a FALSE value so + // that \Drupal\Core\Installer\InstallerKernel::getInstallProfile() returns + // the same value as \Drupal\Core\DrupalKernel::getInstallProfile() when the + // site does not have an install profile. + if ($install_state['parameters']['profile'] === '') { + $install_state['parameters']['profile'] = FALSE; + } } if (!empty($install_state['parameters']['langcode'])) { $install_state['parameters']['langcode'] = preg_replace('/[^a-zA-Z_0-9\-]/', '', $install_state['parameters']['langcode']); @@ -451,7 +459,8 @@ function install_begin_request($class_loader, &$install_state) { $module_list->setPathname($name, $profile->getPathname()); } - if ($profile = _install_select_profile($install_state)) { + $profile = _install_select_profile($install_state); + if ($profile !== NULL) { $install_state['parameters']['profile'] = $profile; install_load_profile($install_state); if (isset($install_state['profile_info']['distribution']['install']['theme'])) { @@ -835,15 +844,17 @@ function install_tasks($install_state) { // Load the profile install file, because it is not always loaded when // hook_install_tasks() is invoked (e.g. batch processing). $profile = $install_state['parameters']['profile']; - $profile_install_file = $install_state['profiles'][$profile]->getPath() . '/' . $profile . '.install'; - if (file_exists($profile_install_file)) { - include_once \Drupal::root() . '/' . $profile_install_file; - } - $function = $install_state['parameters']['profile'] . '_install_tasks'; - if (function_exists($function)) { - $result = $function($install_state); - if (is_array($result)) { - $tasks += $result; + if ($profile !== FALSE) { + $profile_install_file = $install_state['profiles'][$profile]->getPath() . '/' . $profile . '.install'; + if (file_exists($profile_install_file)) { + include_once \Drupal::root() . '/' . $profile_install_file; + } + $function = $install_state['parameters']['profile'] . '_install_tasks'; + if (function_exists($function)) { + $result = $function($install_state); + if (is_array($result)) { + $tasks += $result; + } } } } @@ -1202,7 +1213,7 @@ function install_database_errors($database, $settings_file) { * thrown if a profile cannot be chosen automatically. */ function install_select_profile(&$install_state) { - if (empty($install_state['parameters']['profile'])) { + if (!array_key_exists('profile', $install_state['parameters'])) { // If there are no profiles at all, installation cannot proceed. if (empty($install_state['profiles'])) { throw new NoProfilesException(\Drupal::service('string_translation')); @@ -1245,9 +1256,9 @@ function install_select_profile(&$install_state) { * The current installer state, containing a 'profiles' key, which is an * associative array of profiles with the machine-readable names as keys. * - * @return string|null + * @return string|null|false * The machine-readable name of the selected profile or NULL if no profile was - * selected. + * selected or FALSE if the site has no profile. * * @see install_select_profile() */ @@ -1257,9 +1268,9 @@ function _install_select_profile(&$install_state) { return key($install_state['profiles']); } // If a valid profile has already been selected, return the selection. - if (!empty($install_state['parameters']['profile'])) { + if (array_key_exists('profile', $install_state['parameters'])) { $profile = $install_state['parameters']['profile']; - if (isset($install_state['profiles'][$profile])) { + if ($profile === FALSE || isset($install_state['profiles'][$profile])) { return $profile; } } @@ -1497,8 +1508,10 @@ function _install_get_version_info($version) { */ function install_load_profile(&$install_state) { $profile = $install_state['parameters']['profile']; - $install_state['profiles'][$profile]->load(); - $install_state['profile_info'] = install_profile_info($profile, $install_state['parameters']['langcode'] ?? 'en'); + if ($profile !== FALSE) { + $install_state['profiles'][$profile]->load(); + $install_state['profile_info'] = install_profile_info($profile, $install_state['parameters']['langcode'] ?? 'en'); + } $sync_directory = Settings::get('config_sync_directory'); if (!empty($install_state['parameters']['existing_config']) && !empty($sync_directory)) { @@ -2047,10 +2060,13 @@ function install_check_translations($langcode, $server_pattern) { * Checks installation requirements and reports any errors. */ function install_check_requirements($install_state) { + $requirements = []; $profile = $install_state['parameters']['profile']; - // Check the profile requirements. - $requirements = drupal_check_profile($profile); + if ($profile !== FALSE) { + // Check the profile requirements. + $requirements = drupal_check_profile($profile); + } if ($install_state['settings_verified']) { return $requirements; diff --git a/core/includes/install.inc b/core/includes/install.inc index 8eb0630b7b9c..3474c7c3cafa 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -147,9 +147,15 @@ function drupal_install_profile_distribution_version() { * * @return array * The list of modules to install. + * + * @todo https://www.drupal.org/i/3005959 Rework this method as it is not only + * about profiles. */ function drupal_verify_profile($install_state) { $profile = $install_state['parameters']['profile']; + if ($profile === FALSE) { + return []; + } $info = $install_state['profile_info']; // Get the list of available modules for the selected installation profile. @@ -233,9 +239,14 @@ function drupal_install_system($install_state) { // Store the installation profile in configuration to populate the // 'install_profile' container parameter. - \Drupal::configFactory()->getEditable('core.extension') - ->set('profile', $install_state['parameters']['profile']) - ->save(); + $config = \Drupal::configFactory()->getEditable('core.extension'); + if ($install_state['parameters']['profile'] === FALSE) { + $config->clear('profile'); + } + else { + $config->set('profile', $install_state['parameters']['profile']); + } + $config->save(); $connection = Database::getConnection(); $provider = $connection->getProvider(); diff --git a/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php b/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php index 98578d10f816..5c2becd1bff6 100644 --- a/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php +++ b/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php @@ -88,18 +88,29 @@ public function buildForm(array $form, FormStateInterface $form_state, $install_ $sync = new FileStorage($config_sync_directory); $extensions = $sync->read('core.extension'); $site = $sync->read('system.site'); - if (isset($site['name']) && isset($extensions['profile']) && in_array($extensions['profile'], array_keys($names), TRUE)) { - // Ensure the profile can be installed from configuration. Install - // profile's which implement hook_INSTALL() are not supported. - // @todo https://www.drupal.org/project/drupal/issues/2982052 Remove - // this restriction. - $root = \Drupal::root(); - include_once $root . '/core/includes/install.inc'; - $file = $root . '/' . $install_state['profiles'][$extensions['profile']]->getPath() . "/{$extensions['profile']}.install"; - if (is_file($file)) { - require_once $file; + if (isset($site['name'])) { + $install_from_config = FALSE; + if (isset($extensions['profile']) && array_key_exists($extensions['profile'], $names)) { + // Ensure the profile can be installed from configuration. Install + // profile's which implement hook_INSTALL() are not supported. + // @todo https://www.drupal.org/project/drupal/issues/2982052 Remove + // this restriction. + $root = \Drupal::root(); + include_once $root . '/core/includes/install.inc'; + $file = $root . '/' . $install_state['profiles'][$extensions['profile']]->getPath() . "/{$extensions['profile']}.install"; + if (is_file($file)) { + require_once $file; + } + if (!function_exists($extensions['profile'] . '_install')) { + $install_from_config = TRUE; + } } - if (!function_exists($extensions['profile'] . '_install')) { + elseif (empty($extensions['profile'])) { + // Allow sites without a profile to be installed. + $install_from_config = TRUE; + } + + if ($install_from_config) { $form['profile']['#options'][static::CONFIG_INSTALL_PROFILE_KEY] = $this->t('Use existing configuration'); $form['profile'][static::CONFIG_INSTALL_PROFILE_KEY]['#description'] = [ 'description' => [ @@ -139,7 +150,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $profile = $form_state->getValue('profile'); if ($profile === static::CONFIG_INSTALL_PROFILE_KEY) { $sync = new FileStorage(Settings::get('config_sync_directory')); - $profile = $sync->read('core.extension')['profile']; + $profile = $sync->read('core.extension')['profile'] ?? FALSE; $install_state['parameters']['existing_config'] = TRUE; } $install_state['parameters']['profile'] = $profile; diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index 363eba0bf0ef..b7c6d8861231 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -425,16 +425,18 @@ protected function installDefaultThemeFromClassProperty(ContainerInterface $cont // not already specified. $profile = $container->getParameter('install_profile'); - $default_sync_path = $container->get('extension.list.profile')->getPath($profile) . '/config/sync'; - $profile_config_storage = new FileStorage($default_sync_path, StorageInterface::DEFAULT_COLLECTION); - if (!isset($this->defaultTheme) && $profile_config_storage->exists('system.theme')) { - $this->defaultTheme = $profile_config_storage->read('system.theme')['default']; - } + if (!empty($profile)) { + $default_sync_path = $container->get('extension.list.profile')->getPath($profile) . '/config/sync'; + $profile_config_storage = new FileStorage($default_sync_path, StorageInterface::DEFAULT_COLLECTION); + if (!isset($this->defaultTheme) && $profile_config_storage->exists('system.theme')) { + $this->defaultTheme = $profile_config_storage->read('system.theme')['default']; + } - $default_install_path = $container->get('extension.list.profile')->getPath($profile) . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY; - $profile_config_storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION); - if (!isset($this->defaultTheme) && $profile_config_storage->exists('system.theme')) { - $this->defaultTheme = $profile_config_storage->read('system.theme')['default']; + $default_install_path = $container->get('extension.list.profile')->getPath($profile) . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY; + $profile_config_storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION); + if (!isset($this->defaultTheme) && $profile_config_storage->exists('system.theme')) { + $this->defaultTheme = $profile_config_storage->read('system.theme')['default']; + } } // Require a default theme to be specified at this point. diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoProfileTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoProfileTest.php new file mode 100644 index 000000000000..e10cb2910fec --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoProfileTest.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\FunctionalTests\Installer; + +/** + * Verifies that installing from existing configuration without a profile works. + * + * @group Installer + */ +class InstallerExistingConfigNoProfileTest extends InstallerExistingConfigTest { + + /** + * Tests the install from config without a profile. + */ + protected $profile = FALSE; + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php index fc98122f37b3..44347c668723 100644 --- a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php @@ -16,6 +16,8 @@ abstract class InstallerExistingConfigTestBase extends InstallerTestBase { /** * This is set by the profile in the core.extension extracted. + * + * If set to FALSE, then the install will proceed without an install profile. */ protected $profile = NULL; @@ -36,16 +38,30 @@ protected function prepareEnvironment() { $this->profile = $core_extension['profile']; } - // Create a profile for testing. We set core_version_requirement to '*' for - // the test so that it does not need to be updated between major versions. - $info = [ - 'type' => 'profile', - 'core_version_requirement' => '*', - 'name' => 'Configuration installation test profile (' . $this->profile . ')', - ]; + if ($this->profile !== FALSE) { + // Create a profile for testing. We set core_version_requirement to '*' for + // the test so that it does not need to be updated between major versions. + $info = [ + 'type' => 'profile', + 'core_version_requirement' => '*', + 'name' => 'Configuration installation test profile (' . $this->profile . ')', + ]; + + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/' . $this->profile; + + // Put the sync directory inside the profile. + $config_sync_directory = $path . '/config/sync'; + + mkdir($path, 0777, TRUE); + file_put_contents("$path/{$this->profile}.info.yml", Yaml::encode($info)); + } + else { + // If we have no profile we must use an existing sync directory. + $this->existingSyncDirectory = TRUE; + $config_sync_directory = $this->siteDirectory . '/config/sync'; + } - // File API functions are not available yet. - $path = $this->siteDirectory . '/profiles/' . $this->profile; if ($this->existingSyncDirectory) { $config_sync_directory = $this->siteDirectory . '/config/sync'; $this->settings['settings']['config_sync_directory'] = (object) [ @@ -53,13 +69,6 @@ protected function prepareEnvironment() { 'required' => TRUE, ]; } - else { - // Put the sync directory inside the profile. - $config_sync_directory = $path . '/config/sync'; - } - - mkdir($path, 0777, TRUE); - file_put_contents("$path/{$this->profile}.info.yml", Yaml::encode($info)); // Create config/sync directory and extract tarball contents to it. mkdir($config_sync_directory, 0777, TRUE); @@ -81,8 +90,17 @@ protected function prepareEnvironment() { if ($module !== 'core') { $core_extension['module'][$module] = 0; $core_extension['module'] = module_config_sort($core_extension['module']); - file_put_contents($config_sync_directory . '/core.extension.yml', Yaml::encode($core_extension)); } + if ($this->profile === FALSE && array_key_exists('profile', $core_extension)) { + // Remove the profile. + unset($core_extension['module'][$core_extension['profile']]); + unset($core_extension['profile']); + + // Set a default theme to the first theme that will be installed as this + // can not be retrieved from the profile. + $this->defaultTheme = array_key_first($core_extension['theme']); + } + file_put_contents($config_sync_directory . '/core.extension.yml', Yaml::encode($core_extension)); } } -- GitLab