diff --git a/core/modules/filter/config/filter.format.plain_text.yml b/core/modules/filter/config/filter.format.plain_text.yml index ff530c5..a7f72cd 100644 --- a/core/modules/filter/config/filter.format.plain_text.yml +++ b/core/modules/filter/config/filter.format.plain_text.yml @@ -1,27 +1,26 @@ +# Every site requires at least one text format as fallback format that +# - is accessible to all users. +# - is secure, using very basic formatting only. +# - may be modified by installation profiles to have other properties. format: plain_text name: 'Plain text' -cache: '1' status: '1' weight: '10' roles: - anonymous: anonymous - authenticated: authenticated - administrator: administrator + - anonymous + - authenticated +cache: '1' filters: + # Escape all HTML. filter_html_escape: module: filter - settings: { } status: '1' - weight: '0' + # Convert URLs into links. filter_url: module: filter - settings: - filter_url_length: '72' status: '1' - weight: '1' + # Convert linebreaks into paragraphs. filter_autop: module: filter - settings: { } status: '1' - weight: '2' langcode: und diff --git a/core/modules/filter/filter.admin.inc b/core/modules/filter/filter.admin.inc index fa33501..b9fb91c 100644 --- a/core/modules/filter/filter.admin.inc +++ b/core/modules/filter/filter.admin.inc @@ -77,9 +77,9 @@ function filter_admin_overview($form) { function filter_admin_overview_submit($form, &$form_state) { $filter_formats = filter_formats(); foreach ($form_state['values']['formats'] as $id => $data) { + // Only update if this is a form element with weight. if (is_array($data) && isset($data['weight'])) { - // Only update if this is a form element with weight. - $filter_formats[$id]->weight = $data['weight']; + $filter_formats[$id]->set('weight', $data['weight']); $filter_formats[$id]->save(); } } @@ -209,6 +209,10 @@ function filter_admin_format_form($form, &$form_state, $format) { '#title' => t('Enabled filters'), '#prefix' => '
', '#suffix' => '
', + // This item is used as a pure wrapping container with heading. Ignore its + // value, since 'filters' should only contain filter definitions. + // @see http://drupal.org/node/1829202 + '#input' => FALSE, ); foreach ($filter_info as $name => $filter) { $form['filters']['status'][$name] = array( @@ -226,6 +230,10 @@ function filter_admin_format_form($form, &$form_state, $format) { '#type' => 'item', '#title' => t('Filter processing order'), '#theme' => 'filter_admin_format_filter_order', + // This item is used as a pure wrapping container with heading. Ignore its + // value, since 'filters' should only contain filter definitions. + // @see http://drupal.org/node/1829202 + '#input' => FALSE, ); foreach ($filter_info as $name => $filter) { $form['filters']['order'][$name]['filter'] = array( @@ -256,8 +264,7 @@ function filter_admin_format_form($form, &$form_state, $format) { $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); + $settings_form = $function($form, $form_state, $filters[$name], $format, $filter['default settings'], $filters); if (!empty($settings_form)) { $form['filters']['settings'][$name] = array( '#type' => 'details', @@ -339,10 +346,13 @@ function filter_admin_format_form_submit($form, &$form_state) { // Remove unnecessary values. form_state_values_clean($form_state); + // Reduce roles to a simple list of enabled roles. + $form_state['values']['roles'] = array_keys(array_filter($form_state['values']['roles'])); + // Add the submitted form values to the text format, and save it. $format = $form['#format']; foreach ($form_state['values'] as $key => $value) { - $format->$key = $value; + $format->set($key, $value); } $status = $format->save(); diff --git a/core/modules/filter/filter.install b/core/modules/filter/filter.install index 6f7432a..39ef59b 100644 --- a/core/modules/filter/filter.install +++ b/core/modules/filter/filter.install @@ -3,13 +3,6 @@ /** * @file * Install, update, and uninstall functions for the Filter module. - * - * All sites require at least one text format (the fallback format) that all - * users have access to, so add it here. We initialize it as a simple, safe - * plain text format with very basic formatting, but it can be modified by - * installation profiles to have other properties. - * - * @see core/modules/filter/config/filter.format.plain_text.yml */ use Drupal\Component\Uuid\Uuid; @@ -47,28 +40,32 @@ function filter_update_8000() { */ function filter_update_8001() { $uuid = new Uuid(); - $result = db_query('SELECT * FROM {filter_format}', array(), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $filter_format) { - // Find the settings for this format. - $filters = array(); - $settings = db_query('SELECT * FROM {filter} WHERE format = :format', array(':format' => $filter_format['format']), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($settings as $setting) { - $setting['settings'] = unserialize($setting['settings']); - $filters[$setting['name']] = $setting; + $result = db_query('SELECT format, name, cache, status, weight FROM {filter_format}', array(), array('fetch' => PDO::FETCH_ASSOC)); + foreach ($result as $format) { + $id = $format['format']; + + // Generate a UUID. + $format['uuid'] = $uuid->generate(); + + // Add user roles. + $format['roles'] = array_keys(user_roles(FALSE, 'use text format ' . $format['format'])); + + // Retrieve and prepare all filters. + $filters = db_query('SELECT name, module, status, weight, settings FROM {filter} WHERE format = :format ORDER BY weight, module, name', array( + ':format' => $id, + ), array('fetch' => PDO::FETCH_ASSOC))->fetchAllAssoc('name'); + foreach ($filters as $name => &$filter) { + // The filter name is used as key only. + unset($filter['name']); + $filter['settings'] = unserialize($filter['settings']); } + $format['filters'] = $filters; // Save the config object. - $filter_format += array( - 'filters' => $filters, - 'roles' => drupal_map_assoc(array_keys(user_roles(FALSE, 'use text format ' . $filter_format['format']))), - 'uuid' => $uuid->generate(), - ); - $config = config('filter.format.' . $filter_format['format']); - foreach ($filter_format as $key => $value) { - $config->set($key, $value); - } + $config = config('filter.format.' . $id); + $config->setData($format); $config->save(); - update_config_manifest_add('filter.format', array($filter_format['format'])); + update_config_manifest_add('filter.format', array($id)); } } diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index b9c9b9d..b914dd9 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -171,56 +171,6 @@ function filter_menu() { } /** - * Implements MODULE_config_import_create(). - */ -function filter_config_import_create($name, $new_config, $old_config) { - if (strpos($name, 'filter.format.') !== 0) { - return FALSE; - } - - $filter_format = entity_create('filter_format', $new_config->get()); - $filter_format->save(); - return TRUE; -} - -/** - * Implements MODULE_config_import_change(). - */ -function filter_config_import_change($name, $new_config, $old_config) { - if (strpos($name, 'filter.format.') !== 0) { - return FALSE; - } - - list(, , $id) = explode('.', $name); - $filter_format = entity_load('filter_format', $id); - - $filter_format->original = clone $filter_format; - foreach ($old_config->get() as $property => $value) { - $filter_format->original->$property = $value; - } - - foreach ($new_config->get() as $property => $value) { - $filter_format->$property = $value; - } - - $filter_format->save(); - return TRUE; -} - -/** - * Implements MODULE_config_import_delete(). - */ -function filter_config_import_delete($name, $new_config, $old_config) { - if (strpos($name, 'filter.format.') !== 0) { - return FALSE; - } - - list(, , $id) = explode('.', $name); - entity_delete_multiple('filter_format', array($id)); - return TRUE; -} - -/** * Access callback: Checks access for disabling text formats. * * @param $format @@ -603,6 +553,7 @@ function filter_get_filters() { $info[$name] += array( 'description' => '', 'weight' => 0, + 'default settings' => array(), ); } $filters = array_merge($filters, $info); @@ -627,25 +578,6 @@ function _filter_list_cmp($a, $b) { } /** - * Sorts an array of filters by filter status, weight, module, name. - * - * @see filter_list_format() - * @see Drupal\filter\Plugin\Core\Entity\FilterFormat::save() - */ -function _filter_format_filter_cmp($a, $b) { - if ($a['status'] != $b['status']) { - return !empty($a['status']) ? -1 : 1; - } - if ($a['weight'] != $b['weight']) { - return ($a['weight'] < $b['weight']) ? -1 : 1; - } - elseif ($a['module'] != $b['module']) { - return strcmp($a['module'], $b['module']); - } - return strcmp($a['name'], $b['name']); -} - -/** * Checks if the text in a certain text format is allowed to be cached. * * This function can be used to check whether the result of the filtering @@ -722,12 +654,11 @@ function filter_list_format($format_id) { $filter['name'] = $filter_name; $filters['all'][$filter_format->format][$filter_name] = $filter; } - @uasort($filters['all'][$filter_format->format], '_filter_format_filter_cmp'); + uasort($filters['all'][$filter_format->format], 'Drupal\filter\Plugin\Core\Entity\FilterFormat::sortFilters'); + // Convert filters into objects. + // @todo Retain filters as arrays or convert to plugins. foreach ($filters['all'][$filter_format->format] as $filter_name => $filter) { - // Before Conversion to CMI, filter were objects, now they are arrays. - // Convert filters back to objects to reduce the impact of changes. - // @todo Follow-up: filters should be arrays instead of objects. - $filters['all'][$filter_format->format][$filter_name] = (object)$filter; + $filters['all'][$filter_format->format][$filter_name] = (object) $filter; } } cache()->set('filter_list_format', $filters['all']); @@ -742,11 +673,8 @@ function filter_list_format($format_id) { $filter->title = $filter_info[$name]['title']; $filter->settings = isset($filter->settings) ? $filter->settings : array(); - // Merge in default settings. - if (isset($filter_info[$name]['default settings'])) { - $filter->settings += $filter_info[$name]['default settings']; - } + $filter->settings += $filter_info[$name]['default settings']; $format_filters[$name] = $filter; } diff --git a/core/modules/filter/lib/Drupal/filter/FilterFormatStorageController.php b/core/modules/filter/lib/Drupal/filter/FilterFormatStorageController.php index 7e5bbdb..8cffc23 100644 --- a/core/modules/filter/lib/Drupal/filter/FilterFormatStorageController.php +++ b/core/modules/filter/lib/Drupal/filter/FilterFormatStorageController.php @@ -19,55 +19,69 @@ class FilterFormatStorageController extends ConfigStorageController { * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::preSave(). */ protected function preSave(EntityInterface $entity) { - $entity->name = trim($entity->name); + parent::preSave($entity); + + $entity->name = trim($entity->label()); $entity->cache = _filter_format_is_cacheable($entity); $filter_info = filter_get_filters(); foreach ($filter_info as $name => $filter) { - // If the format does not specify an explicit weight for a filter, assign - // a default weight, either defined in hook_filter_info(), or the default of - // 0 by filter_get_filters(). - if (!isset($entity->filters[$name]['weight'])) { - $entity->filters[$name]['weight'] = $filter['weight']; + // Ensure a consistent definition and order of properties for all filters + // by merging the actual filter definition into defaults. + $defaults = array( + 'module' => $filter['module'], + // The filter ID has to be temporarily injected into the properties, in + // order to sort all filters below. + // @todo Rethink filter sorting to remove dependency on filter IDs. + 'name' => $name, + // Unless explicitly enabled, all filters are disabled by default. + 'status' => 0, + // If no explicit weight was defined for a filter, assign either the + // default weight defined in hook_filter_info() or the default of 0 by + // filter_get_filters(). + 'weight' => $filter['weight'], + 'settings' => $filter['default settings'], + ); + // All available filters are saved for each format, in order to retain all + // filter properties regardless of whether a filter is currently enabled + // or not, since some filters require extensive configuration. + // @todo Do not save disabled filters whose properties are identical to + // all default properties. + if (isset($entity->filters[$name])) { + $entity->filters[$name] = array_merge($defaults, $entity->filters[$name]); } - $entity->filters[$name]['status'] = isset($entity->filters[$name]['status']) ? $entity->filters[$name]['status'] : 0; - $entity->filters[$name]['module'] = $filter['module']; - - // If settings were passed, only ensure default settings. - if (isset($entity->filters[$name]['settings'])) { - if (isset($filter['default settings'])) { - $entity->filters[$name]['settings'] = array_merge($filter['default settings'], $entity->filters[$name]['settings']); - } - } - // Otherwise, use default settings or fall back to an empty array. else { - $entity->filters[$name]['settings'] = isset($filter['default settings']) ? $filter['default settings'] : array(); + $entity->filters[$name] = $defaults; } - - // Sort filters properties by key, to minimize diff issues. - ksort($entity->filters[$name]); + // The module definition from hook_filter_info() always takes precedence + // and needs to be updated in case it changes. + $entity->filters[$name]['module'] = $filter['module']; } - // Sort filters by enabled/disabled first then by weight - @uasort($entity->filters, '_filter_format_filter_cmp'); + // Sort all filters. + uasort($entity->filters, 'Drupal\filter\Plugin\Core\Entity\FilterFormat::sortFilters'); + // Remove the 'name' property from all filters that was added above. + foreach ($entity->filters as &$filter) { + unset($filter['name']); + } } /** * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave(). */ protected function postSave(EntityInterface $entity, $update) { - if ($update == SAVED_UPDATED) { + parent::postSave($entity, $update); + + if ($update) { // Clear the filter cache whenever a text format is updated. - cache('filter')->deleteTags(array('filter_format' => $entity->format)); + cache('filter')->deleteTags(array('filter_format' => $entity->id())); } - filter_formats_reset(); - if (!empty($entity->status)) { - // Save user permissions. - if ($permission = filter_permission_name($entity)) { - foreach ($entity->roles as $rid => $enabled) { - user_role_change_permissions($rid, array($permission => $enabled)); - } + // Save user role permissions. + if ($permission = filter_permission_name($entity)) { + foreach (user_roles() as $rid => $name) { + $enabled = in_array($rid, $entity->roles); + user_role_change_permissions($rid, array($permission => $enabled)); } } } diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Core/Entity/FilterFormat.php b/core/modules/filter/lib/Drupal/filter/Plugin/Core/Entity/FilterFormat.php index 8e47dbb..2f6e6d4 100644 --- a/core/modules/filter/lib/Drupal/filter/Plugin/Core/Entity/FilterFormat.php +++ b/core/modules/filter/lib/Drupal/filter/Plugin/Core/Entity/FilterFormat.php @@ -12,11 +12,11 @@ use Drupal\Core\Annotation\Translation; /** - * Defines the Filter Format entity. + * Represents a text format. * * @Plugin( * id = "filter_format", - * label = @Translation("Filter Format"), + * label = @Translation("Text format"), * module = "filter", * controller_class = "Drupal\filter\FilterFormatStorageController", * config_prefix = "filter.format", @@ -39,7 +39,11 @@ class FilterFormat extends ConfigEntityBase { public $format; /** - * Name of the text format (Filtered HTML). + * Unique label of the text format. + * + * Since text formats impact a site's security, two formats with the same + * label but different filter configuration would impose a security risk. + * Therefore, each text format label must be unique. * * @todo Rename to $label. * @@ -55,51 +59,80 @@ class FilterFormat extends ConfigEntityBase { public $uuid; /** - * Flag to indicate whether format is cacheable. (1 = cacheable, 0 = not - * cacheable). - * - * @var int - */ - public $cache = 0; - - /** - * The status of the text format. (1 = enabled, 0 = disabled) + * Whether the text format is enabled or disabled. * - * @var int + * @var bool */ public $status = 1; /** - * Weight of text format to use when listing. + * Weight of this format in the text format selector. + * + * The first/lowest text format that is accessible for a user is used as + * default format. * * @var int */ public $weight = 0; /** - * An array of name value pairs of the roles that can use this format. + * List of user role IDs that are allowed to use this format. * * @var array */ public $roles = array(); /** - * An array of name value pairs of the enabled filters for this text format. + * Whether processed text of this format can be cached. * - * Each element of this array must contain at least the following values: - * - weight: Weight of filter within format. - * - settings: An array of name value pairs that store the filter settings - * for the specific format. + * @var bool + */ + public $cache = 0; + + /** + * Configured filters for this text format. + * + * An associative array of filters assigned to the text format, keyed by the + * ID of each filter (prefixed with module name) and using the properties: + * - module: The name of the module providing the filter. + * - status: (optional) A Boolean indicating whether the filter is + * enabled in the text format. Defaults to disabled. + * - weight: (optional) The weight of the filter in the text format. If + * omitted, the default value is determined in the following order: + * - if any, the currently stored weight is retained. + * - if any, the default weight from hook_filter_info() is taken over. + * - otherwise, a default weight of 10, which usually sorts it last. + * - settings: (optional) An array of configured settings for the filter. + * See hook_filter_info() for details. * * @var array */ public $filters = array(); /** - * Implements \Drupal\Core\Entity\EntityInterface::id(). + * Overrides \Drupal\Core\Entity\Entity::id(). */ public function id() { return $this->format; } + /** + * Helper callback for uasort() to sort filters by status, weight, module, and name. + * + * @see Drupal\filter\FilterFormatStorageController::preSave() + * @see filter_list_format() + */ + public static function sortFilters($a, $b) { + if ($a['status'] != $b['status']) { + return !empty($a['status']) ? -1 : 1; + } + if ($a['weight'] != $b['weight']) { + return ($a['weight'] < $b['weight']) ? -1 : 1; + } + if ($a['module'] != $b['module']) { + return strnatcasecmp($a['module'], $b['module']); + } + return strnatcasecmp($a['name'], $b['name']); + } + } diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterDefaultFormatTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterDefaultFormatTest.php index 02031e3..cbeecd0 100644 --- a/core/modules/filter/lib/Drupal/filter/Tests/FilterDefaultFormatTest.php +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterDefaultFormatTest.php @@ -42,16 +42,9 @@ function testDefaultTextFormats() { // Adjust the weights so that the first and second formats (in that order) // are the two lowest weighted formats available to any user. - $minimum_weight = NULL; + $minimum_weight = 0; foreach (entity_load_multiple('filter_format') as $format) { - if (is_null($minimum_weight)) { - $minimum_weight = $format->weight; - } - else { - if ($minimum_weight > $format->weight) { - $minimum_weight = $format->weight; - } - } + $minimum_weight = min($minimum_weight, $format->weight); } $edit = array(); $edit['formats[' . $first_format->format . '][weight]'] = $minimum_weight - 2; diff --git a/core/modules/php/config/filter.format.php_code.yml b/core/modules/php/config/filter.format.php_code.yml index 504b970..cee6e9b 100644 --- a/core/modules/php/config/filter.format.php_code.yml +++ b/core/modules/php/config/filter.format.php_code.yml @@ -1,11 +1,10 @@ format: php_code name: 'PHP code' -cache: '0' status: '1' weight: '11' +cache: '0' filters: php_code: module: php status: '1' - weight: '0' langcode: und diff --git a/core/modules/system/lib/Drupal/system/Tests/Module/ModuleTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Module/ModuleTestBase.php index f23204b..b7fd25c 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Module/ModuleTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Module/ModuleTestBase.php @@ -99,7 +99,7 @@ function assertModuleConfig($module) { return; } $module_file_storage = new FileStorage($module_config_dir); - $names = $module_file_storage->listAll(); + $names = $module_file_storage->listAll($module . '.'); // Verify that the config directory is not empty. $this->assertTrue($names); diff --git a/core/modules/taxonomy/taxonomy.install b/core/modules/taxonomy/taxonomy.install index 41e7d2d..64275cc 100644 --- a/core/modules/taxonomy/taxonomy.install +++ b/core/modules/taxonomy/taxonomy.install @@ -71,7 +71,7 @@ function taxonomy_schema() { 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, - 'description' => 'The filter format of the description.', + 'description' => 'The filter format ID of the description.', ), 'weight' => array( 'type' => 'int', diff --git a/core/modules/user/user.install b/core/modules/user/user.install index 6416654..d20b352 100644 --- a/core/modules/user/user.install +++ b/core/modules/user/user.install @@ -75,7 +75,7 @@ function user_schema() { 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, - 'description' => 'The Filter Format id of the signature.', + 'description' => 'The filter format ID of the signature.', ), 'created' => array( 'type' => 'int', diff --git a/core/profiles/standard/config/filter.format.filtered_html.yml b/core/profiles/standard/config/filter.format.filtered_html.yml index 3ba3ef8..11623fe 100644 --- a/core/profiles/standard/config/filter.format.filtered_html.yml +++ b/core/profiles/standard/config/filter.format.filtered_html.yml @@ -1,35 +1,22 @@ format: filtered_html name: 'Filtered HTML' -cache: '1' status: '1' weight: '0' roles: - anonymous: anonymous - authenticated: authenticated - administrator: administrator + - anonymous + - authenticated +cache: '1' filters: filter_url: module: filter - settings: - filter_url_length: '72' status: '1' - weight: '0' filter_html: module: filter - settings: - allowed_html: '
    1. ' - filter_html_help: '1' - filter_html_nofollow: '0' status: '1' - weight: '1' filter_autop: module: filter - settings: { } status: '1' - weight: '2' filter_htmlcorrector: module: filter - settings: { } status: '1' - weight: '10' langcode: und diff --git a/core/profiles/standard/config/filter.format.full_html.yml b/core/profiles/standard/config/filter.format.full_html.yml index 6fb754c..26ff727 100644 --- a/core/profiles/standard/config/filter.format.full_html.yml +++ b/core/profiles/standard/config/filter.format.full_html.yml @@ -1,27 +1,18 @@ format: full_html name: 'Full HTML' -cache: '1' status: '1' weight: '1' roles: - administrator: administrator - anonymous: '0' - authenticated: '0' + - administrator +cache: '1' filters: filter_url: module: filter - settings: - filter_url_length: '72' status: '1' - weight: '0' filter_autop: module: filter - settings: { } status: '1' - weight: '1' filter_htmlcorrector: module: filter - settings: { } status: '1' - weight: '10' langcode: und