diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php index eed4f89..04ca4c0 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -122,6 +122,13 @@ class Config { protected $typedConfigManager; /** + * The config lock service. + * + * @var \Drupal\Core\Config\ConfigLockInterface + */ + protected $configLock; + + /** * Constructs a configuration object. * * @param string $name @@ -133,14 +140,17 @@ class Config { * An event dispatcher instance to use for configuration events. * @param \Drupal\Core\Config\TypedConfigManager $typed_config * The typed configuration manager service. + * @param \Drupal\Core\Config\ConfigLockInterface $config_lock + * The config lock service. * @param \Drupal\Core\Language\Language $language * The language object used to override configuration data. */ - public function __construct($name, StorageInterface $storage, EventDispatcher $event_dispatcher, TypedConfigManager $typed_config, Language $language = NULL) { + public function __construct($name, StorageInterface $storage, EventDispatcher $event_dispatcher, TypedConfigManager $typed_config, ConfigLockInterface $config_lock, Language $language = NULL) { $this->name = $name; $this->storage = $storage; $this->eventDispatcher = $event_dispatcher; $this->typedConfigManager = $typed_config; + $this->configLock = $config_lock; $this->language = $language; } @@ -476,8 +486,12 @@ public function load() { * * @return \Drupal\Core\Config\Config * The configuration object. + * + * @throws \Drupal\Core\Config\LockedConfigException + * If the configuration can not be written due to the locking system. */ public function save() { + $this->checkLock('save'); // Validate the configuration object name before saving. static::validateName($this->name); @@ -507,6 +521,7 @@ public function save() { * The configuration object. */ public function delete() { + $this->checkLock('delete'); // @todo Consider to remove the pruning of data for Config::delete(). $this->data = array(); $this->storage->delete($this->name); @@ -517,6 +532,35 @@ public function delete() { } /** + * Checks if the configuration is write locked. + * + * @throws \Drupal\Core\Config\LockedConfigException + */ + protected function checkLock($op) { + if ($this->configLock->isLocked($this->name)) { + throw new LockedConfigException(format_string('Can not @op configuration object @name because the configuration system is locked to @lock_name.', array( + '@op' => $op, + '@name' => $this->name, + '@lock_name' => $this->configLock->getLockName(), + ))); + } + } + + /** + * Sets the configuration lock. + * + * @param \Drupal\Core\Config\ConfigLockInterface $config_lock + * The configuration lock object. + * + * @return \Drupal\Core\Config\Config + * The configuration object. + */ + public function setConfigLock(ConfigLockInterface $config_lock) { + $this->configLock = $config_lock; + return $this; + } + + /** * Retrieves the storage used to load and save this configuration object. * * @return \Drupal\Core\Config\StorageInterface diff --git a/core/lib/Drupal/Core/Config/ConfigFactory.php b/core/lib/Drupal/Core/Config/ConfigFactory.php index 8aeb301..97ab317 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactory.php +++ b/core/lib/Drupal/Core/Config/ConfigFactory.php @@ -74,6 +74,13 @@ class ConfigFactory implements EventSubscriberInterface { protected $typedConfigManager; /** + * The config lock service. + * + * @var \Drupal\Core\Config\ConfigLockInterface + */ + protected $configLock; + + /** * Constructs the Config factory. * * @param \Drupal\Core\Config\StorageInterface $storage @@ -141,7 +148,7 @@ public function get($name) { // If the configuration object does not exist in the configuration // storage or static cache create a new object and add it to the static // cache. - $this->cache[$cache_key] = new Config($name, $this->storage, $this->eventDispatcher, $this->typedConfigManager, $this->language); + $this->cache[$cache_key] = new Config($name, $this->storage, $this->eventDispatcher, $this->typedConfigManager, $this->getConfigLockService(), $this->language); if ($this->canOverride($name)) { // Get and apply any language overrides. @@ -226,7 +233,7 @@ public function loadMultiple(array $names) { } $cache_key = $this->getCacheKey($name); - $this->cache[$cache_key] = new Config($name, $this->storage, $this->eventDispatcher, $this->typedConfigManager, $this->language); + $this->cache[$cache_key] = new Config($name, $this->storage, $this->eventDispatcher, $this->typedConfigManager, $this->getConfigLockService(), $this->language); $this->cache[$cache_key]->initWithData($data); if ($this->canOverride($name)) { if (isset($language_names[$name]) && isset($storage_data[$language_names[$name]])) { @@ -301,14 +308,29 @@ public function reset($name = NULL) { * The renamed config object. */ public function rename($old_name, $new_name) { + // If either calls to isLocked() return FALSE permit the rename. + if ($this->configLock->isLocked($old_name) && $this->configLock->isLocked($new_name)) { + throw new LockedConfigException(format_string('Can not rename configuration object @name because the configuration system is locked to @lock_name.', array( + '@name' => $new_name, + '@lock_name' => $this->configLock->getLockName(), + ))); + } $this->storage->rename($old_name, $new_name); $old_cache_key = $this->getCacheKey($old_name); if (isset($this->cache[$old_cache_key])) { + // Write lock the old object so that existing references can not do anything + $config_lockdown = new ConfigLock(); + $this->cache[$old_cache_key]->setConfigLock($config_lockdown->completeLockDown()); unset($this->cache[$old_cache_key]); } + // Rename the configuration lock if necessary + if ($this->configLock->getLockName() === $old_name) { + $this->configLock->unlock()->lock($new_name); + } + $new_cache_key = $this->getCacheKey($new_name); - $this->cache[$new_cache_key] = new Config($new_name, $this->storage, $this->eventDispatcher, $this->typedConfigManager, $this->language); + $this->cache[$new_cache_key] = new Config($new_name, $this->storage, $this->eventDispatcher, $this->typedConfigManager, $this->getConfigLockService(), $this->language); $this->cache[$new_cache_key]->load(); return $this->cache[$new_cache_key]; } @@ -464,6 +486,32 @@ public function onConfigSave(ConfigEvent $event) { } /** + * Gets and creates if necessary the config lock service. + * + * @return \Drupal\Core\Config\ConfigLockInterface + * The configuration lock service. + */ + public function getConfigLockService() { + if (!isset($this->configLock)) { + $this->setConfigLock(new ConfigLock()); + } + return $this->configLock; + } + + /** + * Sets the configuration lock service. + * + * @param \Drupal\Core\Config\ConfigLockInterface $config_lock + * The configuration lock service. + * + * @return $this + */ + public function setConfigLock(ConfigLockInterface $config_lock) { + $this->configLock = $config_lock; + return $this; + } + + /** * {@inheritdoc} */ static function getSubscribedEvents() { diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 0feabdf..0da3aaf 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -260,9 +260,11 @@ public function validate() { * Writes an array of config changes from the source to the target storage. */ protected function importConfig() { + $config_lock = $this->configFactory->getConfigLockService(); foreach (array('delete', 'create', 'update') as $op) { foreach ($this->getUnprocessed($op) as $name) { - $config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + $config_lock->lock($name); + $config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager, $config_lock); if ($op == 'delete') { $config->delete(); } @@ -272,6 +274,7 @@ protected function importConfig() { $config->save(); } $this->setProcessed($op, $name); + $config_lock->unlock(); } } } @@ -287,6 +290,7 @@ protected function importConfig() { protected function importInvokeOwner() { // First pass deleted, then new, and lastly changed configuration, in order // to handle dependencies correctly. + $config_lock = $this->configFactory->getConfigLockService(); foreach (array('delete', 'create', 'update') as $op) { foreach ($this->getUnprocessed($op) as $name) { // Call to the configuration entity's storage controller to handle the @@ -295,17 +299,19 @@ protected function importInvokeOwner() { // Validate the configuration object name before importing it. // Config::validateName($name); if ($entity_type = config_get_entity_type_by_name($name)) { - $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + $config_lock->lock($name); + $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager, $config_lock); $old_config->load(); $data = $this->storageComparer->getSourceStorage()->read($name); - $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager, $config_lock); if ($data !== FALSE) { $new_config->setData($data); } $method = 'import' . ucfirst($op); $handled_by_module = $this->entityManager->getStorageController($entity_type)->$method($name, $new_config, $old_config); + $config_lock->unlock(); } if (!empty($handled_by_module)) { $this->setProcessed($op, $name); diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index dc4f3b9..4b7f624 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -107,8 +107,7 @@ public function installDefaultConfig($type, $name) { if ($this->activeStorage->exists($name)) { continue; } - - $new_config = new Config($name, $this->activeStorage, $this->eventDispatcher, $this->typedConfig); + $new_config = new Config($name, $this->activeStorage, $this->eventDispatcher, $this->typedConfig, $this->configFactory->getConfigLockService()); $data = $source_storage->read($name); if ($data !== FALSE) { $new_config->setData($data); diff --git a/core/lib/Drupal/Core/Config/ConfigLock.php b/core/lib/Drupal/Core/Config/ConfigLock.php new file mode 100644 index 0000000..5b01303 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigLock.php @@ -0,0 +1,73 @@ +completeLockDown) { + return TRUE; + } + if ($this->configNameLock !== NULL && $config_name !== $this->configNameLock){ + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function lock($config_name) { + if ($this->isLocked($config_name)) { + throw new ExistingLockConfigException(String::format('Already locked with @name.', array('@name' => $this->getLockName()))); + } + $this->configNameLock = $config_name; + return $this; + } + + /** + * {@inheritdoc} + */ + public function completeLockDown() { + $this->completeLockDown = TRUE; + return $this; + } + + /** + * {@inheritdoc} + */ + public function unlock() { + $this->completeLockDown = FALSE; + $this->configNameLock = NULL; + return $this; + } + + /** + * @return string|bool + */ + public function getLockName() { + if ($this->completeLockDown) { + return static::LOCK_DOWN_NAME; + } + if (isset($this->configNameLock)) { + return $this->configNameLock; + } + return FALSE; + } +} diff --git a/core/lib/Drupal/Core/Config/ConfigLockInterface.php b/core/lib/Drupal/Core/Config/ConfigLockInterface.php new file mode 100644 index 0000000..3178662 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigLockInterface.php @@ -0,0 +1,61 @@ +getConfigLockService(); + $config_lock->lock('system.site'); + $message = 'Expected configuration lock exception thrown.'; + try { + $config->save(); + $this->fail($message); + } + catch (LockedConfigException $e) { + $this->pass($message); + $this->assertTRUE(strpos($e->getMessage(), 'system.site'), 'Exception message contains configuration lock name.'); + $this->assertTRUE(strpos($e->getMessage(), $name), 'Exception message contains configuration name.'); + } + + $config_lock->unlock()->lock($name); + $message = 'Write locked configuration saved.'; + try { + $config->save(); + $this->pass($message); + } + catch (LockedConfigException $e) { + $this->fail($message); + } + + $config_lock->unlock(); + $message = 'Configuration lock exception not thrown.'; + try { + $config->save(); + $this->pass($message); + } + catch (LockedConfigException $e) { + $this->fail($message); + } + + $config_lock->completeLockDown(); + $message = 'Expected configuration lock exception thrown.'; + try { + $config->save(); + $this->fail($message); + } + catch (LockedConfigException $e) { + $this->pass($message); + $this->assertTRUE(strpos($e->getMessage(), 'all configuration'), 'Exception message contains configuration lock name.'); + $this->assertTRUE(strpos($e->getMessage(), $name), 'Exception message contains configuration name.'); + } + + $config_lock->unlock(); + $message = 'Expected configuration lock exception not thrown.'; + try { + $config->save(); + $this->pass($message); + } + catch (LockedConfigException $e) { + $this->fail($message); + } + + $config_lock->completeLockDown(); + $message = 'Expected configuration lock exception thrown.'; + try { + $config->delete(); + $this->fail($message); + } + catch (LockedConfigException $e) { + $this->pass($message); + } + + $new_name = 'config_test.test_locking_rename'; + try { + \Drupal::configFactory()->rename($name, $new_name); + $this->fail($message); + } + catch (LockedConfigException $e) { + $this->pass($message); + } + + $config_lock->unlock()->lock($name); + $message = 'Successfully renamed configuration.'; + try { + $renamed_config = \Drupal::configFactory()->rename($name, $new_name); + $this->pass($message); + // Check that the lock name has been updated. + $this->assertIdentical($config_lock->getLockName(), $new_name); + } + catch (LockedConfigException $e) { + $this->fail($message); + } + + $message = 'Successfully deleted configuration.'; + try { + $renamed_config->delete(); + $this->pass($message); + } + catch (LockedConfigException $e) { + $this->fail($message); + } + + $config_lock->unlock(); + + // The original configuration object should still be locked down. + try { + $config->save(); + $this->fail($message); + } + catch (LockedConfigException $e) { + $this->pass($message); + } + } + + /** * Tests the validation of configuration object names. */ function testNameValidation() { diff --git a/core/tests/Drupal/Tests/Core/Config/ConfigLockTest.php b/core/tests/Drupal/Tests/Core/Config/ConfigLockTest.php new file mode 100644 index 0000000..6f2ac2d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Config/ConfigLockTest.php @@ -0,0 +1,76 @@ + 'ConfigLock test', + 'description' => 'Tests the ConfigLock class.', + 'group' => 'Configuration' + ); + } + + public function testConfigLock() { + $config_lock = new ConfigLock(); + + // Nothing is locked. + $this->assertFalse($config_lock->isLocked('system.site')); + $this->assertFalse($config_lock->isLocked('system.module')); + + // Lock so that isLocked would return FALSE for system.site but TRUE for any + // other config name. + $config_lock->lock('system.site'); + $this->assertFalse($config_lock->isLocked('system.site')); + $this->assertTrue($config_lock->isLocked('system.module')); + $this->assertEquals('system.site', $config_lock->getLockName()); + + // Enable a complete lock down so even system.site is locked. + $config_lock->completeLockDown(); + $this->assertTrue($config_lock->isLocked('system.site')); + $this->assertTrue($config_lock->isLocked('system.module')); + $this->assertEquals('all configuration', $config_lock->getLockName()); + + // Release the locks and confirm that everything is reset. + $config_lock->unlock(); + $this->assertFalse($config_lock->isLocked('system.site')); + $this->assertFalse($config_lock->isLocked('system.module')); + $this->assertFalse($config_lock->getLockName()); + + // Enable a complete lock down without locking on a name. + $config_lock->completeLockDown(); + $this->assertTrue($config_lock->isLocked('system.site')); + $this->assertTrue($config_lock->isLocked('system.module')); + + // Test changing lock name and chaining methods. + $config_lock + ->unlock() + ->lock('system.site') + ->completeLockDown() + ->unlock() + ->lock('system.module') + ->unlock(); + } + + /** + * Tests calling lock twice with different names. + * + * @expectedException \Drupal\Core\Config\ExistingLockConfigException + */ + public function testExistingLockConfigException() { + // Test changing the configuration name so that that isLocked would return + // TRUE for system.site but FALSE for system.module. + $config_lock = new ConfigLock(); + $config_lock->lock('system.module'); + $config_lock->lock('system.site'); + } +} \ No newline at end of file