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/block/lib/Drupal/block/Tests/BlockTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockTest.php index c607973..b5047c9 100644 --- a/core/modules/block/lib/Drupal/block/Tests/BlockTest.php +++ b/core/modules/block/lib/Drupal/block/Tests/BlockTest.php @@ -36,12 +36,12 @@ function setUp() { config('system.site')->set('page.front', 'test-page')->save(); // Create Full HTML text format. - $full_html_format = array( + $full_html_format_config = array( 'format' => 'full_html', 'name' => 'Full HTML', ); - $full_html_format = (object) $full_html_format; - filter_format_save($full_html_format); + $full_html_format = entity_create('filter_format', $full_html_format_config); + $full_html_format->save(); $this->checkPermissions(array(), TRUE); // Create and log in an administrative user having access to the Full HTML 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..d4328ce 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('config_test', 'system'); 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, 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..89ca680 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php @@ -0,0 +1,79 @@ + 'Snapshot functionality', + 'description' => 'Config snapshot creation and updating.', + 'group' => 'Configuration', + ); + } + + /** + * Tests config snapshot creation and updating. + */ + function testSnapshot() { + $active = $this->container->get('config.storage'); + $staging = $this->container->get('config.storage.staging'); + $snapshot = $this->container->get('config.storage.snapshot'); + + $config_name = 'system.performance'; + $config_key = 'cache.page.max_age'; + $original_data = '0'; + $new_data = '10'; + + // Verify that we have an initial snapshot that matches the active + // configuration. + $this->assertFalse(config_sync_get_changes($snapshot, $active)); + + // Change a configuration value. + $config = config($config_name); + $config->set($config_key, $new_data)->save(); + $staging_data = $config->get(); + + // Verify the active configuration contains the saved value. + $this->assertIdentical(config($config_name)->get($config_key), $new_data); + + // Verify that the active and snapshot storage do not match. + $this->assertTrue(config_sync_get_changes($snapshot, $active)); + + // Reset data back to original value. + config_restore_from_snapshot($config_name, 'change'); + + // Verify that the active and snapshot storage match again. + $this->assertFalse(config_sync_get_changes($snapshot, $active)); + + // Change a configuration value in staging. + $staging->write($config_name, $staging_data); + + // Verify that active and snapshot match, and that staging doesn't match + // either of them. + $this->assertFalse(config_sync_get_changes($snapshot, $active)); + $this->assertTrue(config_sync_get_changes($snapshot, $staging)); + $this->assertTrue(config_sync_get_changes($staging, $active)); + + // Import changed data from staging to active. + config_import(); + + // Verify changed config was properly imported. + $this->assertIdentical(config($config_name)->get($config_key), $new_data); + + // Verify that a new snapshot was created which and that it matches + // the active config. + $this->assertFalse(config_sync_get_changes($snapshot, $active)); + } + +} diff --git a/core/modules/filter/filter.admin.inc b/core/modules/filter/filter.admin.inc index 3c01ac2..c7a891c 100644 --- a/core/modules/filter/filter.admin.inc +++ b/core/modules/filter/filter.admin.inc @@ -75,13 +75,12 @@ function filter_admin_overview($form) { * Form submission handler for filter_admin_overview(). */ function filter_admin_overview_submit($form, &$form_state) { + $filter_formats = filter_formats(); foreach ($form_state['values']['formats'] as $id => $data) { if (is_array($data) && isset($data['weight'])) { // Only update if this is a form element with weight. - db_update('filter_format') - ->fields(array('weight' => $data['weight'])) - ->condition('format', $id) - ->execute(); + $filter_formats[$id]->weight = $data['weight']; + $filter_formats[$id]->save(); } } filter_formats_reset(); @@ -113,10 +112,8 @@ function filter_admin_overview_submit($form, &$form_state) { function filter_admin_format_page($format = NULL) { if (!isset($format->name)) { drupal_set_title(t('Add text format')); - $format = (object) array( - 'format' => NULL, - 'name' => '', - ); + + $format = entity_create('filter_format', array()); } return drupal_get_form('filter_admin_format_form', $format); } @@ -187,88 +184,75 @@ function filter_admin_format_form($form, &$form_state, $format) { $form['roles']['#default_value'] = array($admin_role); } + // Filter status. + $form['filters']['status'] = array( + '#type' => 'item', + '#title' => t('Enabled filters'), + '#prefix' => '
', + '#suffix' => '
', + ); + + // Filter order (tabledrag). + $form['filters']['order'] = array( + '#type' => 'item', + '#title' => t('Filter processing order'), + '#theme' => 'filter_admin_format_filter_order', + ); + + // Filter settings. + $form['filter_settings'] = array( + '#type' => 'vertical_tabs', + '#title' => t('Filter settings'), + ); + // Retrieve available filters and load all configured filters for existing // text formats. - $filter_info = filter_get_filters(); $filters = !empty($format->format) ? filter_list_format($format->format) : array(); // Prepare filters for form sections. - foreach ($filter_info as $name => $filter) { + foreach (filter_get_filters() as $name => $filter) { // Create an empty filter object for new/unconfigured filters. if (!isset($filters[$name])) { - $filters[$name] = new stdClass(); - $filters[$name]->format = $format->format; - $filters[$name]->module = $filter['module']; - $filters[$name]->name = $name; - $filters[$name]->status = 0; - $filters[$name]->weight = $filter['weight']; - $filters[$name]->settings = array(); + $filters[$name] = $format->filterPlugins[$name]; } } $form['#filters'] = $filters; - // Filter status. - $form['filters']['status'] = array( - '#type' => 'item', - '#title' => t('Enabled filters'), - '#prefix' => '
', - '#suffix' => '
', - ); - foreach ($filter_info as $name => $filter) { + foreach ($filters as $name => $filter) { $form['filters']['status'][$name] = array( '#type' => 'checkbox', - '#title' => $filter['title'], - '#default_value' => $filters[$name]->status, + '#title' => $filter->title, + '#default_value' => $filter->status, '#parents' => array('filters', $name, 'status'), - '#description' => $filter['description'], - '#weight' => $filter['weight'], + '#description' => $filter->description, + '#weight' => $filter->weight, ); - } - // Filter order (tabledrag). - $form['filters']['order'] = array( - '#type' => 'item', - '#title' => t('Filter processing order'), - '#theme' => 'filter_admin_format_filter_order', - ); - foreach ($filter_info as $name => $filter) { $form['filters']['order'][$name]['filter'] = array( - '#markup' => $filter['title'], + '#markup' => $filter->title, ); $form['filters']['order'][$name]['weight'] = array( '#type' => 'weight', - '#title' => t('Weight for @title', array('@title' => $filter['title'])), + '#title' => t('Weight for @title', array('@title' => $filter->title)), '#title_display' => 'invisible', '#delta' => 50, - '#default_value' => $filters[$name]->weight, + '#default_value' => $filter->weight, '#parents' => array('filters', $name, 'weight'), ); - $form['filters']['order'][$name]['#weight'] = $filters[$name]->weight; - } - - // Filter settings. - $form['filter_settings'] = array( - '#type' => 'vertical_tabs', - '#title' => t('Filter settings'), - ); - - foreach ($filter_info as $name => $filter) { - if (isset($filter['settings callback'])) { - $function = $filter['settings callback']; - // Pass along stored filter settings and default settings, but also the - // format object and all filters to allow for complex implementations. - $defaults = (isset($filter['default settings']) ? $filter['default settings'] : array()); - $settings_form = $function($form, $form_state, $filters[$name], $format, $defaults, $filters); - if (!empty($settings_form)) { - $form['filters']['settings'][$name] = array( - '#type' => 'details', - '#title' => $filter['title'], - '#parents' => array('filters', $name, 'settings'), - '#weight' => $filter['weight'], - '#group' => 'filter_settings', - ); - $form['filters']['settings'][$name] += $settings_form; - } + $form['filters']['order'][$name]['#weight'] = $filter->weight; + + // Pass along stored filter settings and default settings, but also the + // format object and all filters to allow for complex implementations. + $settings_form = $filter->settings($form, $form_state, $format); + if (!empty($settings_form)) { + $form['filters']['settings'][$name] = array( + '#type' => 'details', + '#title' => $filter->title, + '#parents' => array('filters', $name, 'settings'), + '#weight' => $filter->weight, + '#group' => 'filter_settings', + ); + $form['filters']['settings'][$name] += $settings_form; } } @@ -322,9 +306,12 @@ function filter_admin_format_form_validate($form, &$form_state) { form_set_value($form['format'], $format_format, $form_state); form_set_value($form['name'], $format_name, $form_state); - $result = db_query("SELECT format FROM {filter_format} WHERE name = :name AND format <> :format", array(':name' => $format_name, ':format' => $format_format))->fetchField(); - if ($result) { - form_set_error('name', t('Text format names must be unique. A format named %name already exists.', array('%name' => $format_name))); + $filter_formats = entity_load_multiple('filter_format'); + foreach ($filter_formats as $format) { + if ($format->name == $format_name && $format->format != $format_format) { + form_set_error('name', t('Text format names must be unique. A format named %name already exists.', array('%name' => $format_name))); + break; + } } } @@ -342,14 +329,7 @@ function filter_admin_format_form_submit($form, &$form_state) { foreach ($form_state['values'] as $key => $value) { $format->$key = $value; } - $status = filter_format_save($format); - - // Save user permissions. - if ($permission = filter_permission_name($format)) { - foreach ($format->roles as $rid => $enabled) { - user_role_change_permissions($rid, array($permission => $enabled)); - } - } + $status = $format->save(); switch ($status) { case SAVED_NEW: diff --git a/core/modules/filter/filter.api.php b/core/modules/filter/filter.api.php index f11a528..8d6aa01 100644 --- a/core/modules/filter/filter.api.php +++ b/core/modules/filter/filter.api.php @@ -11,309 +11,23 @@ */ /** - * Define content filters. - * - * User submitted content is passed through a group of filters before it is - * output in HTML, in order to remove insecure or unwanted parts, correct or - * enhance the formatting, transform special keywords, etc. A group of filters - * is referred to as a "text format". Administrators can create as many text - * formats as needed. Individual filters can be enabled and configured - * differently for each text format. - * - * This hook is invoked by filter_get_filters() and allows modules to register - * input filters they provide. - * - * Filtering is a two-step process. First, the content is 'prepared' by calling - * the 'prepare callback' function for every filter. The purpose of the - * 'prepare callback' is to escape HTML-like structures. For example, imagine a - * filter which allows the user to paste entire chunks of programming code - * without requiring manual escaping of special HTML characters like < or &. If - * the programming code were left untouched, then other filters could think it - * was HTML and change it. For many filters, the prepare step is not necessary. - * - * The second step is the actual processing step. The result from passing the - * text through all the filters' prepare steps gets passed to all the filters - * again, this time with the 'process callback' function. The process callbacks - * should then actually change the content: transform URLs into hyperlinks, - * convert smileys into images, etc. - * - * For performance reasons content is only filtered once; the result is stored - * in the cache table and retrieved from the cache the next time the same piece - * of content is displayed. If a filter's output is dynamic, it can override - * the cache mechanism, but obviously this should be used with caution: having - * one filter that does not support caching in a particular text format - * disables caching for the entire format, not just for one filter. - * - * Beware of the filter cache when developing your module: it is advised to set - * your filter to 'cache' => FALSE while developing, but be sure to remove that - * setting if it's not needed, when you are no longer in development mode. - * - * @return - * An associative array of filters, whose keys are internal filter names, - * which should be unique and therefore prefixed with the name of the module. - * Each value is an associative array describing the filter, with the - * following elements (all are optional except as noted): - * - title: (required) An administrative summary of what the filter does. - * - description: Additional administrative information about the filter's - * behavior, if needed for clarification. - * - settings callback: The name of a function that returns configuration - * form elements for the filter. See hook_filter_FILTER_settings() for - * details. - * - default settings: An associative array containing default settings for - * the filter, to be applied when the filter has not been configured yet. - * - prepare callback: The name of a function that escapes the content before - * the actual filtering happens. See hook_filter_FILTER_prepare() for - * details. - * - process callback: (required) The name the function that performs the - * actual filtering. See hook_filter_FILTER_process() for details. - * - cache (default TRUE): Specifies whether the filtered text can be cached. - * Note that setting this to FALSE makes the entire text format not - * cacheable, which may have an impact on the site's overall performance. - * See filter_format_allowcache() for details. - * - tips callback: The name of a function that returns end-user-facing - * filter usage guidelines for the filter. See hook_filter_FILTER_tips() - * for details. - * - weight: A default weight for the filter in new text formats. - * - * @see filter_example.module - * @see hook_filter_info_alter() - */ -function hook_filter_info() { - $filters['filter_html'] = array( - 'title' => t('Limit allowed HTML tags'), - 'description' => t('Allows you to restrict the HTML tags the user can use. It will also remove harmful content such as JavaScript events, JavaScript URLs and CSS styles from those tags that are not removed.'), - 'process callback' => '_filter_html', - 'settings callback' => '_filter_html_settings', - 'default settings' => array( - 'allowed_html' => '