diff --git a/core/includes/config.inc b/core/includes/config.inc
index 8c2772b..429ced7 100644
--- a/core/includes/config.inc
+++ b/core/includes/config.inc
@@ -1,6 +1,14 @@
");
+ $xml_object = new SimpleXMLElement("");
config_array_to_xml($data, $xml_object);
// Pretty print the result.
@@ -240,3 +251,208 @@ function config_array_to_xml($array, &$xml_object) {
}
}
}
+
+/**
+ * Reload config from disc and write new settings to the active store.
+ */
+function config_reload() {
+ $config_changes = config_get_changes_from_disc();
+ if (empty($config_changes)) {
+ return;
+ }
+
+ $lock_attempts = 0;
+ $lock_acquired = FALSE;
+ do {
+ if (!$lock_acquired = lock_acquire(__FUNCTION__, 5)) {
+ lock_wait(__FUNCTION__, 5);
+ continue;
+ }
+
+ try {
+ config_reload_run_hooks($config_changes);
+ config_reload_save_changes($config_changes);
+ }
+ catch (ConfigException $e) {
+ watchdog_exception('config_reload', $e);
+ config_reload_run_error_hooks($config_changes);
+ lock_release(__FUNCTION__);
+ return;
+ }
+ } while ($lock_acquired === FALSE && ++$lock_attempts < 5);
+
+ if ($lock_acquired) {
+ lock_release(__FUNCTION__);
+ return TRUE;
+ }
+ else {
+ watchdog('config_reload', 'Failed to get lock while trying to reload config.');
+ return FALSE;
+ }
+}
+
+/**
+ * Writes an array of config file changes to the active store.
+ *
+ * @param $config_changes
+ * An array of changes to be written.
+ */
+function config_reload_save_changes($config_changes) {
+ foreach (array('new', 'changed', 'deleted') as $type) {
+ foreach ($config_changes[$type] as $name) {
+ if ($type == 'deleted') {
+ config($name)->delete();
+ }
+ else {
+ // Get the active store object, set the new data from file, then
+ // save, which will also update the .sig file.
+ $active_store_config = config($name);
+ $active_store_config->setData(config($name, 'Drupal\Core\Config\DrupalConfigFile')->get());
+ $active_store_config->save();
+ }
+ }
+ }
+}
+
+/**
+ * Runs hook_config_reload_validate() and hook_config_reload() implementations.
+ *
+ * @param $config_changes
+ * An array of changes to be loaded.
+ */
+function config_reload_run_hooks($config_changes) {
+ $active_config_tree = config_tree();
+ $file_config_tree = config_tree('Drupal\Core\Config\DrupalConfigFile');
+
+ foreach (config_sort_module_reload_dependencies(module_implements('config_reload_validate')) as $module) {
+ $function = $module . '_config_reload_validate';
+ $function($config_changes, $active_config_tree, $file_config_tree);
+ }
+
+ // We allow modules to signal that they would like to be rerun after all
+ // other modules by returning CONFIG_DEFER_RELOAD. Loop until there are no
+ // modules left that indicate they would like to be rerun, checking that we're
+ // not stuck rerunning the same list of modules over and over at each cycle.
+ $modules = config_sort_module_reload_dependencies(module_implements('config_reload'));
+ do {
+ $initial_module_list = $modules;
+ $modules = array();
+ foreach ($initial_module_list as $module) {
+ $function = $module . '_config_reload';
+ if ($function($config_changes, $file_config_tree, $active_config_tree) === CONFIG_DEFER_RELOAD) {
+ $modules[] = $module;
+ }
+ }
+ } while ($modules && $modules != $initial_module_list);
+
+ // If there are modules left that haven't run their reload hook, then we hit
+ // an infinite loop.
+ if ($modules) {
+ throw new ConfigException("Dependency loop detected while reloading configuration from disc:");
+ }
+}
+
+/**
+ * Runs hook_config_reload_error() implementations.
+ *
+ * During a reload run, modules may make changes that cannot be rolled back.
+ * This hook allows modules to react to an error that occurs after they have
+ * made such changes, and make sure that the state of configuration in the
+ * active store is correct.
+ *
+ * @param $config_changes
+ * An array of changes to be loaded.
+ */
+function config_reload_run_error_hooks($config_changes) {
+ $active_config_tree = config_tree();
+ $file_config_tree = config_tree('Drupal\Core\Config\DrupalConfigFile');
+
+ $modules = config_sort_module_reload_dependencies(module_implements('config_reload_error'));
+ foreach ($modules as $module) {
+ $function = $module . '_config_reload_error';
+ try {
+ $function($config_changes, $active_config_tree, $file_config_tree);
+ }
+ catch (ConfigException $e) {
+ // Just keep going, because we need to allow all modules to react even if
+ // some of them are behaving badly.
+ }
+ }
+}
+
+/**
+ * Load all the config names! From disc.
+ */
+function config_get_names_from_disc() {
+ $config_names = array();
+ foreach (glob(config_get_config_directory() . '/' . '*.xml') as $key => $file) {
+ $parts = explode('/', $file);
+ $file = array_pop($parts);
+ $name = str_replace('.xml', '', $file);
+ $config_names[] = $name;
+ }
+ return $config_names;
+}
+
+/**
+ * Returns a DrupalConfigTree object with the given storage backend.
+ *
+ * @param $storage_class
+ * A storage class.
+ * @return
+ * A DrupalConfigTree object.
+ */
+function config_tree($storage_class = NULL) {
+ if ($storage_class === NULL) {
+ $storage_class = variable_get('config_default_storage', 'Drupal\Core\Config\DrupalVerifiedStorageSQL');
+ }
+ return new DrupalConfigTree($storage_class);
+}
+
+/**
+ * Sort the given list of modules based on dependency.
+ *
+ * @param $modules
+ * A list of modules.
+ * @return
+ * The list of modules sorted by dependency.
+ */
+function config_sort_module_reload_dependencies($modules) {
+ // Get all module data so we can find find weights and sort.
+ $module_data = system_rebuild_module_data();
+
+ $sorted_modules = array();
+ foreach ($modules as $module) {
+ $sorted_modules[$module] = $module_data[$module]->sort;
+ }
+ arsort($sorted_modules);
+ return array_keys($sorted_modules);
+}
+
+/**
+ * Returns a list of changes on disc compared to the active store.
+ *
+ * @return
+ * The list of files changed on disc compared to the active store.
+ */
+function config_get_changes_from_disc() {
+ $disc_config_names = config_get_names_from_disc();
+ $active_config_names = config_get_verified_storage_names_with_prefix();
+ $config_changes = array(
+ 'new' => array_diff($disc_config_names, $active_config_names),
+ 'changed' => array(),
+ 'deleted' => array_diff($active_config_names, $disc_config_names),
+ );
+ foreach (array_intersect($disc_config_names, $active_config_names) as $name) {
+ $active_config = config($name);
+ $file_config = config($name, 'Drupal\Core\Config\DrupalConfigFile');
+ if ($active_config->get() != $file_config->get()) {
+ $config_changes['changed'][] = $name;
+ }
+ }
+ if (empty($config_changes['new']) && empty($config_changes['changed']) && empty($config_changes['deleted'])) {
+ return FALSE;
+ }
+ return $config_changes;
+}
+
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 07e25a0..941b667 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1035,7 +1035,7 @@ function install_settings_form_submit($form, &$form_state) {
$config_path = conf_path() . '/files/' . $settings['config_directory_name']['value'];
if (!file_prepare_directory($config_path, FILE_CREATE_DIRECTORY)) {
// How best to handle errors here?
- };
+ }
// Write out a .htaccess file that will protect the config directory from
// prying eyes.
diff --git a/core/includes/module.inc b/core/includes/module.inc
index df9c138..37e6e23 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -463,7 +463,7 @@ function module_enable($module_list, $enable_dependencies = TRUE) {
$versions = drupal_get_schema_versions($module);
$version = $versions ? max($versions) : SCHEMA_INSTALLED;
- // Copy any default configuration data to the system config directory/
+ // Copy any default configuration data to the active store.
config_install_default_config($module);
// If the module has no current updates, but has some that were
diff --git a/core/lib/Drupal/Core/Config/DrupalConfig.php b/core/lib/Drupal/Core/Config/DrupalConfig.php
index 54397e7..6207daa 100644
--- a/core/lib/Drupal/Core/Config/DrupalConfig.php
+++ b/core/lib/Drupal/Core/Config/DrupalConfig.php
@@ -97,6 +97,10 @@ class DrupalConfig {
}
}
+ public function setData(array $data) {
+ $this->data = $data;
+ }
+
/**
* Sets value in this config object.
*
diff --git a/core/lib/Drupal/Core/Config/DrupalConfigFile.php b/core/lib/Drupal/Core/Config/DrupalConfigFile.php
new file mode 100644
index 0000000..763ee7d
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/DrupalConfigFile.php
@@ -0,0 +1,144 @@
+name = $name;
+ }
+
+ /**
+ * Checks whether the XML configuration file already exists on disk.
+ *
+ * @return
+ * Boolean based on file's existence.
+ */
+ protected function exists() {
+ return file_exists($this->getFilePath());
+ }
+
+ /**
+ * Returns the path to the XML configuration file.
+ *
+ * @return
+ * @todo
+ */
+ public function getFilePath() {
+ return config_get_config_directory() . '/' . $this->name . '.xml';
+ }
+
+ /**
+ * Writes the contents of the configuration file to disk.
+ *
+ * @param $data
+ * The data to be written to the file.
+ *
+ * @throws
+ * Exception
+ */
+ public function write($data) {
+ if (!file_put_contents($this->getFilePath(), $data)) {
+ throw new \Exception('Failed to write configuration file: ' . $this->getFilePath());
+ }
+ }
+
+ /**
+ * Returns the contents of the configuration file.
+ *
+ * @return
+ * @todo
+ */
+ public function read() {
+ if ($this->exists()) {
+ return file_get_contents($this->getFilePath());
+ }
+ throw new \Exception('Failed to read configuration file: ' . $this->getFilePath());
+ }
+
+ /**
+ * Deletes a configuration file.
+ */
+ public function delete() {
+ return @drupal_unlink($this->getFilePath());
+ }
+
+ /**
+ * Copies the configuration data from the verified storage into a file.
+ */
+ public function copyToFile() {
+ // TODO: no-op to keep the interface happy.
+ }
+
+ /**
+ * Copies the configuration data from the file into the verified storage.
+ */
+ public function copyFromFile() {
+ // TODO: no-op to keep the interface happy.
+ }
+
+ /**
+ * Deletes the configuration data file.
+ */
+ public function deleteFile() {
+ return $this->delete();
+ }
+
+ /**
+ * Checks whether the file and the verified storage is in sync.
+ *
+ * @return
+ * TRUE if the file and the verified storage contains the same data, FALSE
+ * if not.
+ */
+ public function isOutOfSync() {
+ return FALSE;
+ }
+
+ /**
+ * Writes the configuration data into the active storage but not the file.
+ *
+ * @param $data
+ * The configuration data to write into active storage.
+ */
+ public function writeToActive($data) {
+ // TODO: no-op to keep the interface happy.
+ }
+
+ /**
+ * Writes the configuration data into the file.
+ *
+ * @param $data
+ * The configuration data to write into the file.
+ */
+ public function writeToFile($data) {
+ return $this->write($data);
+ }
+
+ /**
+ * Gets names starting with this prefix.
+ *
+ * @param $prefix
+ * The prefix of the files we are searching for.
+ *
+ * @return
+ * An array of file names under a branch.
+ *
+ * @see config_get_signed_file_storage_names_with_prefix()
+ */
+ public static function getNamesWithPrefix($prefix) {
+ return config_get_signed_file_storage_names_with_prefix($prefix);
+ }
+}
+
diff --git a/core/lib/Drupal/Core/Config/DrupalConfigTree.php b/core/lib/Drupal/Core/Config/DrupalConfigTree.php
new file mode 100644
index 0000000..0d551ac
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/DrupalConfigTree.php
@@ -0,0 +1,18 @@
+storage_class = $storage_class;
+ }
+
+ public function get($name) {
+ return config($name, $this->storage_class);
+ }
+
+}
+
diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc
new file mode 100644
index 0000000..a0a0a0b
--- /dev/null
+++ b/core/modules/config/config.admin.inc
@@ -0,0 +1,58 @@
+ $config_files) {
+ if (count($config_files) == 0) {
+ continue;
+ }
+
+ $form[$config_change_type] = array(
+ '#type' => 'fieldset',
+ '#title' => $config_change_type . '(' . count($config_files) . ')',
+ '#collapsible' => TRUE,
+ );
+
+ $rows = array();
+ foreach ($config_files as $config_file) {
+ $rows[] = array('data' => array($config_file));
+ }
+
+ $form[$config_change_type]['config_files'] = array(
+ '#markup' => theme('table', array('header' => array('filename'), 'rows' => $rows)),
+ );
+ }
+
+ $form['reload'] = array(
+ '#type' => 'submit',
+ '#value' => 'Reload config from disc',
+ );
+ }
+ else {
+ $form['no_changes'] = array(
+ '#markup' => 'There are no changes on disc to reload.'
+ );
+ }
+ return $form;
+}
+
+/**
+ * Reload config from disc form submit handler.
+ */
+function config_admin_config_reload_form_submit($form, &$form_state) {
+ if (config_reload()) {
+ drupal_set_message('Configuration successfully reloaded from disc.');
+ }
+ else {
+ drupal_set_message('There was an error reloading configuration from disc.', 'error');
+ }
+}
+
diff --git a/core/modules/config/config.api.php b/core/modules/config/config.api.php
new file mode 100644
index 0000000..0f05b84
--- /dev/null
+++ b/core/modules/config/config.api.php
@@ -0,0 +1,35 @@
+ 'Configuration reload',
+ 'description' => 'Reload configuration from disc.',
+ 'weight' => -25,
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('config_admin_config_reload_form'),
+ 'access arguments' => array('reload configuration from disc'),
+ 'file' => 'config.admin.inc',
+ );
+ return $items;
+}
+
diff --git a/core/modules/config/config.test b/core/modules/config/config.test
index 395a3a9..5db0678 100644
--- a/core/modules/config/config.test
+++ b/core/modules/config/config.test
@@ -268,3 +268,124 @@ class ConfigFileContentTestCase extends DrupalWebTestCase {
// Attempt to delete non-existing configuration.
}
}
+
+/**
+ * Tests config_reload() functionality.
+ */
+class ConfigReloadTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Reload configuration',
+ 'description' => 'Tests reloading configuration files and saving to the active store.',
+ 'group' => 'Configuration',
+ );
+ }
+
+ public function setUp() {
+ parent::setUp('config_test');
+ }
+
+ public function testDeletedConfigFile() {
+ // Check a value in the active store for config_test.
+ $config = config('config_test.system');
+ $this->assertTrue($config->get('foo') == 'bar', 'The config_test.system.xml file has been loaded into the active store.');
+ // Delete one of the config_test files.
+ $config_file_path = config_get_config_directory() . '/config_test.system.xml';
+ unlink($config_file_path);
+ // Run config_reload().
+ config_reload();
+ // Check that the value has disappeared.
+ $config = config('config_test.system');
+ $this->assertTrue($config->get('foo') === NULL, 'The config_test.system.xml file values has been removed from the active store.');
+ }
+
+ public function testNewConfigFile() {
+ // Create a new file.
+ $xml =<<
+
+ new value
+
+XML;
+ $new_config_file_path = config_get_config_directory() . '/config_test.new.xml';
+ file_put_contents($new_config_file_path, $xml);
+ // Run config_reload().
+ config_reload();
+ // Check that the value has appeared.
+ $config = config('config_test.new');
+ $this->assertTrue($config->get('add_me') == 'new value', 'A new config file was loaded into the active store by config_reload().');
+ }
+
+ public function testUpdatedConfigFile() {
+ // Update a config_test config file in-place, check that the value does not
+ // appear in the active store.
+ $xml =<<
+
+ not bar
+
+XML;
+ $config_file_path = config_get_config_directory() . '/config_test.system.xml';
+ file_put_contents($config_file_path, $xml);
+ $config = config('config_test.system');
+ $this->assertTrue($config->get('foo') == 'bar', 'The config_test.system:foo value has not changed.');
+ // Run config_reload().
+ config_reload();
+ // Check that the updated value has appeared.
+ $config = config('config_test.system');
+ $this->assertTrue($config->get('foo') == 'not bar', 'The config_test.system:foo value has been updated by the config_reload() run.');
+ }
+
+ public function testReloadHook() {
+ // Delete a file so that hook_config_reload() hooks are run.
+ $config_file_path = config_get_config_directory() . '/config_test.system.xml';
+ unlink($config_file_path);
+ // Run config_reload().
+ config_reload();
+ // Check that we get called when config_reload() is run.
+ $config_reload_hook_called = isset($GLOBALS['hook_config_reload']) && $GLOBALS['hook_config_reload'] == 'config_test_config_reload';
+ $this->assertTrue($config_reload_hook_called, "The config_test module's hook_config_reload() implementation was called.");
+ }
+
+ public function testReloadErrorHook() {
+ // Delete a file so that hook_config_reload() hooks are run.
+ $config_file_path = config_get_config_directory() . '/config_test.system.xml';
+ unlink($config_file_path);
+ // Enable the module that will blow up during a config_reload() or set a
+ // global or something.
+ $GLOBALS['config_reload_throw_error'] = TRUE;
+ // Run config_reload().
+ config_reload();
+ // Check that we get called when config_reload() is run and the explosion
+ // happens.
+ $config_reload_error_hook_called = isset($GLOBALS['hook_config_reload_error']) && $GLOBALS['hook_config_reload_error'] == 'config_test_config_reload_error';
+ $this->assertTrue($config_reload_error_hook_called, "The config_test module's hook_config_reload_error() implementation was called.");
+ }
+
+ public function testReloadValidateHook() {
+ // Delete a file so that hook_config_reload() hooks are run.
+ $config_file_path = config_get_config_directory() . '/config_test.system.xml';
+ unlink($config_file_path);
+ // Run config_reload().
+ config_reload();
+ // Check that we get called when config_reload() is run.
+ $config_reload_validate_hook_called = isset($GLOBALS['hook_config_reload_validate']) && $GLOBALS['hook_config_reload_validate'] == 'config_test_config_reload_validate';
+ $this->assertTrue($config_reload_validate_hook_called, "The config_test module's hook_config_reload_validate() implementation was called.");
+ }
+
+ public function testReloadValidateHookThrowsError() {
+ // Delete a file so that hook_config_reload() hooks are run.
+ $config_file_path = config_get_config_directory() . '/config_test.system.xml';
+ unlink($config_file_path);
+ // Enable the module that will blow up during a hook_config_reload_validate() or set a
+ // global or something.
+ $GLOBALS['config_reload_validate_throw_error'] = TRUE;
+ // Run config_reload().
+ config_reload();
+ // Check that the config reload run didn't update the active store.
+ $config = config('config_test.system');
+ $this->assertTrue($config->get('foo') == 'bar', 'The config_test.system:foo value has not changed.');
+ }
+}
+
diff --git a/core/modules/config/config_test/config/config_test.delete.xml b/core/modules/config/config_test/config/config_test.delete.xml
new file mode 100644
index 0000000..4ec808f
--- /dev/null
+++ b/core/modules/config/config_test/config/config_test.delete.xml
@@ -0,0 +1,4 @@
+
+
+ bar
+
diff --git a/core/modules/config/config_test/config/config_test.system.xml b/core/modules/config/config_test/config/config_test.system.xml
new file mode 100644
index 0000000..6ea745f
--- /dev/null
+++ b/core/modules/config/config_test/config/config_test.system.xml
@@ -0,0 +1,4 @@
+
+
+ bar
+
diff --git a/core/modules/config/config_test/config_test.info b/core/modules/config/config_test/config_test.info
new file mode 100644
index 0000000..8735450
--- /dev/null
+++ b/core/modules/config/config_test/config_test.info
@@ -0,0 +1,6 @@
+name = Configuration test module
+package = Core
+version = VERSION
+core = 8.x
+dependencies[] = config
+hidden = TRUE
diff --git a/core/modules/config/config_test/config_test.module b/core/modules/config/config_test/config_test.module
new file mode 100644
index 0000000..f14fd93
--- /dev/null
+++ b/core/modules/config/config_test/config_test.module
@@ -0,0 +1,36 @@
+get($file_name)->get();
+ $style['is_new'] = TRUE;
+ module_invoke_all('image_style_save', $style);
+ image_style_flush($style);
+ }
+ }
+ foreach ($config_changes['changed'] as $file_name) {
+ if (strpos($file_name, 'image.styles.') === 0) {
+ $style = $files_config_tree->get($file_name)->get();
+ $style['is_new'] = FALSE;
+ module_invoke_all('image_style_save', $style);
+ image_style_flush($style);
+ }
+ }
+ foreach ($config_changes['deleted'] as $file_name) {
+ if (strpos($file_name, 'image.styles.') === 0) {
+ image_style_flush($style);
+ $style['old_name'] = $style['name'];
+ $style['name'] = '';
+ module_invoke_all('image_style_delete', $style);
+ }
+ }
+}
+
+/**
* Delete an image style.
*
* @param $style
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index 978b0f4..1594d12 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -3203,3 +3203,4 @@ function system_actions_remove_orphans() {
actions_synchronize(TRUE);
drupal_goto('admin/config/system/actions/manage');
}
+
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 94fd3e0..0eb89e9 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -745,6 +745,18 @@ function system_menu() {
'file' => 'system.admin.inc',
);
+ // Config system reload.
+ $items['admin/config/config/reload'] = array(
+ 'title' => 'Configuration reload',
+ 'description' => 'Reload configuration from disc.',
+ 'position' => 'left',
+ 'weight' => -10,
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('system_admin_config_reload_form'),
+ 'access arguments' => array('reload configuration from disc'),
+ 'file' => 'system.admin.inc',
+ );
+
// Media settings.
$items['admin/config/media'] = array(
'title' => 'Media',