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