diff --git a/core/includes/config.inc b/core/includes/config.inc index 20e6bb4..bfef876 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -206,6 +206,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); @@ -226,6 +227,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); @@ -237,6 +239,65 @@ 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) { + $snapshot_storage->deleteAll(); + foreach ($source_storage->listAll() as $name) { + $snapshot_storage->write($name, $source_storage->read($name)); + } +} + +/** + * Resets configuration to the state of the last import. + * + * @param string $config_name + * Name of the config object to be restored. + * @param string $op + * The operation to be performed, must be one of 'change', 'create' or + * 'delete'. + * + * @return boolean + */ +function config_restore_from_snapshot($config_name, $op) { + $source_storage = drupal_container()->get('config.storage.snapshot'); + $target_storage = drupal_container()->get('config.storage'); + $config_changes = array( + 'create' => array(), + 'delete' => array(), + 'change' => array(), + ); + + if (!isset($config_changes[$op])) { + return FALSE; + } + $config_changes[$op] = array($config_name); + + if (!lock()->acquire('config_import')) { + return FALSE; + } + + $success = FALSE; + 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(); + $success = TRUE; + } + catch (ConfigException $e) { + watchdog_exception('config_restore_from_snapshot', $e); + } + 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 7f4b26b..ede851c 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1806,6 +1806,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/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 52af901..b2700f9 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -60,6 +60,12 @@ public function build(ContainerBuilder $container) { ->register('config.storage.staging', 'Drupal\Core\Config\FileStorage') ->addArgument(config_get_config_directory(CONFIG_STAGING_DIRECTORY)); + // Register import snapshot configuration storage. + $container + ->register('config.storage.snapshot', 'Drupal\Core\Config\DatabaseStorage') + ->addArgument(new Reference('database')) + ->addArgument('config_snapshot'); + // Register the service for the default database connection. $container->register('database', 'Drupal\Core\Database\Connection') ->setFactoryClass('Drupal\Core\Database\Database') diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php index a04a928..b0c7810 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php @@ -27,6 +27,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..c768c98 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php @@ -19,7 +19,7 @@ class ConfigImportTest extends DrupalUnitTestBase { * * @var array */ - public static $modules = array('config_test'); + public static $modules = array('system', 'config_test'); public static function getInfo() { return array( @@ -32,6 +32,8 @@ public static function getInfo() { function setUp() { parent::setUp(); + $this->installSchema('system', 'config_snapshot'); + config_install_default_config('module', 'config_test'); // Installing config_test's default configuration pollutes the global // variable being used for recording hook invocations by this test already, @@ -60,6 +62,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'); + + // Create a snapshot and verify that it matches the active configuration. + config_import_create_snapshot($storage, $snapshot); + $this->assertFalse(config_sync_get_changes($snapshot, $storage)); // Verify the default configuration values exist. $config = config($dynamic_name); @@ -86,6 +93,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 +106,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'); + + // Create a snapshot and verify that it matches the active configuration. + config_import_create_snapshot($storage, $snapshot); + $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 +151,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 +165,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'); + + // Create a snapshot and verify that it matches the active configuration. + config_import_create_snapshot($storage, $snapshot); + $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 +217,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..84781f5 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php @@ -0,0 +1,77 @@ + '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(); + $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_restore_from_snapshot($name, 'change'); + + // 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 ca5d6de..2e73a16 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1377,6 +1377,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; }