diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index cfc0497bbc..c35008ad68 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -6,6 +6,9 @@ */ use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\ConfigImporterException; +use Drupal\Core\Config\StorageComparer; use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; use Drupal\Core\Database\DatabaseExceptionWrapper; @@ -188,6 +191,12 @@ 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, + // TRUE when installing from existing configuration. + 'config_install' => FALSE, + // The path to the configuration to install when installing from config. + 'profile_sync' => NULL, // TRUE when there are valid config directories. 'config_verified' => FALSE, // TRUE when there is a valid database connection. @@ -445,10 +454,16 @@ function install_begin_request($class_loader, &$install_state) { } } - // 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'])) { - $install_state['parameters']['langcode'] = $install_state['profile_info']['distribution']['langcode']; + // Use the language from profile configuration if available. + if (!empty($install_state['config_install']) && $install_state['config']['system.site']) { + $install_state['parameters']['langcode'] = $install_state['config']['system.site']['default_langcode']; + } + else { + // Otherwise, 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'])) { + $install_state['parameters']['langcode'] = $install_state['profile_info']['distribution']['langcode']; + } } // Set the default language to the selected language, if any. @@ -787,6 +802,33 @@ function install_tasks($install_state) { ], ]; + if (!empty($install_state['config_install'])) { + // 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 synchronisation 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 = [ + 'drupal_install_config_directories' => [ + 'run' => empty($install_state['profile_sync']) ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED, + ], + 'install_config_import_batch' => [ + 'display_name' => t('Install configuration'), + 'type' => 'batch', + ], + 'install_config_download_translations' => [], + 'install_config_fix_profile' => [], + ]; + $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 @@ -1461,9 +1503,25 @@ function _install_get_version_info($version) { * profile information will be added here. */ function install_load_profile(&$install_state) { + global $config_directories; + $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 (!empty($install_state['profile_info']['config_install']) && !empty($config_directories[CONFIG_SYNC_DIRECTORY])) { + $install_state['config_install'] = TRUE; + $install_state['config']['system.site'] = \Drupal::service('config.storage.sync')->read('system.site'); + } + elseif (!empty($install_state['profile_info']['profile_sync'])) { + // If the profile has a config/sync directory copy the information to the + // install_state global. + $install_state['config_install'] = TRUE; + $install_state['profile_sync'] = $install_state['profile_info']['profile_sync']; + + if (!empty($install_state['profile_info']['config'])) { + $install_state['config'] = $install_state['profile_info']['config']; + } + } } /** @@ -2217,3 +2275,194 @@ 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 = [ + 'operations' => [], + 'finished' => 'install_config_import_batch_finish', + 'title' => t('Synchronizing configuration'), + 'init_message' => t('Starting configuration synchronization.'), + 'progress_message' => t('Completed @current step of @total.'), + 'error_message' => t('Configuration synchronization has encountered an error.'), + 'file' => drupal_get_path('module', 'config') . '/config.admin.inc', + ]; + foreach ($sync_steps as $sync_step) { + $batch['operations'][] = ['install_config_import_batch_process', [$config_importer, $sync_step]]; + } + + return $batch; + } + catch (ConfigImporterException $e) { + global $install_state; + // There are validation errors. + drupal_set_message(t('The configuration synchronization failed validation.'), 'error'); + foreach ($config_importer->getErrors() as $message) { + drupal_set_message($message, 'error'); + } + install_display_output(['#title' => t('Configuration validation')], $install_state); + } +} + +/** + * Processes the config import batch and persists the importer. + * + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * The batch config importer object to persist. + * @param string $sync_step + * The synchronisation step to do. + * @param $context + * The batch context. + * + * @see install_config_import_batch() + */ +function install_config_import_batch_process(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'] += $errors; + } +} + +/** + * Finish config importer batch. + * + * @see install_config_import_batch() + */ +function install_config_import_batch_finish($success, $results, $operations) { + if ($success) { + if (!empty($results['errors'])) { + foreach ($results['errors'] as $error) { + drupal_set_message($error, 'error'); + \Drupal::logger('config_sync')->error($error); + } + drupal_set_message(t('The configuration was imported with errors.'), 'warning'); + } + else { + // Configuration sync needs a complete cache flush. + drupal_flush_all_caches(); + } + } + 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], + // The arguments are usually added with print_r which does not handle + // circular references very well. All operations use the $config_importer. + '@arguments' => sprintf('%s::%s', get_class($error_operation[1][0]), $error_operation[1][1]) + ]); + drupal_set_message($message, 'error'); + } +} + + +/** + * 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); + } +} + + +/** + * Fixes configuration if the install profile has made changes in hook_install(). + */ +function install_config_fix_profile() { + global $install_state; + // It is possible that installing the profile makes unintended configuration + // changes. + $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; + // There are validation errors. + drupal_set_message(t('The configuration synchronization failed validation.'), 'error'); + foreach ($config_importer->getErrors() as $message) { + drupal_set_message($message, 'error'); + } + 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 c5b93b58a6..5ba241bbf0 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -10,6 +10,7 @@ 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; @@ -482,12 +483,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['profile_sync'])) { + // 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, + // 'profile_sync' is a path to the directory. + $config_directories[CONFIG_SYNC_DIRECTORY] = $install_state['profile_sync']; + } $settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [ 'value' => $config_directories[CONFIG_SYNC_DIRECTORY], 'required' => TRUE, @@ -1090,6 +1099,13 @@ function install_profile_info($profile, $langcode = 'en') { $info['dependencies'] = array_unique(array_merge($required, $info['dependencies'], $locale)); + // If the profile has a config/sync directory use that to install drupal. + $profile_path = drupal_get_path('profile', $profile); + if (is_dir($profile_path . '/config/sync')) { + $info['profile_sync'] = $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 06fed4b1b0..25937e691e 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -405,6 +405,14 @@ protected function createExtensionChangelist() { $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 + // 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/Installer/Form/SiteConfigureForm.php b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php index c28b7f8fd4..0632ca9233 100644 --- a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php +++ b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php @@ -118,6 +118,7 @@ protected function getEditableConfigNames() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + global $install_state; $form['#title'] = $this->t('Configure site'); // Warn about settings.php permissions risk @@ -145,12 +146,14 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['site_information'] = [ '#type' => 'fieldgroup', '#title' => $this->t('Site information'), + '#access' => empty($install_state['config_install']), ]; $form['site_information']['site_name'] = [ '#type' => 'textfield', '#title' => $this->t('Site name'), '#required' => TRUE, '#weight' => -20, + '#access' => empty($install_state['config_install']), ]; $form['site_information']['site_mail'] = [ '#type' => 'email', @@ -159,6 +162,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#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']), ]; $form['admin_account'] = [ @@ -188,6 +192,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['regional_settings'] = [ '#type' => 'fieldgroup', '#title' => $this->t('Regional settings'), + '#access' => empty($install_state['config_install']), ]; $countries = $this->countryManager->getList(); $form['regional_settings']['site_default_country'] = [ @@ -198,6 +203,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#options' => $countries, '#description' => $this->t('Select the default country for the site.'), '#weight' => 0, + '#access' => empty($install_state['config_install']), ]; $form['regional_settings']['date_default_timezone'] = [ '#type' => 'select', @@ -208,17 +214,20 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#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']), ]; $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']), ]; $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']), ]; $form['update_notifications']['enable_update_status_emails'] = [ '#type' => 'checkbox', @@ -229,6 +238,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { 'input[name="enable_update_status_module"]' => ['checked' => TRUE], ], ], + '#access' => empty($install_state['config_install']), ]; $form['actions'] = ['#type' => 'actions']; @@ -255,21 +265,25 @@ public function validateForm(array &$form, FormStateInterface $form_state) { * {@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'])) { + $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']) && $update_status_module) { $this->moduleInstaller->install(['file', 'update'], FALSE); // Add the site maintenance account's email address to the list of