From de724027fe48a96562bf6566cd2f62de2ba64a73 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 20 Jun 2018 19:03:54 +0100 Subject: [PATCH] Issue #2788777 by alexpott, bircher, jribeiro, Eli-T, mpotter, douggreen, GoZ, DamienMcKenna, Dane Powell, jibran, szeidler, Alumei, andypost, dawehner, johndevman: Allow a site-specific profile to be installed from existing config --- core/includes/install.core.inc | 182 +++++++++++++++++- core/includes/install.inc | 28 ++- .../lib/Drupal/Core/Config/ConfigImporter.php | 8 + .../Config/Importer/ConfigImporterBatch.php | 77 ++++++++ .../Core/Installer/Form/SiteConfigureForm.php | 32 ++- core/modules/config/src/Form/ConfigSync.php | 50 ++--- .../ConfigUninstallViaCliImportTest.php | 1 + .../ContentModerationWorkflowConfigTest.php | 2 +- .../ContentTranslationConfigImportTest.php | 1 + .../Kernel/Config/NodeImportChangeTest.php | 2 +- .../Kernel/Config/NodeImportCreateTest.php | 2 +- .../system/src/SystemConfigSubscriber.php | 3 + core/modules/system/system.install | 13 ++ ...nstallerExistingConfigMultilingualTest.php | 24 +++ .../InstallerExistingConfigNoConfigTest.php | 42 ++++ ...nstallerExistingConfigNoSystemSiteTest.php | 49 +++++ ...tallerExistingConfigProfileHookInstall.php | 64 ++++++ .../Installer/InstallerExistingConfigTest.php | 30 +++ .../InstallerExistingConfigTestBase.php | 99 ++++++++++ .../Core/Config/ConfigImportRecreateTest.php | 2 +- .../ConfigImportRenameValidationTest.php | 2 +- .../ConfigImporterMissingContentTest.php | 2 +- .../Core/Config/ConfigImporterTest.php | 2 +- .../Core/Config/ConfigOverrideTest.php | 1 + .../Core/Config/ConfigSnapshotTest.php | 1 + .../Entity/ContentEntityNullStorageTest.php | 1 + .../config_install/multilingual.tar.gz | Bin 0 -> 11753 bytes .../testing_config_install.tar.gz | Bin 0 -> 10802 bytes .../testing_config_install_no_config.tar.gz | Bin 0 -> 1024 bytes 29 files changed, 662 insertions(+), 58 deletions(-) create mode 100644 core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigMultilingualTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoConfigTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigNoSystemSiteTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigProfileHookInstall.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php create mode 100644 core/tests/fixtures/config_install/multilingual.tar.gz create mode 100644 core/tests/fixtures/config_install/testing_config_install.tar.gz create mode 100644 core/tests/fixtures/config_install/testing_config_install_no_config.tar.gz 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 0000000000000000000000000000000000000000..d43aafa1e58ad82b62fa487bf9d28554e295e630 GIT binary patch literal 11753 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7cd14a2e3a67bfa578c20ad20e9bf19e353b1b80 GIT binary patch literal 10802 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