Commit 8fbece06 authored by Gábor Hojtsy's avatar Gábor Hojtsy
Browse files

Issue #2980670 by alexpott, bircher, marcvangend, webchick, jibran, Gábor...

Issue #2980670 by alexpott, bircher, marcvangend, webchick, jibran, Gábor Hojtsy, benjifisher, smaz: Install a site from config if the config directory is set in settings.php
parent b8c4f6ec
......@@ -10,6 +10,7 @@
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Config\Importer\ConfigImporterBatch;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Database\Database;
......@@ -1527,16 +1528,23 @@ 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['parameters']['existing_config']) && !empty($config_directories[CONFIG_SYNC_DIRECTORY])) {
$install_state['config_install_path'] = $config_directories[CONFIG_SYNC_DIRECTORY];
}
// 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'])) {
elseif (!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'];
}
}
if (!empty($install_state['config_install_path'])) {
$sync = new FileStorage($install_state['config_install_path']);
$install_state['config']['system.site'] = $sync->read('system.site');
}
}
......
......@@ -10,7 +10,6 @@
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;
......@@ -1127,8 +1126,6 @@ function install_profile_info($profile, $langcode = 'en') {
// 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;
}
......
......@@ -84,6 +84,29 @@ public function onConfigImporterValidate(ConfigImporterEvent $event) {
*/
protected function validateModules(ConfigImporter $config_importer) {
$core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
// Get the install profile from the site's configuration.
$current_core_extension = $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension');
$install_profile = isset($current_core_extension['profile']) ? $current_core_extension['profile'] : NULL;
// Ensure the profile is not changing.
if ($install_profile !== $core_extension['profile']) {
if (drupal_installation_attempted()) {
$config_importer->logError($this->t('The selected installation profile %install_profile does not match the profile stored in configuration %config_profile.', [
'%install_profile' => $install_profile,
'%config_profile' => $core_extension['profile'],
]));
// If this error has occurred the other checks are irrelevant.
return;
}
else {
$config_importer->logError($this->t('Cannot change the install profile from %profile to %new_profile once Drupal is installed.', [
'%profile' => $install_profile,
'%new_profile' => $core_extension['profile'],
]));
}
}
// Get a list of modules with dependency weights as values.
$module_data = $this->getModuleData();
$nonexistent_modules = array_keys(array_diff_key($core_extension['module'], $module_data));
......@@ -111,10 +134,6 @@ protected function validateModules(ConfigImporter $config_importer) {
}
}
// Get the install profile from the site's configuration.
$current_core_extension = $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension');
$install_profile = isset($current_core_extension['profile']) ? $current_core_extension['profile'] : NULL;
// Ensure that all modules being uninstalled are not required by modules
// that will be installed after the import.
$uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall');
......@@ -133,11 +152,6 @@ protected function validateModules(ConfigImporter $config_importer) {
$profile_name = $module_data[$install_profile]->info['name'];
$config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the install profile.', ['%profile' => $profile_name]));
}
// Ensure the profile is not changing.
if ($install_profile !== $core_extension['profile']) {
$config_importer->logError($this->t('Cannot change the install profile from %profile to %new_profile once Drupal is installed.', ['%profile' => $install_profile, '%new_profile' => $core_extension['profile']]));
}
}
/**
......
......@@ -2,6 +2,7 @@
namespace Drupal\Core\Installer\Form;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
......@@ -12,6 +13,13 @@
*/
class SelectProfileForm extends FormBase {
/**
* The key used in the profile list for the install from config option.
*
* This key must not be a valid profile extension name.
*/
const CONFIG_INSTALL_PROFILE_KEY = '::existing_config::';
/**
* {@inheritdoc}
*/
......@@ -23,6 +31,7 @@ public function getFormId() {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $install_state = NULL) {
global $config_directories;
$form['#title'] = $this->t('Select an installation profile');
$profiles = [];
......@@ -78,6 +87,40 @@ public function buildForm(array $form, FormStateInterface $form_state, $install_
$this->addUmamiWarning($form);
}
}
if (!empty($config_directories[CONFIG_SYNC_DIRECTORY])) {
$sync = new FileStorage($config_directories[CONFIG_SYNC_DIRECTORY]);
$extensions = $sync->read('core.extension');
$site = $sync->read('system.site');
if (isset($site['name']) && isset($extensions['profile']) && in_array($extensions['profile'], array_keys($names), TRUE)) {
// Ensure the the profile can be installed from configuration. Install
// profile's which implement hook_INSTALL() are not supported.
// @todo https://www.drupal.org/project/drupal/issues/2982052 Remove
// this restriction.
module_load_install($extensions['profile']);
if (!function_exists($extensions['profile'] . '_install')) {
$form['profile']['#options'][static::CONFIG_INSTALL_PROFILE_KEY] = $this->t('Use existing configuration');
$form['profile'][static::CONFIG_INSTALL_PROFILE_KEY]['#description'] = [
'description' => [
'#markup' => $this->t('Install %name using existing configuration.', ['%name' => $site['name']]),
],
'info' => [
'#type' => 'item',
'#markup' => $this->t('The configuration from the directory %sync_directory will be used.', ['%sync_directory' => $config_directories[CONFIG_SYNC_DIRECTORY]]),
'#wrapper_attributes' => [
'class' => ['messages', 'messages--status'],
],
'#states' => [
'visible' => [
':input[name="profile"]' => ['value' => static::CONFIG_INSTALL_PROFILE_KEY],
],
],
],
];
}
}
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
......@@ -91,8 +134,14 @@ public function buildForm(array $form, FormStateInterface $form_state, $install_
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
global $install_state;
$install_state['parameters']['profile'] = $form_state->getValue('profile');
global $install_state, $config_directories;
$profile = $form_state->getValue('profile');
if ($profile === static::CONFIG_INSTALL_PROFILE_KEY) {
$sync = new FileStorage($config_directories[CONFIG_SYNC_DIRECTORY]);
$profile = $sync->read('core.extension')['profile'];
$install_state['parameters']['existing_config'] = TRUE;
}
$install_state['parameters']['profile'] = $profile;
}
/**
......
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigSyncDirectoryMultilingualTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
// Ensure the site name 'Multilingual' appears as expected in the 'Use
// existing configuration' radio description.
$this->assertSession()->pageTextContains('Install Multilingual using existing configuration.');
return parent::setUpProfile();
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
}
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles with hook_install() can't be installed from config.
*
* @group Installer
*/
class InstallerExistingConfigSyncDirectoryProfileHookInstall extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* {@inheritdoc}
*/
protected function visitInstaller() {
// Create an .install file with a hook_install() implementation.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
$contents = <<<EOF
<?php
function testing_config_install_multilingual_install() {
}
EOF;
file_put_contents("$path/{$this->profile}.install", $contents);
parent::visitInstaller();
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
// This is the form we are testing so wait until the test method to do
// assertions.
return;
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings() {
// This form will never be reached
return;
}
/**
* Final installer step: Configure site.
*/
protected function setUpSite() {
// This form will never be reached
return;
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
/**
* Tests installing from config is not available due to hook_INSTALL().
*/
public function testConfigSync() {
$this->assertSession()->titleEquals('Select an installation profile | Drupal');
$this->assertSession()->responseNotContains('Use existing configuration');
// Remove the install hook and the option to install from existing
// configuration will be available.
unlink("{$this->siteDirectory}/profiles/{$this->profile}/{$this->profile}.install");
$this->getSession()->reload();
$this->assertSession()->titleEquals('Select an installation profile | Drupal');
$this->assertSession()->responseContains('Use existing configuration');
}
}
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigSyncDriectoryProfileMismatchTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings() {
// Cause a profile mismatch by hacking the URL.
$this->drupalGet(str_replace($this->profile, 'minimal', $this->getUrl()));
parent::setUpSettings();
}
protected function setUpSite() {
// This step will not occur because there is an error.
return;
}
/**
* Tests that profile mismatch fails to install.
*/
public function testConfigSync() {
$this->htmlOutput(NULL);
$this->assertTitle('Configuration validation | Drupal');
$this->assertText('The configuration synchronization failed validation.');
$this->assertText('The selected installation profile minimal does not match the profile stored in configuration testing_config_install_multilingual.');
// Ensure there is no continuation button.
$this->assertNoText('Save and continue');
$this->assertNoFieldById('edit-submit');
}
}
......@@ -4,6 +4,7 @@
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Archiver\ArchiveTar;
use Drupal\Core\Installer\Form\SelectProfileForm;
/**
* Provides a base class for testing installing from existing configuration.
......@@ -15,6 +16,11 @@ abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
*/
protected $profile = NULL;
/**
* @todo
*/
protected $existingSyncDirectory = FALSE;
/**
* {@inheritdoc}
*/
......@@ -33,14 +39,25 @@ protected function prepareEnvironment() {
'core' => \Drupal::CORE_COMPATIBILITY,
'name' => 'Configuration installation test profile (' . $this->profile . ')',
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
if ($this->existingSyncDirectory) {
$config_sync_directory = $this->siteDirectory . '/config/sync';
$this->settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
'value' => $config_sync_directory,
'required' => TRUE,
];
}
else {
// Put the sync directory inside the profile.
$config_sync_directory = $path . '/config/sync';
}
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();
......@@ -96,4 +113,19 @@ public function testConfigSync() {
$this->assertEqual($expected, $change_list);
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
if ($this->existingSyncDirectory) {
$edit = [
'profile' => SelectProfileForm::CONFIG_INSTALL_PROFILE_KEY,
];
$this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
}
else {
parent::setUpProfile();
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment