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

merge-requests/1654/head
Nathaniel Catchpole 2018-06-20 19:03:54 +01:00
parent 34637de7aa
commit de724027fe
29 changed files with 662 additions and 58 deletions

View File

@ -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;
}
}
}
}

View File

@ -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];

View File

@ -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']));

View File

@ -0,0 +1,77 @@
<?php
namespace Drupal\Core\Config\Importer;
use Drupal\Core\Config\ConfigImporter;
/**
* Methods for running the ConfigImporter in a batch.
*
* @see \Drupal\Core\Config\ConfigImporter
*/
class ConfigImporterBatch {
/**
* 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 synchronization step to do.
* @param array $context
* The batch context.
*/
public static function 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'] = 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);
}
}
}

View File

@ -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 <a href=":drupal">Drupal.org</a>.', [':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

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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',

View File

@ -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'));

View File

@ -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']);
}
/**

View File

@ -28,7 +28,7 @@ class NodeImportCreateTest extends KernelTestBase {
$this->installEntitySchema('user');
// Set default storage backend.
$this->installConfig(['field']);
$this->installConfig(['system', 'field']);
}
/**

View File

@ -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.'));
}

View File

@ -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;
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigMultilingualTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles invalid config can not be installed.
*
* @group Installer
*/
class InstallerExistingConfigNoConfigTest extends InstallerExistingConfigTestBase {
protected $profile = 'no_config_profile';
/**
* Final installer step: Configure site.
*/
protected function setUpSite() {
// There are errors therefore there is nothing to do here.
return;
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config.tar.gz';
}
/**
* Tests that profiles with an empty config/sync directory do not work.
*/
public function testConfigSync() {
$this->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');
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Testing installing from config without system.site.
*
* @group Installer
*/
class InstallerExistingConfigNoSystemSiteTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// File API functions are not available yet.
unlink($this->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';
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles with hook_install() can't be installed from config.
*
* @group Installer
*/
class InstallerExistingConfigProfileHookInstall extends InstallerExistingConfigTestBase {
protected $profile = 'config_profile_with_hook_install';
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
// Create an .install file with a hook_install() implementation.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
$contents = <<<EOF
<?php
function config_profile_with_hook_install_install() {
}
EOF;
file_put_contents("$path/{$this->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.');
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
public function setUpSite() {
// The configuration is from a site installed in French.
// So after selecting the profile the installer detects that the site must
// be installed in French, thus we change the button translation.
$this->translations['Save and continue'] = 'Enregistrer et continuer';
parent::setUpSite();
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install.tar.gz';
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Archiver\ArchiveTar;
/**
* Provides a base class for testing installing from existing configuration.
*/
abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
/**
* This is set by the profile in the core.extension extracted.
*/
protected $profile = NULL;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$archiver = new ArchiveTar($this->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);
}
}

View File

@ -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'));

View File

@ -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(

View File

@ -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.

View File

@ -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.

View File

@ -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'));
}

View File

@ -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'));

View File

@ -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();

Binary file not shown.

Binary file not shown.