Commit ffaabb7c authored by catch's avatar catch

Issue #1998576 by beejeebus, alexpott, swentel: Make the config import process...

Issue #1998576 by beejeebus, alexpott, swentel: Make the config import process use full config trees again.
parent d790fb7a
......@@ -22,12 +22,6 @@
* The name of the module or theme to install default configuration for.
*/
function config_install_default_config($type, $name) {
// If this module defines any ConfigEntity types then create an empty
// manifest file for each of them.
foreach (config_get_module_config_entities($name) as $entity_info) {
config('manifest.' . $entity_info['config_prefix'])->save();
}
$config_dir = drupal_get_path($type, $name) . '/config';
if (is_dir($config_dir)) {
$source_storage = new FileStorage($config_dir);
......@@ -60,12 +54,6 @@ function config_uninstall_default_config($type, $name) {
foreach ($config_names as $config_name) {
config($config_name)->delete();
}
// If this module defines any ConfigEntity types, then delete the manifest
// file for each of them.
foreach (config_get_module_config_entities($name) as $entity_type) {
config('manifest.' . $entity_info['config_prefix'])->delete();
}
}
/**
......
......@@ -1410,29 +1410,6 @@ function update_variables_to_config($config_name, array $variable_map) {
db_delete('variable')->condition('name', array_keys($variable_map), 'IN')->execute();
}
/**
* Adds entries in a configuration entity manifest file during updates.
*
* @param string $config_prefix
* The configuration entity prefix from the annotation.
* @param array $ids
* An array of configuration entities to add to the manifest.
*/
function update_config_manifest_add($config_prefix, array $ids) {
$manifest = config('manifest.' . $config_prefix);
// Add record to manifest for each config entity. Operate on the data array
// as a whole, because $manifest->get() would treat dots in ids as nesting.
$data = $manifest->get();
foreach ($ids as $id) {
$data[$id] = array('name' => $config_prefix . '.' . $id);
}
$manifest->setData($data);
// Write manifest to disk.
$manifest->save();
}
/**
* Installs a default configuration file into the active store.
*
......
......@@ -373,15 +373,6 @@ public function delete(array $entities) {
foreach ($entities as $id => $entity) {
$config = $this->configFactory->get($this->getConfigPrefix() . $entity->id());
$config->delete();
// Remove the entity from the manifest file. Entity IDs can contain a dot
// so we can not use Config::clear() to remove the entity from the
// manifest.
$manifest = $this->configFactory->get('manifest.' . $this->entityInfo['config_prefix']);
$manifest_data = $manifest->get();
unset($manifest_data[$entity->id()]);
$manifest->setData($manifest_data);
$manifest->save();
}
$this->postDelete($entities);
......@@ -452,28 +443,6 @@ public function save(EntityInterface $entity) {
$this->invokeHook('insert', $entity);
}
$update_manifest = FALSE;
$config = $this->configFactory->get('manifest.' . $this->entityInfo['config_prefix']);
$manifest = $config->get();
// If the save operation resulted in a rename remove the old entity id from
// the manifest file.
if ($id !== $entity->id()) {
// Entity IDs can contain a dot so we can not use Config::clear() to
// remove the entity from the manifest.
unset($manifest[$id]);
$update_manifest = TRUE;
}
// Add this entity to the manifest file if necessary.
if (!isset($manifest[$entity->id()])) {
$manifest[$entity->id()] = array(
'name' => $this->getConfigPrefix() . $entity->id(),
);
$update_manifest = TRUE;
}
if ($update_manifest) {
$config->setData($manifest)->save();
}
unset($entity->original);
return $return;
......
......@@ -139,13 +139,10 @@ public function addChangelistCreate() {
*/
public function addChangelistUpdate() {
foreach (array_intersect($this->getSourceNames(), $this->getTargetNames()) as $name) {
// Ignore manifest files.
if (substr($name, 0, 9) != 'manifest.') {
$source_config_data = $this->sourceStorage->read($name);
$target_config_data = $this->targetStorage->read($name);
if ($source_config_data !== $target_config_data) {
$this->addChangeList('update', array($name));
}
$source_config_data = $this->sourceStorage->read($name);
$target_config_data = $this->targetStorage->read($name);
if ($source_config_data !== $target_config_data) {
$this->addChangeList('update', array($name));
}
}
return $this;
......
<?php
/**
* @file
* Contains \Drupal\Core\Config\StorageComparerManifest.
*/
namespace Drupal\Core\Config;
/**
* Defines a config storage comparer that uses config entity manifests.
*
* Config entities maintain 'manifest' files that list the objects they are
* currently handling. Each file is a simple indexed array of config object
* names. In order to generate a list of objects that have been created or
* deleted we need to open these files in both the source and target storage,
* generate an array of the objects, and compare them.
*/
class StorageComparerManifest extends StorageComparer {
/**
* List of config entities managed by manifests in the source storage.
*
* @see \Drupal\Core\Config\StorageComparerManifest::getSourceManifestData()
*
* @var array
*/
protected $sourceManifestData = array();
/**
* List of config entities managed by manifests in the target storage.
*
* @see \Drupal\Core\Config\StorageComparerManifest::getTargetManifestData()
*
* @var array
*/
protected $targetManifestData = array();
/**
* {@inheritdoc}
*/
public function addChangelistDelete() {
foreach (array_diff_key($this->getTargetManifestData(), $this->getSourceManifestData()) as $value) {
$this->addChangeList('delete', array($value['name']));
}
return $this;
}
/**
* {@inheritdoc}
*/
public function addChangelistCreate() {
foreach (array_diff_key($this->getSourceManifestData(), $this->getTargetManifestData()) as $value) {
$this->addChangeList('create', array($value['name']));
}
return $this;
}
/**
* Gets the list of config entities from the source storage's manifest files.
*
* @return array
* The list of config entities in the source storage whose entity type has a
* manifest in the source storage.
*/
protected function getSourceManifestData() {
if (empty($this->sourceManifestData)) {
foreach ($this->getSourceStorage()->listAll('manifest') as $name) {
if ($source_manifest_data = $this->getSourceStorage()->read($name)) {
$this->sourceManifestData = array_merge($this->sourceManifestData, $source_manifest_data);
}
}
}
return $this->sourceManifestData;
}
/**
* Gets the list of config entities from the target storage's manifest files.
*
* @see \Drupal\Core\Config\ConfigImporter::getSourceManifestData()
*
* @return array
* The list of config entities in the target storage whose entity type has a
* manifest in the source storage.
*/
protected function getTargetManifestData() {
if (empty($this->targetManifestData)) {
foreach ($this->getSourceStorage()->listAll('manifest') as $name) {
if ($target_manifest_data = $this->targetStorage->read($name)) {
$this->targetManifestData = array_merge($this->targetManifestData, $target_manifest_data);
}
}
}
return $this->targetManifestData;
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->sourceManifestData = $this->targetManifestData = array();
return parent::reset();
}
}
......@@ -271,7 +271,6 @@ function block_update_8008() {
'type' => 'text_textarea_with_summary',
))
->save();
update_config_manifest_add('entity.form_display', array($form_display->get('id')));
// Initialize state for future calls.
$sandbox['last'] = 0;
......
......@@ -9,7 +9,7 @@
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\StorageComparerManifest;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\Config\StorageInterface;
/**
......@@ -27,16 +27,8 @@
*/
function config_admin_sync_form(array &$form, array &$form_state, StorageInterface $source_storage) {
$source_list = $source_storage->listAll();
if (empty($source_list)) {
$form['no_changes'] = array(
'#markup' => t('There is no configuration to import.'),
);
$form['actions']['#access'] = FALSE;
return $form;
}
$config_comparer = new StorageComparerManifest(Drupal::service('config.storage.staging'), Drupal::service('config.storage'));
if (!$config_comparer->createChangelist()->hasChanges()) {
$config_comparer = new StorageComparer(Drupal::service('config.storage.staging'), Drupal::service('config.storage'));
if (empty($source_list) || !$config_comparer->createChangelist()->hasChanges()) {
$form['no_changes'] = array(
'#markup' => t('There are no configuration changes.'),
);
......@@ -146,14 +138,6 @@ function config_admin_import_form_submit($form, &$form_state) {
$config_importer->import();
drupal_flush_all_caches();
drupal_set_message(t('The configuration was imported successfully.'));
// Once a sync completes, we empty the staging directory. This prevents
// changes from being accidentally overwritten by stray files getting
// imported later.
$source_storage = $config_importer->getStorageComparer()->getSourceStorage();
foreach ($source_storage->listAll() as $name) {
$source_storage->delete($name);
}
}
catch (ConfigException $e) {
// Return a negative result for UI purposes. We do not differentiate between
......
......@@ -36,6 +36,19 @@ function config_permission() {
return $permissions;
}
/**
* Implements hook_file_download().
*/
function config_file_download($uri) {
$scheme = file_uri_scheme($uri);
$target = file_uri_target($uri);
if ($scheme == 'temporary' && $target == 'config.tar.gz') {
return array(
'Content-disposition' => 'attachment; filename="config.tar.gz"',
);
}
}
/**
* Implements hook_menu().
*/
......@@ -48,6 +61,16 @@ function config_menu() {
'access arguments' => array('synchronize configuration'),
'file' => 'config.admin.inc',
);
$items['admin/config/development/export'] = array(
'title' => 'Configuration export',
'description' => 'Export your site configuration',
'route_name' => 'config_export',
);
$items['admin/config/development/import'] = array(
'title' => 'Configuration import',
'description' => 'Import configuration for your site',
'route_name' => 'config_import',
);
$items['admin/config/development/sync/diff/%'] = array(
'title' => 'Configuration file diff',
'description' => 'Diff between active and staged configuraiton.',
......
......@@ -4,3 +4,21 @@ config_diff:
_content: '\Drupal\config\Controller\ConfigController::diff'
requirements:
_permission: 'synchronize configuration'
config_export_download:
pattern: '/admin/config/development/export-download'
defaults:
_controller: '\Drupal\config\Controller\ConfigController::downloadExport'
requirements:
_permission: 'export configuration'
config_export:
pattern: '/admin/config/development/export'
defaults:
_form: '\Drupal\config\Form\ConfigExportForm'
requirements:
_permission: 'export configuration'
config_import:
pattern: '/admin/config/development/import'
defaults:
_form: '\Drupal\config\Form\ConfigImportForm'
requirements:
_permission: 'import configuration'
......@@ -9,6 +9,7 @@
use Drupal\Core\ControllerInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Component\Archiver\ArchiveTar;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -52,6 +53,20 @@ public function __construct(StorageInterface $target_storage, StorageInterface $
$this->sourceStorage = $source_storage;
}
/**
* Downloads a tarball of the site configuration.
*/
public function downloadExport() {
$archiver = new ArchiveTar(file_directory_temp() . '/config.tar.gz', 'gz');
$config_dir = config_get_config_directory();
$config_files = array();
foreach (\Drupal::service('config.storage')->listAll() as $config_name) {
$config_files[] = $config_dir . '/' . $config_name . '.yml';
}
$archiver->createModify($config_files, '', config_get_config_directory());
return file_download('temporary', 'config.tar.gz');
}
/**
* Shows diff of specificed configuration file.
*
......
<?php
namespace Drupal\config\Form;
use Drupal\Core\Form\FormInterface;
class ConfigExportForm implements FormInterface {
public function getFormID() {
return 'config_export_form';
}
public function buildForm(array $form, array &$form_state) {
$form['#action'] = '/admin/config/development/export-download';
$form['description'] = array(
'#markup' => '<p>' . t('Use the export button below to download your site configuration.') . '</p>',
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Export'),
);
return $form;
}
public function validateForm(array &$form, array &$form_state) {
}
public function submitForm(array &$form, array &$form_state) {
}
}
<?php
namespace Drupal\config\Form;
use Drupal\Core\Form\FormInterface;
use Drupal\Component\Archiver\ArchiveTar;
class ConfigImportForm implements FormInterface {
public function getFormID() {
return 'config_import_form';
}
public function buildForm(array $form, array &$form_state) {
$form['description'] = array(
'#markup' => '<p>' . t('Use the upload button below.') . '</p>',
);
$form['import_tarball'] = array(
'#type' => 'file',
'#value' => t('Select your configuration export file'),
'#description' => t('This form will redirect you to the import configuration screen.'),
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Upload'),
);
return $form;
}
public function validateForm(array &$form, array &$form_state) {
if (!empty($_FILES['files']['error']['import_tarball'])) {
form_set_error('import_tarball', t('The import tarball could not be uploaded.'));
}
else {
$form_state['values']['import_tarball'] = $_FILES['files']['tmp_name']['import_tarball'];
}
}
public function submitForm(array &$form, array &$form_state) {
if ($path = $form_state['values']['import_tarball']) {
\Drupal::service('config.storage.staging')->deleteAll();
$archiver = new ArchiveTar($path, 'gz');
$files = array();
foreach ($archiver->listContent() as $file) {
$files[] = $file['filename'];
}
$archiver->extractList($files, config_get_config_directory(CONFIG_STAGING_DIRECTORY));
drupal_unlink($path);
drupal_set_message('Your configuration files were successfully uploaded, ready for import.');
$form_state['redirect'] = 'admin/config/development/sync';
}
}
}
......@@ -193,23 +193,6 @@ function testNameValidation() {
catch (ConfigNameException $e) {
$this->pass($message);
}
// Write configuration with an invalid name (missing a namespace) to
// staging.
$staging = $this->container->get('config.storage.staging');
$manifest_data = config('manifest.invalid_object_name')->get();
$manifest_data['new']['name'] = 'invalid';
$staging->write('manifest.invalid_object_name', $manifest_data);
// Verify that an exception is thrown when importing.
$message = 'Expected ConfigNameException was thrown when attempting to sync invalid configuration.';
try {
$this->configImporter()->import();
$this->fail($message);
}
catch (ConfigNameException $e) {
$this->pass($message);
}
}
}
......@@ -35,7 +35,6 @@ public static function getInfo() {
* Tests CRUD operations.
*/
function testCRUD() {
$manifest_name = 'manifest.config_test.dynamic';
$default_langcode = language_default()->langcode;
// Verify default properties on a newly created empty entity.
$empty = entity_create('config_test', array());
......@@ -123,10 +122,6 @@ function testCRUD() {
$this->fail('EntityMalformedException was not thrown.');
}
// Verify that the config manifest entry exists.
$manifest_data = config($manifest_name)->get();
$this->assertTrue(isset($manifest_data[$config_test->id()]), 'Configuration manifest for config_test.dynamic entities updated after an entity save.');
// Verify that the correct status is returned and properties did not change.
$this->assertIdentical($status, SAVED_NEW);
$this->assertIdentical($config_test->id(), $expected['id']);
......@@ -181,13 +176,6 @@ function testCRUD() {
// Verify that originalID points to new ID directly after renaming.
$this->assertIdentical($config_test->id(), $new_id);
$this->assertIdentical($config_test->getOriginalID(), $new_id);
// Verify that the config manifest entry exists.
$manifest_data = config($manifest_name)->get();
// Check that the old id is not in the manifest.
$this->assertFalse(isset($manifest_data[$old_id]), 'Old id removed from configuration manifest after an entity save.');
// Check that the new id is in the manifest.
$this->assertTrue(isset($manifest_data[$new_id]), 'New id added to configuration manifest after an entity save.');
}
// Test config entity prepopulation.
......
......@@ -29,6 +29,7 @@ function setUp() {
$this->web_user = $this->drupalCreateUser(array('synchronize configuration'));
$this->drupalLogin($this->web_user);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
}
/**
......@@ -40,13 +41,8 @@ function testImport() {
$storage = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
// Verify the configuration to create and update does not exist yet.
$this->assertIdentical($staging->exists($name), FALSE, $name . ' not found.');
$this->assertIdentical($staging->exists($dynamic_name), FALSE, $dynamic_name . ' not found.');
// Verify that the import UI recognises that the staging folder is empty.
$this->drupalGet('admin/config/development/sync');
$this->assertText('There is no configuration to import.');
$this->assertText('There are no configuration changes.');
$this->assertNoFieldById('edit-submit', t('Import all'));
// Create updated configuration object.
......@@ -66,15 +62,9 @@ function testImport() {
'protected_property' => '',
);
$staging->write($dynamic_name, $original_dynamic_data);
// Create manifest for new config entity.
$manifest_data = config('manifest.config_test.dynamic')->get();
$manifest_data[$original_dynamic_data['id']]['name'] = 'config_test.dynamic.' . $original_dynamic_data['id'];
$staging->write('manifest.config_test.dynamic', $manifest_data);
$this->assertIdentical($staging->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
// Verify that both appear as new.
// Verify that both appear as ready to import.
$this->drupalGet('admin/config/development/sync');
$this->assertText($name);
$this->assertText($dynamic_name);
......@@ -82,13 +72,12 @@ function testImport() {
// Import and verify that both do not appear anymore.
$this->drupalPost(NULL, array(), t('Import all'));
$this->assertUrl('admin/config/development/sync');
$this->assertNoText($name);
$this->assertNoText($dynamic_name);
$this->assertNoFieldById('edit-submit', t('Import all'));
// Verify that there are no further changes to import.
$this->assertText(t('There is no configuration to import.'));
$this->assertText(t('There are no configuration changes.'));
// Verify site name has changed.
$this->assertIdentical($new_site_name, config('system.site')->get('name'));
......@@ -118,7 +107,6 @@ function testImportLock() {
// Attempt to import configuration and verify that an error message appears.
$this->drupalPost(NULL, array(), t('Import all'));
$this->assertUrl('admin/config/development/sync');
$this->assertText(t('Another request may be synchronizing configuration already.'));
// Release the lock, just to keep testing sane.
......
......@@ -8,7 +8,8 @@
namespace Drupal\config\Tests;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\StorageComparerManifest;
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Config\StorageComparer;
use Drupal\simpletest\DrupalUnitTestBase;
/**
......@@ -50,9 +51,10 @@ function setUp() {
unset($GLOBALS['hook_config_test']);
// Set up the ConfigImporter object for testing.
$config_comparer = new StorageComparerManifest(
$config_comparer = new StorageComparer(
$this->container->get('config.storage.staging'),
$this->container->get('config.storage'));
$this->container->get('config.storage')
);
$this->configImporter = new ConfigImporter(
$config_comparer->createChangelist(),
$this->container->get('event_dispatcher'),
......@@ -60,6 +62,7 @@ function setUp() {
$this->container->get('plugin.manager.entity'),
$this->container->get('lock')
);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
}
/**
......@@ -76,6 +79,21 @@ function testNoImport() {
$this->assertFalse(isset($GLOBALS['hook_config_test']));
}
/**
* Tests that trying to import from an empty staging configuration directory
* fails.
*/
function testEmptyImportFails() {
try {
$this->container->get('config.storage.staging')->deleteAll();
$this->configImporter->reset()->import();
$this->assertFalse(FALSE, "ConfigImporterException not thrown, we didn't stop an empty import.");
}
catch (ConfigImporterException $e) {
$this->assertTrue(TRUE, 'ConfigImporterException thrown, successfully stopping an empty import.');
}
}
/**
* Tests deletion of configuration during import.
*/
......@@ -88,12 +106,13 @@ function testDeleted() {
$config = config($dynamic_name);
$this->assertIdentical($config->get('id'), 'dotted.default');
// Create an empty manifest to delete the configuration object.
$staging->write('manifest.config_test.dynamic', array());
// Delete the file from the staging directory.
$staging->delete($dynamic_name);
// Import.
$this->configImporter->reset()->import();
// Verify the values have disappeared.
// Verify the file has been removed.
$this->assertIdentical($storage->read($dynamic_name), FALSE);
$config = config($dynamic_name);
......@@ -122,8 +141,6 @@ function testNew() {
// Verify the configuration to create does not exist yet.
$this->assertIdentical($storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.');
$this->assertIdentical($staging->exists($dynamic_name), FALSE, $dynamic_name . ' not found.');
// Create new config entity.
$original_dynamic_data = array(
'id' => 'new',
......@@ -137,11 +154,6 @@ function testNew() {
);
$staging->write($dynamic_name, $original_dynamic_data);