diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index c608052cfb6..f2a5a6a64d7 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -6,6 +6,11 @@ */ use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Batch\BatchBuilder; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\ConfigImporterException; +use Drupal\Core\Config\Importer\ConfigImporterBatch; +use Drupal\Core\Config\StorageComparer; use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; use Drupal\Core\Database\DatabaseExceptionWrapper; @@ -198,6 +203,10 @@ function install_state_defaults() { // The last task that was completed during the previous installation // request. 'completed_task' => NULL, + // Partial configuration cached during an installation from existing config. + 'config' => NULL, + // The path to the configuration to install when installing from config. + 'config_install_path' => NULL, // TRUE when there are valid config directories. 'config_verified' => FALSE, // TRUE when there is a valid database connection. @@ -473,9 +482,13 @@ function install_begin_request($class_loader, &$install_state) { // @todo Remove as part of https://www.drupal.org/node/2186491 drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml'); - // Use the language from the profile configuration, if available, to override - // the language previously set in the parameters. - if (isset($install_state['profile_info']['distribution']['langcode'])) { + // Use the language from profile configuration if available. + if (!empty($install_state['config_install_path']) && $install_state['config']['system.site']) { + $install_state['parameters']['langcode'] = $install_state['config']['system.site']['default_langcode']; + } + elseif (isset($install_state['profile_info']['distribution']['langcode'])) { + // Otherwise, Use the language from the profile configuration, if available, + // to override the language previously set in the parameters. $install_state['parameters']['langcode'] = $install_state['profile_info']['distribution']['langcode']; } @@ -818,6 +831,30 @@ function install_tasks($install_state) { ], ]; + if (!empty($install_state['config_install_path'])) { + // The chosen profile indicates that rather than installing a new site, an + // instance of the same site should be installed from the given + // configuration. + // That means we need to remove the steps installing the extensions and + // replace them with a configuration synchronization step. + unset($tasks['install_download_translation']); + $key = array_search('install_profile_modules', array_keys($tasks), TRUE); + unset($tasks['install_profile_modules']); + unset($tasks['install_profile_themes']); + unset($tasks['install_install_profile']); + $config_tasks = [ + 'install_config_import_batch' => [ + 'display_name' => t('Install configuration'), + 'type' => 'batch', + ], + 'install_config_download_translations' => [], + 'install_config_revert_install_changes' => [], + ]; + $tasks = array_slice($tasks, 0, $key, TRUE) + + $config_tasks + + array_slice($tasks, $key, NULL, TRUE); + } + // Now add any tasks defined by the installation profile. if (!empty($install_state['parameters']['profile'])) { // Load the profile install file, because it is not always loaded when @@ -1494,6 +1531,14 @@ function install_load_profile(&$install_state) { $profile = $install_state['parameters']['profile']; $install_state['profiles'][$profile]->load(); $install_state['profile_info'] = install_profile_info($profile, isset($install_state['parameters']['langcode']) ? $install_state['parameters']['langcode'] : 'en'); + // If the profile has a config/sync directory copy the information to the + // install_state global. + if (!empty($install_state['profile_info']['config_install_path'])) { + $install_state['config_install_path'] = $install_state['profile_info']['config_install_path']; + if (!empty($install_state['profile_info']['config'])) { + $install_state['config'] = $install_state['profile_info']['config']; + } + } } /** @@ -2260,3 +2305,134 @@ function install_write_profile($install_state) { throw new InstallProfileMismatchException($install_state['parameters']['profile'], $settings_profile, $settings_path, \Drupal::translation()); } } + +/** + * Creates a batch for the config importer to process. + * + * @see install_tasks() + */ +function install_config_import_batch() { + // We need to manually trigger the installation of core-provided entity types, + // as those will not be handled by the module installer. + // @see install_profile_modules() + install_core_entity_type_definitions(); + + // Get the sync storage. + $sync = \Drupal::service('config.storage.sync'); + // Match up the site UUIDs, the install_base_system install task will have + // installed the system module and created a new UUID. + $system_site = $sync->read('system.site'); + \Drupal::configFactory()->getEditable('system.site')->set('uuid', $system_site['uuid'])->save(); + + // Create the storage comparer and the config importer. + $config_manager = \Drupal::service('config.manager'); + $storage_comparer = new StorageComparer($sync, \Drupal::service('config.storage'), $config_manager); + $storage_comparer->createChangelist(); + $config_importer = new ConfigImporter( + $storage_comparer, + \Drupal::service('event_dispatcher'), + $config_manager, + \Drupal::service('lock.persistent'), + \Drupal::service('config.typed'), + \Drupal::service('module_handler'), + \Drupal::service('module_installer'), + \Drupal::service('theme_handler'), + \Drupal::service('string_translation') + ); + + try { + $sync_steps = $config_importer->initialize(); + + $batch_builder = new BatchBuilder(); + $batch_builder + ->setFinishCallback([ConfigImporterBatch::class, 'finish']) + ->setTitle(t('Importing configuration')) + ->setInitMessage(t('Starting configuration import.')) + ->setErrorMessage(t('Configuration import has encountered an error.')); + + foreach ($sync_steps as $sync_step) { + $batch_builder->addOperation([ConfigImporterBatch::class, 'process'], [$config_importer, $sync_step]); + } + + return $batch_builder->toArray(); + } + catch (ConfigImporterException $e) { + global $install_state; + // There are validation errors. + $messenger = \Drupal::messenger(); + $messenger->addError(t('The configuration synchronization failed validation.')); + foreach ($config_importer->getErrors() as $message) { + $messenger->addError($message); + } + install_display_output(['#title' => t('Configuration validation')], $install_state); + } +} + +/** + * Replaces install_download_translation() during configuration installs. + * + * @param array $install_state + * An array of information about the current installation state. + * + * @return string + * A themed status report, or an exception if there are requirement errors. + * Upon successful download the page is reloaded and no output is returned. + * + * @see install_download_translation() + */ +function install_config_download_translations(&$install_state) { + $needs_download = isset($install_state['parameters']['langcode']) && !isset($install_state['translations'][$install_state['parameters']['langcode']]) && $install_state['parameters']['langcode'] !== 'en'; + if ($needs_download) { + return install_download_translation($install_state); + } +} + +/** + * Reverts configuration if hook_install() implementations have made changes. + * + * This step ensures that the final configuration matches the configuration + * provided to the installer. + */ +function install_config_revert_install_changes() { + global $install_state; + + $config_manager = \Drupal::service('config.manager'); + $storage_comparer = new StorageComparer(\Drupal::service('config.storage.sync'), \Drupal::service('config.storage'), $config_manager); + $storage_comparer->createChangelist(); + if ($storage_comparer->hasChanges()) { + $config_importer = new ConfigImporter( + $storage_comparer, + \Drupal::service('event_dispatcher'), + $config_manager, + \Drupal::service('lock.persistent'), + \Drupal::service('config.typed'), + \Drupal::service('module_handler'), + \Drupal::service('module_installer'), + \Drupal::service('theme_handler'), + \Drupal::service('string_translation') + ); + try { + $config_importer->import(); + } + catch (ConfigImporterException $e) { + global $install_state; + $messenger = \Drupal::messenger(); + // There are validation errors. + $messenger->addError(t('The configuration synchronization failed validation.')); + foreach ($config_importer->getErrors() as $message) { + $messenger->addError($message); + } + install_display_output(['#title' => t('Configuration validation')], $install_state); + } + + // At this point the configuration should match completely. + if (\Drupal::moduleHandler()->moduleExists('language')) { + // If the English language exists at this point we need to ensure + // install_download_additional_translations_operations() does not delete + // it. + if (ConfigurableLanguage::load('en')) { + $install_state['profile_info']['keep_english'] = TRUE; + } + } + } +} diff --git a/core/includes/install.inc b/core/includes/install.inc index 56b450ec95c..d65c85fff97 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -10,6 +10,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\OpCodeCache; use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Config\FileStorage; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Site\Settings; @@ -481,12 +482,20 @@ function _drupal_rewrite_settings_dump_one(\stdClass $variable, $prefix = '', $s * @see update_prepare_d8_bootstrap() */ function drupal_install_config_directories() { - global $config_directories; + global $config_directories, $install_state; - // Add a randomized config directory name to settings.php, unless it was - // manually defined in the existing already. + // If settings.php does not contain a config sync directory name we need to + // configure one. if (empty($config_directories[CONFIG_SYNC_DIRECTORY])) { - $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync'; + if (empty($install_state['config_install_path'])) { + // Add a randomized config directory name to settings.php + $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync'; + } + else { + // Install profiles can contain a config sync directory. If they do, + // 'config_install_path' is a path to the directory. + $config_directories[CONFIG_SYNC_DIRECTORY] = $install_state['config_install_path']; + } $settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [ 'value' => $config_directories[CONFIG_SYNC_DIRECTORY], 'required' => TRUE, @@ -1099,9 +1108,10 @@ function install_profile_info($profile, $langcode = 'en') { 'version' => NULL, 'hidden' => FALSE, 'php' => DRUPAL_MINIMUM_PHP, + 'config_install_path' => NULL, ]; - $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml"; - $info = \Drupal::service('info_parser')->parse($profile_file); + $profile_path = drupal_get_path('profile', $profile); + $info = \Drupal::service('info_parser')->parse("$profile_path/$profile.info.yml"); $info += $defaults; // drupal_required_modules() includes the current profile as a dependency. @@ -1114,6 +1124,12 @@ function install_profile_info($profile, $langcode = 'en') { // remove any duplicates. $info['install'] = array_unique(array_merge($info['install'], $required, $info['dependencies'], $locale)); + // If the profile has a config/sync directory use that to install drupal. + if (is_dir($profile_path . '/config/sync')) { + $info['config_install_path'] = $profile_path . '/config/sync'; + $sync = new FileStorage($profile_path . '/config/sync'); + $info['config']['system.site'] = $sync->read('system.site'); + } $cache[$profile][$langcode] = $info; } return $cache[$profile][$langcode]; diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index b58f96358ba..c0937277506 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -405,6 +405,14 @@ class ConfigImporter { $module_list = array_reverse($module_list); $this->extensionChangelist['module']['install'] = array_intersect(array_keys($module_list), $install); + // If we're installing the install profile ensure it comes last. This will + // occur when installing a site from configuration. + $install_profile_key = array_search($new_extensions['profile'], $this->extensionChangelist['module']['install'], TRUE); + if ($install_profile_key !== FALSE) { + unset($this->extensionChangelist['module']['install'][$install_profile_key]); + $this->extensionChangelist['module']['install'][] = $new_extensions['profile']; + } + // Work out what themes to install and to uninstall. $this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme'])); $this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme'])); diff --git a/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php b/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php new file mode 100644 index 00000000000..8aee289e0d1 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php @@ -0,0 +1,77 @@ +doSyncStep($sync_step, $context); + if ($errors = $config_importer->getErrors()) { + if (!isset($context['results']['errors'])) { + $context['results']['errors'] = []; + } + $context['results']['errors'] = array_merge($errors, $context['results']['errors']); + } + } + + /** + * Finish batch. + * + * This function is a static function to avoid serializing the ConfigSync + * object unnecessarily. + * + * @param bool $success + * Indicate that the batch API tasks were all completed successfully. + * @param array $results + * An array of all the results that were updated in update_do_one(). + * @param array $operations + * A list of the operations that had not been completed by the batch API. + */ + public static function finish($success, $results, $operations) { + $messenger = \Drupal::messenger(); + if ($success) { + if (!empty($results['errors'])) { + $logger = \Drupal::logger('config_sync'); + foreach ($results['errors'] as $error) { + $messenger->addError($error); + $logger->error($error); + } + $messenger->addWarning(t('The configuration was imported with errors.')); + } + elseif (!drupal_installation_attempted()) { + // Display a success message when not installing Drupal. + $messenger->addStatus(t('The configuration was imported successfully.')); + } + } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = t('An error occurred while processing %error_operation with arguments: @arguments', ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]); + $messenger->addError($message); + } + } + +} diff --git a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php index d5f8f56a791..2cca9625999 100644 --- a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php +++ b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php @@ -121,6 +121,7 @@ class SiteConfigureForm extends ConfigFormBase { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + global $install_state; $form['#title'] = $this->t('Configure site'); // Warn about settings.php permissions risk @@ -148,12 +149,14 @@ class SiteConfigureForm extends ConfigFormBase { $form['site_information'] = [ '#type' => 'fieldgroup', '#title' => $this->t('Site information'), + '#access' => empty($install_state['config_install_path']), ]; $form['site_information']['site_name'] = [ '#type' => 'textfield', '#title' => $this->t('Site name'), '#required' => TRUE, '#weight' => -20, + '#access' => empty($install_state['config_install_path']), ]; $form['site_information']['site_mail'] = [ '#type' => 'email', @@ -162,6 +165,7 @@ class SiteConfigureForm extends ConfigFormBase { '#description' => $this->t("Automated emails, such as registration information, will be sent from this address. Use an address ending in your site's domain to help prevent these emails from being flagged as spam."), '#required' => TRUE, '#weight' => -15, + '#access' => empty($install_state['config_install_path']), ]; $form['admin_account'] = [ @@ -191,6 +195,7 @@ class SiteConfigureForm extends ConfigFormBase { $form['regional_settings'] = [ '#type' => 'fieldgroup', '#title' => $this->t('Regional settings'), + '#access' => empty($install_state['config_install_path']), ]; $countries = $this->countryManager->getList(); $form['regional_settings']['site_default_country'] = [ @@ -201,6 +206,7 @@ class SiteConfigureForm extends ConfigFormBase { '#options' => $countries, '#description' => $this->t('Select the default country for the site.'), '#weight' => 0, + '#access' => empty($install_state['config_install_path']), ]; $form['regional_settings']['date_default_timezone'] = [ '#type' => 'select', @@ -211,17 +217,20 @@ class SiteConfigureForm extends ConfigFormBase { '#description' => $this->t('By default, dates in this site will be displayed in the chosen time zone.'), '#weight' => 5, '#attributes' => ['class' => ['timezone-detect']], + '#access' => empty($install_state['config_install_path']), ]; $form['update_notifications'] = [ '#type' => 'fieldgroup', '#title' => $this->t('Update notifications'), '#description' => $this->t('The system will notify you when updates and important security releases are available for installed components. Anonymous information about your site is sent to Drupal.org.', [':drupal' => 'https://www.drupal.org']), + '#access' => empty($install_state['config_install_path']), ]; $form['update_notifications']['enable_update_status_module'] = [ '#type' => 'checkbox', '#title' => $this->t('Check for updates automatically'), '#default_value' => 1, + '#access' => empty($install_state['config_install_path']), ]; $form['update_notifications']['enable_update_status_emails'] = [ '#type' => 'checkbox', @@ -232,6 +241,7 @@ class SiteConfigureForm extends ConfigFormBase { 'input[name="enable_update_status_module"]' => ['checked' => TRUE], ], ], + '#access' => empty($install_state['config_install_path']), ]; $form['actions'] = ['#type' => 'actions']; @@ -258,21 +268,25 @@ class SiteConfigureForm extends ConfigFormBase { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $this->config('system.site') - ->set('name', (string) $form_state->getValue('site_name')) - ->set('mail', (string) $form_state->getValue('site_mail')) - ->save(TRUE); + global $install_state; - $this->config('system.date') - ->set('timezone.default', (string) $form_state->getValue('date_default_timezone')) - ->set('country.default', (string) $form_state->getValue('site_default_country')) - ->save(TRUE); + if (empty($install_state['config_install_path'])) { + $this->config('system.site') + ->set('name', (string) $form_state->getValue('site_name')) + ->set('mail', (string) $form_state->getValue('site_mail')) + ->save(TRUE); + + $this->config('system.date') + ->set('timezone.default', (string) $form_state->getValue('date_default_timezone')) + ->set('country.default', (string) $form_state->getValue('site_default_country')) + ->save(TRUE); + } $account_values = $form_state->getValue('account'); // Enable update.module if this option was selected. $update_status_module = $form_state->getValue('enable_update_status_module'); - if ($update_status_module) { + if (empty($install_state['config_install_path']) && $update_status_module) { $this->moduleInstaller->install(['file', 'update'], FALSE); // Add the site maintenance account's email address to the list of diff --git a/core/modules/config/src/Form/ConfigSync.php b/core/modules/config/src/Form/ConfigSync.php index 73c7a1b8b1a..57fb1d860e0 100644 --- a/core/modules/config/src/Form/ConfigSync.php +++ b/core/modules/config/src/Form/ConfigSync.php @@ -4,6 +4,7 @@ namespace Drupal\config\Form; use Drupal\Core\Config\ConfigImporterException; use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\Importer\ConfigImporterBatch; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleInstallerInterface; @@ -337,14 +338,14 @@ class ConfigSync extends FormBase { $sync_steps = $config_importer->initialize(); $batch = [ 'operations' => [], - 'finished' => [get_class($this), 'finishBatch'], + 'finished' => [ConfigImporterBatch::class, 'finish'], 'title' => t('Synchronizing configuration'), 'init_message' => t('Starting configuration synchronization.'), 'progress_message' => t('Completed step @current of @total.'), 'error_message' => t('Configuration synchronization has encountered an error.'), ]; foreach ($sync_steps as $sync_step) { - $batch['operations'][] = [[get_class($this), 'processBatch'], [$config_importer, $sync_step]]; + $batch['operations'][] = [[ConfigImporterBatch::class, 'process'], [$config_importer, $sync_step]]; } batch_set($batch); @@ -368,20 +369,15 @@ class ConfigSync extends FormBase { * The synchronization step to do. * @param array $context * The batch context. + * + * @deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use + * \Drupal\Core\Config\Importer\ConfigImporterBatch::process() instead. + * + * @see https://www.drupal.org/node/2897299 */ public static function processBatch(ConfigImporter $config_importer, $sync_step, &$context) { - if (!isset($context['sandbox']['config_importer'])) { - $context['sandbox']['config_importer'] = $config_importer; - } - - $config_importer = $context['sandbox']['config_importer']; - $config_importer->doSyncStep($sync_step, $context); - if ($errors = $config_importer->getErrors()) { - if (!isset($context['results']['errors'])) { - $context['results']['errors'] = []; - } - $context['results']['errors'] = array_merge($context['results']['errors'], $errors); - } + @trigger_error('\Drupal\config\Form\ConfigSync::processBatch() deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use \Drupal\Core\Config\Importer\ConfigImporterBatch::process() instead. See https://www.drupal.org/node/2897299'); + ConfigImporterBatch::process($config_importer, $sync_step, $context); } /** @@ -389,27 +385,15 @@ class ConfigSync extends FormBase { * * This function is a static function to avoid serializing the ConfigSync * object unnecessarily. + * + * @deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use + * \Drupal\Core\Config\Importer\ConfigImporterBatch::finish() instead. + * + * @see https://www.drupal.org/node/2897299 */ public static function finishBatch($success, $results, $operations) { - if ($success) { - if (!empty($results['errors'])) { - foreach ($results['errors'] as $error) { - \Drupal::messenger()->addError($error); - \Drupal::logger('config_sync')->error($error); - } - \Drupal::messenger()->addWarning(\Drupal::translation()->translate('The configuration was imported with errors.')); - } - else { - \Drupal::messenger()->addStatus(\Drupal::translation()->translate('The configuration was imported successfully.')); - } - } - else { - // An error occurred. - // $operations contains the operations that remained unprocessed. - $error_operation = reset($operations); - $message = \Drupal::translation()->translate('An error occurred while processing %error_operation with arguments: @arguments', ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]); - \Drupal::messenger()->addError($message); - } + @trigger_error('\Drupal\config\Form\ConfigSync::finishBatch() deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use \Drupal\Core\Config\Importer\ConfigImporterBatch::finish() instead. See https://www.drupal.org/node/2897299'); + ConfigImporterBatch::finish($success, $results, $operations); } } diff --git a/core/modules/config/tests/src/Kernel/ConfigUninstallViaCliImportTest.php b/core/modules/config/tests/src/Kernel/ConfigUninstallViaCliImportTest.php index 5f77447f316..0ab0fd1764e 100644 --- a/core/modules/config/tests/src/Kernel/ConfigUninstallViaCliImportTest.php +++ b/core/modules/config/tests/src/Kernel/ConfigUninstallViaCliImportTest.php @@ -32,6 +32,7 @@ class ConfigUninstallViaCliImportTest extends KernelTestBase { $this->markTestSkipped('This test has to be run from the CLI'); } + $this->installConfig(['system']); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); // Set up the ConfigImporter object for testing. diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php index ee4392738bf..1b2125b7dd5 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationWorkflowConfigTest.php @@ -59,7 +59,7 @@ class ContentModerationWorkflowConfigTest extends KernelTestBase { $this->installEntitySchema('node'); $this->installEntitySchema('user'); $this->installEntitySchema('content_moderation_state'); - $this->installConfig('content_moderation'); + $this->installConfig(['system', 'content_moderation']); NodeType::create([ 'type' => 'example', diff --git a/core/modules/content_translation/tests/src/Kernel/ContentTranslationConfigImportTest.php b/core/modules/content_translation/tests/src/Kernel/ContentTranslationConfigImportTest.php index 44e816089ec..0fe6b9f6e0b 100644 --- a/core/modules/content_translation/tests/src/Kernel/ContentTranslationConfigImportTest.php +++ b/core/modules/content_translation/tests/src/Kernel/ContentTranslationConfigImportTest.php @@ -33,6 +33,7 @@ class ContentTranslationConfigImportTest extends KernelTestBase { protected function setUp() { parent::setUp(); + $this->installConfig(['system']); $this->installEntitySchema('entity_test_mul'); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); diff --git a/core/modules/node/tests/src/Kernel/Config/NodeImportChangeTest.php b/core/modules/node/tests/src/Kernel/Config/NodeImportChangeTest.php index e1b17f2c1c9..d670e6a44ca 100644 --- a/core/modules/node/tests/src/Kernel/Config/NodeImportChangeTest.php +++ b/core/modules/node/tests/src/Kernel/Config/NodeImportChangeTest.php @@ -26,7 +26,7 @@ class NodeImportChangeTest extends KernelTestBase { parent::setUp(); // Set default storage backend. - $this->installConfig(['field', 'node_test_config']); + $this->installConfig(['system', 'field', 'node_test_config']); } /** diff --git a/core/modules/node/tests/src/Kernel/Config/NodeImportCreateTest.php b/core/modules/node/tests/src/Kernel/Config/NodeImportCreateTest.php index cb985173ae9..0b27bc2a22c 100644 --- a/core/modules/node/tests/src/Kernel/Config/NodeImportCreateTest.php +++ b/core/modules/node/tests/src/Kernel/Config/NodeImportCreateTest.php @@ -28,7 +28,7 @@ class NodeImportCreateTest extends KernelTestBase { $this->installEntitySchema('user'); // Set default storage backend. - $this->installConfig(['field']); + $this->installConfig(['system', 'field']); } /** diff --git a/core/modules/system/src/SystemConfigSubscriber.php b/core/modules/system/src/SystemConfigSubscriber.php index 519155b8497..0ab0d15fc61 100644 --- a/core/modules/system/src/SystemConfigSubscriber.php +++ b/core/modules/system/src/SystemConfigSubscriber.php @@ -72,6 +72,9 @@ class SystemConfigSubscriber implements EventSubscriberInterface { * The config import event. */ public function onConfigImporterValidateSiteUUID(ConfigImporterEvent $event) { + if (!$event->getConfigImporter()->getStorageComparer()->getSourceStorage()->exists('system.site')) { + $event->getConfigImporter()->logError($this->t('This import does not contain system.site configuration, so has been rejected.')); + } if (!$event->getConfigImporter()->getStorageComparer()->validateSiteUuid()) { $event->getConfigImporter()->logError($this->t('Site UUID in source storage does not match the target storage.')); } diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 3ad360ff242..0fe129b0b6d 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1002,6 +1002,19 @@ function system_requirements($phase) { ]; } + // During installs from configuration don't support install profiles that + // implement hook_install. + if ($phase == 'install' && !empty($install_state['config_install_path'])) { + $install_hook = $install_state['parameters']['profile'] . '_install'; + if (function_exists($install_hook)) { + $requirements['config_install'] = [ + 'title' => t('Configuration install'), + 'value' => $install_state['parameters']['profile'], + 'description' => t('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.'), + 'severity' => REQUIREMENT_ERROR, + ]; + } + } return $requirements; } diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigMultilingualTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigMultilingualTest.php new file mode 100644 index 00000000000..db05d32031e --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigMultilingualTest.php @@ -0,0 +1,24 @@ +assertTitle('Configuration validation | Drupal'); + $this->assertText('The configuration synchronization failed validation.'); + $this->assertText('This import is empty and if applied would delete all of your configuration, so has been rejected.'); + + // Ensure there is no continuation button. + $this->assertNoText('Save and continue'); + $this->assertNoFieldById('edit-submit'); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoSystemSiteTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoSystemSiteTest.php new file mode 100644 index 00000000000..4ac68d4e863 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoSystemSiteTest.php @@ -0,0 +1,49 @@ +siteDirectory . '/profiles/' . $this->profile . '/config/sync/system.site.yml'); + } + + /** + * {@inheritdoc} + */ + public function setUpSite() { + return; + } + + /** + * Tests that profiles with no system.site do not work. + */ + public function testConfigSync() { + $this->htmlOutput(NULL); + $this->assertTitle('Configuration validation | Drupal'); + $this->assertText('The configuration synchronization failed validation.'); + $this->assertText('This import does not contain system.site configuration, so has been rejected.'); + + // Ensure there is no continuation button. + $this->assertNoText('Save and continue'); + $this->assertNoFieldById('edit-submit'); + } + + /** + * {@inheritdoc} + */ + protected function getConfigTarball() { + return __DIR__ . '/../../../fixtures/config_install/testing_config_install.tar.gz'; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigProfileHookInstall.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigProfileHookInstall.php new file mode 100644 index 00000000000..2d9d6cdfc12 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigProfileHookInstall.php @@ -0,0 +1,64 @@ +siteDirectory . '/profiles/' . $this->profile; + $contents = <<profile}.install", $contents); + parent::visitInstaller(); + } + + /** + * Installer step: Configure settings. + */ + protected function setUpSettings() { + // There are errors therefore there is nothing to do here. + return; + } + + /** + * Final installer step: Configure site. + */ + protected function setUpSite() { + // There are errors therefore there is nothing to do here. + return; + } + + /** + * {@inheritdoc} + */ + protected function getConfigTarball() { + // We're not going to get to the config import stage so this does not + // matter. + return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config.tar.gz'; + } + + /** + * Confirms the installation has failed and the expected error is displayed. + */ + public function testConfigSync() { + $this->assertTitle('Requirements problem | Drupal'); + $this->assertText($this->profile); + $this->assertText('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.'); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTest.php new file mode 100644 index 00000000000..ebf4c2a8c5b --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTest.php @@ -0,0 +1,30 @@ +translations['Save and continue'] = 'Enregistrer et continuer'; + parent::setUpSite(); + } + + /** + * {@inheritdoc} + */ + protected function getConfigTarball() { + return __DIR__ . '/../../../fixtures/config_install/testing_config_install.tar.gz'; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php new file mode 100644 index 00000000000..e093be96676 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php @@ -0,0 +1,99 @@ +getConfigTarball(), 'gz'); + + if ($this->profile === NULL) { + $core_extension = Yaml::decode($archiver->extractInString('core.extension.yml')); + $this->profile = $core_extension['profile']; + } + + // Create a profile for testing. + $info = [ + 'type' => 'profile', + 'core' => \Drupal::CORE_COMPATIBILITY, + 'name' => 'Configuration installation test profile (' . $this->profile . ')', + ]; + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/' . $this->profile; + + 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. + $config_sync_directory = $path . '/config/sync'; + mkdir($config_sync_directory, 0777, TRUE); + $files = []; + $list = $archiver->listContent(); + if (is_array($list)) { + /** @var array $list */ + foreach ($list as $file) { + $files[] = $file['filename']; + } + $archiver->extractList($files, $config_sync_directory); + } + } + + /** + * Gets the filepath to the configuration tarball. + * + * The tarball will be extracted to the install profile's config/sync + * directory for testing. + * + * @return string + * The filepath to the configuration tarball. + */ + abstract protected function getConfigTarball(); + + /** + * {@inheritdoc} + */ + protected function installParameters() { + $parameters = parent::installParameters(); + + // The options that change configuration are disabled when installing from + // existing configuration. + unset($parameters['forms']['install_configure_form']['site_name']); + unset($parameters['forms']['install_configure_form']['site_mail']); + unset($parameters['forms']['install_configure_form']['update_status_module']); + + return $parameters; + } + + /** + * Confirms that the installation installed the configuration correctly. + */ + public function testConfigSync() { + // After installation there is no snapshot and nothing to import. + $change_list = $this->configImporter()->getStorageComparer()->getChangelist(); + $expected = [ + 'create' => [], + // The system.mail is changed configuration because the test system + // changes it to ensure that mails are not sent. + 'update' => ['system.mail'], + 'delete' => [], + 'rename' => [], + ]; + $this->assertEqual($expected, $change_list); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php index 7d9c9ae23f7..fef464ed7f2 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php @@ -32,7 +32,7 @@ class ConfigImportRecreateTest extends KernelTestBase { parent::setUp(); $this->installEntitySchema('node'); - $this->installConfig(['field', 'node']); + $this->installConfig(['system', 'field', 'node']); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRenameValidationTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRenameValidationTest.php index 84b5a2de131..f52b3956ab9 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRenameValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImportRenameValidationTest.php @@ -39,7 +39,7 @@ class ConfigImportRenameValidationTest extends KernelTestBase { $this->installEntitySchema('user'); $this->installEntitySchema('node'); - $this->installConfig(['field']); + $this->installConfig(['system', 'field']); // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php index 59f9cb2ed72..db09e3f9f66 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php @@ -33,7 +33,7 @@ class ConfigImporterMissingContentTest extends KernelTestBase { $this->installSchema('system', 'sequences'); $this->installEntitySchema('entity_test'); $this->installEntitySchema('user'); - $this->installConfig(['config_test']); + $this->installConfig(['system', 'config_test']); // Installing config_test's default configuration pollutes the global // variable being used for recording hook invocations by this test already, // so it has to be cleared out manually. diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php index dc4e0ae12b9..6e8adf79a77 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php @@ -33,7 +33,7 @@ class ConfigImporterTest extends KernelTestBase { protected function setUp() { parent::setUp(); - $this->installConfig(['config_test']); + $this->installConfig(['system', 'config_test']); // Installing config_test's default configuration pollutes the global // variable being used for recording hook invocations by this test already, // so it has to be cleared out manually. diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigOverrideTest.php index b2ddaa396dd..550e0d5b2b0 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigOverrideTest.php @@ -20,6 +20,7 @@ class ConfigOverrideTest extends KernelTestBase { protected function setUp() { parent::setUp(); + $this->installConfig(['system']); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); } diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSnapshotTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSnapshotTest.php index 62eff04d417..645638d8323 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSnapshotTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSnapshotTest.php @@ -24,6 +24,7 @@ class ConfigSnapshotTest extends KernelTestBase { */ protected function setUp() { parent::setUp(); + $this->installConfig(['system']); // Update the config snapshot. This allows the parent::setUp() to write // configuration files. \Drupal::service('config.manager')->createSnapshot(\Drupal::service('config.storage'), \Drupal::service('config.storage.snapshot')); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityNullStorageTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityNullStorageTest.php index 6da9bb3baad..001d9ba3dee 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityNullStorageTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityNullStorageTest.php @@ -43,6 +43,7 @@ class ContentEntityNullStorageTest extends KernelTestBase { * @see \Drupal\Core\Entity\Event\BundleConfigImportValidate */ public function testDeleteThroughImport() { + $this->installConfig(['system']); $contact_form = ContactForm::create(['id' => 'test']); $contact_form->save(); diff --git a/core/tests/fixtures/config_install/multilingual.tar.gz b/core/tests/fixtures/config_install/multilingual.tar.gz new file mode 100644 index 00000000000..d43aafa1e58 Binary files /dev/null and b/core/tests/fixtures/config_install/multilingual.tar.gz differ diff --git a/core/tests/fixtures/config_install/testing_config_install.tar.gz b/core/tests/fixtures/config_install/testing_config_install.tar.gz new file mode 100644 index 00000000000..7cd14a2e3a6 Binary files /dev/null and b/core/tests/fixtures/config_install/testing_config_install.tar.gz differ diff --git a/core/tests/fixtures/config_install/testing_config_install_no_config.tar.gz b/core/tests/fixtures/config_install/testing_config_install_no_config.tar.gz new file mode 100644 index 00000000000..06d74050200 Binary files /dev/null and b/core/tests/fixtures/config_install/testing_config_install_no_config.tar.gz differ