diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index f6c69a5..464f045 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -2480,6 +2480,12 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) { // Register the EntityManager. $container->register('plugin.manager.entity', 'Drupal\Core\Entity\EntityManager'); + + // Register import snapshot configuration storage. + $container + ->register('config.storage.snapshot', 'Drupal\Core\Config\DatabaseStorage') + ->addArgument(new Reference('database')) + ->addArgument('config_snapshot'); } return $container; } diff --git a/core/includes/config.inc b/core/includes/config.inc index 3e22534..5f3060c 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -202,6 +202,7 @@ function config_sync_changes(array $config_changes, StorageInterface $source_sto function config_import() { // Retrieve a list of differences between staging and the active configuration. $source_storage = drupal_container()->get('config.storage.staging'); + $snapshot_storage = drupal_container()->get('config.storage.snapshot'); $target_storage = drupal_container()->get('config.storage'); $config_changes = config_sync_get_changes($source_storage, $target_storage); @@ -222,6 +223,7 @@ function config_import() { try { $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage); config_sync_changes($remaining_changes, $source_storage, $target_storage); + config_import_create_snapshot($target_storage, $snapshot_storage); } catch (ConfigException $e) { watchdog_exception('config_import', $e); @@ -233,6 +235,53 @@ function config_import() { } /** + * Creates a configuration snapshot following a successful import. + * + * @param Drupal\Core\Config\StorageInterface $source_storage + * The storage to synchronize configuration from. + * @param Drupal\Core\Config\StorageInterface $target_storage + * The storage to synchronize configuration to. + */ +function config_import_create_snapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) { + foreach ($snapshot_storage->listAll() as $name) { + $snapshot_storage->delete($name); + } + foreach ($source_storage->listAll() as $name) { + $snapshot_storage->write($name, $source_storage->read($name)); + } +} + +/** + * Resets configuration to the state of the last import. + */ +function config_restore_from_snapshot() { + $source_storage = drupal_container()->get('config.storage.snapshot'); + $target_storage = drupal_container()->get('config.storage'); + $config_changes = config_sync_get_changes($source_storage, $target_storage); + if (empty($config_changes)) { + return; + } + + if (!lock_acquire('config_import')) { + return FALSE; + } + + $success = TRUE; + try { + $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage); + config_sync_changes($remaining_changes, $source_storage, $target_storage); + // Flush all caches and reset static variables after a successful import. + drupal_flush_all_caches(); + } + catch (ConfigException $e) { + watchdog_exception('config_restore_from_snapshot', $e); + $success = FALSE; + } + lock_release(__FUNCTION__); + return $success; +} + +/** * Invokes MODULE_config_import() callbacks for configuration changes. * * @param array $config_changes diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 2b117bc..a333233 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1707,6 +1707,11 @@ function install_finished(&$install_state) { // Will also trigger indexing of profile-supplied content or feeds. drupal_cron_run(); + // Save a snapshot of the intially installed configuration. + $active = drupal_container()->get('config.storage'); + $snapshot = drupal_container()->get('config.storage.snapshot'); + config_import_create_snapshot($active, $snapshot); + return $output; } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php index 31aa81c..a22d25f 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php @@ -26,6 +26,7 @@ public static function getInfo() { */ function testCRUD() { $storage = $this->container->get('config.storage'); + $snapshot = $this->container->get('config.storage.snapshot'); $name = 'config_test.crud'; $config = config($name); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php index 95089f7..ff4af24 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php @@ -7,12 +7,12 @@ namespace Drupal\config\Tests; -use Drupal\simpletest\DrupalUnitTestBase; +use Drupal\simpletest\WebTestBase; /** * Tests importing configuration from files into active configuration. */ -class ConfigImportTest extends DrupalUnitTestBase { +class ConfigImportTest extends WebTestBase { /** * Modules to enable. @@ -37,6 +37,12 @@ function setUp() { // variable being used for recording hook invocations by this test already, // so it has to be cleared out manually. unset($GLOBALS['hook_config_test']); + + // After installation we enabled config_test.module, which changed the + // active configuration. Create a new snapshot to get us back to parity. + $active = drupal_container()->get('config.storage'); + $snapshot = drupal_container()->get('config.storage.snapshot'); + config_import_create_snapshot($active, $snapshot); } /** @@ -60,6 +66,11 @@ function testDeleted() { $dynamic_name = 'config_test.dynamic.default'; $storage = $this->container->get('config.storage'); $staging = $this->container->get('config.storage.staging'); + $snapshot = $this->container->get('config.storage.snapshot'); + + // Verify that we have an initial snapshot of configuration that matches + // the active configuration. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); // Verify the default configuration values exist. $config = config($dynamic_name); @@ -86,6 +97,10 @@ function testDeleted() { // Verify that there is nothing more to import. $this->assertFalse(config_sync_get_changes($staging, $storage)); + + // Verify that a new snapshot was successfully created, and that its values + // match the active configuration. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); } /** @@ -95,6 +110,11 @@ function testNew() { $dynamic_name = 'config_test.dynamic.new'; $storage = $this->container->get('config.storage'); $staging = $this->container->get('config.storage.staging'); + $snapshot = $this->container->get('config.storage.snapshot'); + + // Verify that we have an initial snapshot of configuration that matches + // the active configuration. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); // Verify the configuration to create does not exist yet. $this->assertIdentical($storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.'); @@ -135,6 +155,10 @@ function testNew() { // Verify that there is nothing more to import. $this->assertFalse(config_sync_get_changes($staging, $storage)); + + // Verify that a new snapshot was successfully created, and that its values + // match the active configuration. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); } /** @@ -145,6 +169,11 @@ function testUpdated() { $dynamic_name = 'config_test.dynamic.default'; $storage = $this->container->get('config.storage'); $staging = $this->container->get('config.storage.staging'); + $snapshot = $this->container->get('config.storage.snapshot'); + + // Verify that we have an initial snapshot of configuration that matches + // the active configuration. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); // Verify that the configuration objects to import exist. $this->assertIdentical($storage->exists($name), TRUE, $name . ' found.'); @@ -192,6 +221,10 @@ function testUpdated() { // Verify that there is nothing more to import. $this->assertFalse(config_sync_get_changes($staging, $storage)); + + // Verify that a new snapshot was successfully created, and that its values + // match the active configuration. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); } } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php new file mode 100644 index 0000000..1db07ca --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php @@ -0,0 +1,78 @@ + 'Snapshot functionality', + 'description' => 'Config snapshot creation and updating.', + 'group' => 'Configuration', + ); + } + + /** + * Tests config snapshot creation and updating. + */ + function testSnapshot() { + $storage = $this->container->get('config.storage'); + $staging = $this->container->get('config.storage.staging'); + $snapshot = $this->container->get('config.storage.snapshot'); + $name = 'system.performance'; + $key = 'cache.page.max_age'; + $original_data = '0'; + $new_data = '10'; + + // Verify that we have an initial snapshot of configuration that matches + // the active configuration. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); + + // Write new data to the active directory. + $config = config($name); + $config->set($key, $new_data)->save(); + + // Save it for later. + $staging_data = $config->get(); + + // Verify the active configuration contains the saved value. + $this->assertIdentical(config($name)->get($key), $new_data); + + // Verify that the active and snapshot storage do not match. + $this->assertTrue(config_sync_get_changes($snapshot, $storage)); + + // Reset data back to original value. + config($name)->set($key, $original_data)->save(); + + // Verify that the active and snapshot storage match again. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); + + // Write modified data to staging. + $staging->write($name, $staging_data); + + // Verify that active and snapshot match, and that staging doesn't match + // either. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); + $this->assertTrue(config_sync_get_changes($snapshot, $staging)); + $this->assertTrue(config_sync_get_changes($staging, $storage)); + + // Import. + config_import(); + + // Verify config was imported. + $this->assertIdentical(config($name)->get($key), $new_data); + + // Verify that active and snapshot match after new config imported. + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index e414238..a77a4a6 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1454,6 +1454,26 @@ function system_schema() { ), ); + $schema['config_snapshot'] = array( + 'description' => 'Stores a snapshot of the last imported configuration.', + 'fields' => array( + 'name' => array( + 'description' => 'The identifier for the config object (the name of the file, minus the file extension).', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'data' => array( + 'description' => 'The raw data for this configuration object.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ), + ), + 'primary key' => array('name'), + ); + return $schema; }