Commit 9f57b6fa authored by alexpott's avatar alexpott

Issue #1868772 by tim.plunkett, sun, heyrocker: Convert filters to plugins.

parent 523d64a4
...@@ -107,7 +107,7 @@ function testGetJSSettings() { ...@@ -107,7 +107,7 @@ function testGetJSSettings() {
// Change the allowed HTML tags; the "format_tags" setting for CKEditor // Change the allowed HTML tags; the "format_tags" setting for CKEditor
// should automatically be updated as well. // should automatically be updated as well.
$format = entity_load('filter_format', 'filtered_html'); $format = entity_load('filter_format', 'filtered_html');
$format->filters['filter_html']['settings']['allowed_html'] .= '<pre> <h3>'; $format->filters('filter_html')->settings['allowed_html'] .= '<pre> <h3>';
$format->save(); $format->save();
$expected_config['format_tags'] = 'p;h3;h4;h5;h6;pre'; $expected_config['format_tags'] = 'p;h3;h4;h5;h6;pre';
$this->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.'); $this->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
......
...@@ -15,12 +15,19 @@ filters: ...@@ -15,12 +15,19 @@ filters:
filter_html_escape: filter_html_escape:
module: filter module: filter
status: '1' status: '1'
weight: '-10'
settings: { }
# Convert URLs into links. # Convert URLs into links.
filter_url: filter_url:
module: filter module: filter
status: '1' status: '1'
weight: '0'
settings:
filter_url_length: '72'
# Convert linebreaks into paragraphs. # Convert linebreaks into paragraphs.
filter_autop: filter_autop:
module: filter module: filter
status: '1' status: '1'
weight: '0'
settings: { }
langcode: und langcode: und
...@@ -197,25 +197,9 @@ function filter_admin_format_form($form, &$form_state, $format) { ...@@ -197,25 +197,9 @@ function filter_admin_format_form($form, &$form_state, $format) {
$form['roles']['#default_value'] = array($admin_role); $form['roles']['#default_value'] = array($admin_role);
} }
// Retrieve available filters and load all configured filters for existing // Create filter plugin instances for all available filters, including both
// text formats. // enabled/configured ones as well as new and not yet unconfigured ones.
$filter_info = filter_get_filters(); $filters = $format->filters()->sort();
$filters = !empty($format->format) ? filter_list_format($format->format) : array();
// Prepare filters for form sections.
foreach ($filter_info 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();
}
}
$form['#filters'] = $filters;
// Filter status. // Filter status.
$form['filters']['status'] = array( $form['filters']['status'] = array(
...@@ -228,17 +212,6 @@ function filter_admin_format_form($form, &$form_state, $format) { ...@@ -228,17 +212,6 @@ function filter_admin_format_form($form, &$form_state, $format) {
// @see http://drupal.org/node/1829202 // @see http://drupal.org/node/1829202
'#input' => FALSE, '#input' => FALSE,
); );
foreach ($filter_info as $name => $filter) {
$form['filters']['status'][$name] = array(
'#type' => 'checkbox',
'#title' => $filter['title'],
'#default_value' => $filters[$name]->status,
'#parents' => array('filters', $name, 'status'),
'#description' => $filter['description'],
'#weight' => $filter['weight'],
);
}
// Filter order (tabledrag). // Filter order (tabledrag).
$form['filters']['order'] = array( $form['filters']['order'] = array(
'#type' => 'table', '#type' => 'table',
...@@ -252,48 +225,54 @@ function filter_admin_format_form($form, &$form_state, $format) { ...@@ -252,48 +225,54 @@ function filter_admin_format_form($form, &$form_state, $format) {
'#input' => FALSE, '#input' => FALSE,
'#theme_wrappers' => array('form_element'), '#theme_wrappers' => array('form_element'),
); );
foreach ($filter_info as $name => $filter) { // Filter settings.
$form['filter_settings'] = array(
'#type' => 'vertical_tabs',
'#title' => t('Filter settings'),
);
foreach ($filters as $name => $filter) {
$form['filters']['status'][$name] = array(
'#type' => 'checkbox',
'#title' => $filter->getLabel(),
'#default_value' => $filter->status,
'#parents' => array('filters', $name, 'status'),
'#description' => $filter->getDescription(),
'#weight' => $filter->weight,
);
$form['filters']['order'][$name]['#attributes']['class'][] = 'draggable'; $form['filters']['order'][$name]['#attributes']['class'][] = 'draggable';
$form['filters']['order'][$name]['#weight'] = $filters[$name]->weight; $form['filters']['order'][$name]['#weight'] = $filter->weight;
$form['filters']['order'][$name]['filter'] = array( $form['filters']['order'][$name]['filter'] = array(
'#markup' => $filter['title'], '#markup' => $filter->getLabel(),
); );
$form['filters']['order'][$name]['weight'] = array( $form['filters']['order'][$name]['weight'] = array(
'#type' => 'weight', '#type' => 'weight',
'#title' => t('Weight for @title', array('@title' => $filter['title'])), '#title' => t('Weight for @title', array('@title' => $filter->getLabel())),
'#title_display' => 'invisible', '#title_display' => 'invisible',
'#delta' => 50, '#delta' => 50,
'#default_value' => $filters[$name]->weight, '#default_value' => $filter->weight,
'#parents' => array('filters', $name, 'weight'), '#parents' => array('filters', $name, 'weight'),
'#attributes' => array('class' => array('filter-order-weight')), '#attributes' => array('class' => array('filter-order-weight')),
); );
}
// Make sure filters are in the correct order, since filter_get_filters()
// doesn't return sorted filters.
uasort($form['filters']['order'], 'element_sort');
// Filter settings. // Retrieve the settings form of the filter plugin. The plugin should not be
$form['filter_settings'] = array( // aware of the text format. Therefore, it only receives a set of minimal
'#type' => 'vertical_tabs', // base properties to allow advanced implementations to work.
'#title' => t('Filter settings'), $settings_form = array(
); '#parents' => array('filters', $name, 'settings'),
'#tree' => TRUE,
foreach ($filter_info as $name => $filter) { );
if (isset($filter['settings callback'])) { $settings_form = $filter->settingsForm($settings_form, $form_state);
$function = $filter['settings callback']; if (!empty($settings_form)) {
// Pass along stored filter settings and default settings, but also the $form['filters']['settings'][$name] = array(
// format object and all filters to allow for complex implementations. '#type' => 'details',
$settings_form = $function($form, $form_state, $filters[$name], $format, $filter['default settings'], $filters); '#title' => $filter->getLabel(),
if (!empty($settings_form)) { '#weight' => $filter->weight,
$form['filters']['settings'][$name] = array( '#parents' => array('filters', $name, 'settings'),
'#type' => 'details', '#group' => 'filter_settings',
'#title' => $filter['title'], );
'#parents' => array('filters', $name, 'settings'), $form['filters']['settings'][$name] += $settings_form;
'#weight' => $filter['weight'],
'#group' => 'filter_settings',
);
$form['filters']['settings'][$name] += $settings_form;
}
} }
} }
...@@ -337,7 +316,14 @@ function filter_admin_format_form_submit($form, &$form_state) { ...@@ -337,7 +316,14 @@ function filter_admin_format_form_submit($form, &$form_state) {
// Add the submitted form values to the text format, and save it. // Add the submitted form values to the text format, and save it.
$format = $form['#format']; $format = $form['#format'];
foreach ($form_state['values'] as $key => $value) { foreach ($form_state['values'] as $key => $value) {
$format->set($key, $value); if ($key != 'filters') {
$format->set($key, $value);
}
else {
foreach ($value as $instance_id => $config) {
$format->setFilterConfig($instance_id, $config);
}
}
} }
$status = $format->save(); $status = $format->save();
......
This diff is collapsed.
This diff is collapsed.
...@@ -10,3 +10,6 @@ services: ...@@ -10,3 +10,6 @@ services:
class: Drupal\filter\Access\FormatDisableCheck class: Drupal\filter\Access\FormatDisableCheck
tags: tags:
- { name: access_check } - { name: access_check }
plugin.manager.filter:
class: Drupal\filter\FilterPluginManager
arguments: ['@container.namespaces']
<?php
/**
* @file
* Contains \Drupal\filter\Annotation\Filter.
*/
namespace Drupal\filter\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an filter annotation object.
*
* @Annotation
*/
class Filter extends Plugin {
public $title;
public $description = '';
public $weight = 0;
public $status = FALSE;
public $cache = TRUE;
public $settings = array();
}
<?php
/**
* @file
* Contains \Drupal\filter\FilterBag.
*/
namespace Drupal\filter;
use Drupal\Component\Plugin\PluginBag;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\NestedArray;
/**
* A collection of filters.
*/
class FilterBag extends PluginBag {
/**
* The initial configuration for each filter in the bag.
*
* @var array
* An associative array containing the initial configuration for each filter
* in the bag, keyed by plugin instance ID.
*/
protected $configurations = array();
/**
* The manager used to instantiate the plugins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $manager;
/**
* All possible filter plugin IDs.
*
* @var array
*/
protected $definitions;
/**
* Constructs a FilterBag object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The manager to be used for instantiating plugins.
* @param array $configurations
* (optional) An associative array containing the initial configuration for
* each filter in the bag, keyed by plugin instance ID.
*/
public function __construct(PluginManagerInterface $manager, array $configurations = array()) {
$this->manager = $manager;
$this->configurations = $configurations;
if (!empty($configurations)) {
$this->instanceIDs = array_combine(array_keys($configurations), array_keys($configurations));
}
}
/**
* Retrieves filter definitions and creates an instance for each filter.
*
* This is exclusively used for the text format administration page, on which
* all available filter plugins are exposed, regardless of whether the current
* text format has an active instance.
*
* @todo Refactor text format administration to actually construct/create and
* destruct/remove actual filter plugin instances, using a library approach
* à la blocks.
*/
public function getAll() {
// Retrieve all available filter plugin definitions.
if (!$this->definitions) {
$this->definitions = $this->manager->getDefinitions();
}
// Ensure that there is an instance of all available filters.
// Note that getDefinitions() are keyed by $plugin_id. $instance_id is the
// $plugin_id for filters, since a single filter plugin can only exist once
// in a format.
foreach ($this->definitions as $plugin_id => $definition) {
$this->initializePlugin($plugin_id);
}
return $this->pluginInstances;
}
/**
* Updates the configuration for a filter plugin instance.
*
* If there is no plugin instance yet, a new will be instantiated. Otherwise,
* the existing instance is updated with the new configuration.
*
* @param string $instance_id
* The ID of a filter plugin to set the configuration for.
* @param array $configuration
* The filter plugin configuration to set.
*/
public function setConfig($instance_id, array $configuration) {
$this->configurations[$instance_id] = $configuration;
$this->get($instance_id)->setPluginConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
protected function initializePlugin($instance_id) {
// If the filter was initialized before, just return.
if (isset($this->pluginInstances[$instance_id])) {
return;
}
// Filters have a 1:1 relationship to text formats and can be added and
// instantiated at any time.
$definition = $this->manager->getDefinition($instance_id);
if (isset($definition)) {
$this->addInstanceID($instance_id);
// $configuration is the whole filter plugin instance configuration, as
// contained in the text format configuration. The default configuration
// is the filter plugin definition.
// @todo Configuration should not be contained in definitions. Move into a
// FilterBase::init() method.
$configuration = $definition;
// Merge the actual configuration into the default configuration.
if (isset($this->configurations[$instance_id])) {
$configuration = NestedArray::mergeDeep($configuration, $this->configurations[$instance_id]);
}
$this->pluginInstances[$instance_id] = $this->manager->createInstance($instance_id, $configuration, $this);
}
else {
throw new PluginException(format_string("Unknown filter plugin ID '@filter'.", array('@filter' => $instance_id)));
}
}
/**
* Sorts all filter plugin instances in this bag.
*
* @return \Drupal\filter\FilterBag
*/
public function sort() {
$this->getAll();
uasort($this->instanceIDs, array($this, 'sortHelper'));
return $this;
}
/**
* uasort() callback to sort filters by status, weight, module, and name.
*
* @see \Drupal\filter\FilterFormatStorageController::preSave()
*/
public function sortHelper($aID, $bID) {
$a = $this->get($aID);
$b = $this->get($bID);
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->getPluginId(), $b->getPluginId());
}
/**
* Returns the current configuration of all filters in this bag.
*
* @return array
* An associative array keyed by filter plugin instance ID, whose values
* are filter configurations.
*
* @see \Drupal\filter\Plugin\Filter\FilterInterface::export()
*/
public function export() {
$filters = array();
$this->rewind();
foreach ($this as $instance_id => $instance) {
$filters[$instance_id] = $instance->export();
}
return $filters;
}
}
...@@ -15,11 +15,28 @@ ...@@ -15,11 +15,28 @@
interface FilterFormatInterface extends ConfigEntityInterface { interface FilterFormatInterface extends ConfigEntityInterface {
/** /**
* Helper callback for uasort() to sort filters by status, weight, module, and name. * Returns the collection of filter pugin instances or an individual plugin instance.
* *
* @see Drupal\filter\FilterFormatStorageController::preSave() * @param string $instance_id
* @see filter_list_format() * (optional) The ID of a filter plugin instance to return.
*
* @return \Drupal\filter\FilterBag|\Drupal\filter\Plugin\FilterInterface
* Either the filter bag or a specific filter plugin instance.
*/
public function filters($instance_id = NULL);
/**
* Sets the configuration for a filter plugin instance.
*
* Sets or replaces the configuration of a filter plugin in $this->filters,
* and if instantianted already, also ensures that the actual filter plugin on
* the FilterBag contains the identical configuration.
*
* @param string $instance_id
* The ID of a filter plugin to set the configuration for.
* @param array $configuration
* The filter plugin configuration to set.
*/ */
public static function sortFilters($a, $b); public function setFilterConfig($instance_id, array $configuration);
} }
...@@ -22,45 +22,17 @@ protected function preSave(EntityInterface $entity) { ...@@ -22,45 +22,17 @@ protected function preSave(EntityInterface $entity) {
parent::preSave($entity); parent::preSave($entity);
$entity->name = trim($entity->label()); $entity->name = trim($entity->label());
$entity->cache = _filter_format_is_cacheable($entity);
$filter_info = filter_get_filters();
foreach ($filter_info as $name => $filter) {
// Merge the actual filter definition into the filter default definition.
$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]);
}
else {
$entity->filters[$name] = $defaults;
}
// 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 all filters. // @todo Do not save disabled filters whose properties are identical to
uasort($entity->filters, 'Drupal\filter\Plugin\Core\Entity\FilterFormat::sortFilters'); // all default properties.
// Remove the 'name' property from all filters that was added above.
foreach ($entity->filters as &$filter) { // Determine whether the format can be cached.
unset($filter['name']); // @todo This is a derived/computed definition, not configuration.
$entity->cache = TRUE;
foreach ($entity->filters() as $filter) {
if ($filter->status && !$filter->cache) {
$entity->cache = FALSE;
}
} }
} }
......
<?php
/**
* @file
* Contains \Drupal\filter\FilterPluginManager.
*/
namespace Drupal\filter;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Core\Plugin\Discovery\AlterDecorator;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\Plugin\Discovery\CacheDecorator;
/**
* Manages text processing filters.
*
* @see hook_filter_info_alter()
*/
class FilterPluginManager extends PluginManagerBase {
/**
* Constructs a FilterPluginManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
*/
public function __construct(\Traversable $namespaces) {
$annotation_namespaces = array('Drupal\filter\Annotation' => $namespaces['Drupal\filter']);
$this->discovery = new AnnotatedClassDiscovery('Filter', $namespaces, $annotation_namespaces, 'Drupal\filter\Annotation\Filter');
$this->discovery = new AlterDecorator($this->discovery, 'filter_info');
$this->discovery = new CacheDecorator($this->discovery, 'filter_plugins:' . language(LANGUAGE_TYPE_INTERFACE)->langcode, 'cache', array(
'filter_formats' => TRUE,
));
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = array(), FilterBag $filter_bag = NULL) {
$plugin_definition = $this->discovery->getDefinition($plugin_id);
$plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition);
return new $plugin_class($configuration, $plugin_id, $plugin_definition, $filter_bag);
}
}
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
use Drupal\Core\Entity\Annotation\EntityType; use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation; use Drupal\Core\Annotation\Translation;
use Drupal\filter\FilterFormatInterface; use Drupal\filter\FilterFormatInterface;
use Drupal\filter\FilterBag;
/** /**
* Represents a text format. * Represents a text format.
...@@ -98,24 +99,30 @@ class FilterFormat extends ConfigEntityBase implements FilterFormatInterface { ...@@ -98,24 +99,30 @@ class FilterFormat extends ConfigEntityBase implements FilterFormatInterface {
* Configured filters for this text format. * Configured filters for this text format.
* *
* An associative array of filters assigned to the text format, keyed by the * 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: * instance ID of each filter and using the properties:
* - plugin_id: The plugin ID of the filter plugin instance.
* - module: The name of the module providing the filter. * - module: The name of the module providing the filter.
* - status: (optional) A Boolean indicating whether the filter is