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() {
// Change the allowed HTML tags; the "format_tags" setting for CKEditor
// should automatically be updated as well.
$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();
$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.');
......
......@@ -15,12 +15,19 @@ filters:
filter_html_escape:
module: filter
status: '1'
weight: '-10'
settings: { }
# Convert URLs into links.
filter_url:
module: filter
status: '1'
weight: '0'
settings:
filter_url_length: '72'
# Convert linebreaks into paragraphs.
filter_autop:
module: filter
status: '1'
weight: '0'
settings: { }
langcode: und
......@@ -197,25 +197,9 @@ function filter_admin_format_form($form, &$form_state, $format) {
$form['roles']['#default_value'] = array($admin_role);
}
// 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) {
// 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;
// Create filter plugin instances for all available filters, including both
// enabled/configured ones as well as new and not yet unconfigured ones.
$filters = $format->filters()->sort();
// Filter status.
$form['filters']['status'] = array(
......@@ -228,17 +212,6 @@ function filter_admin_format_form($form, &$form_state, $format) {
// @see http://drupal.org/node/1829202
'#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).
$form['filters']['order'] = array(
'#type' => 'table',
......@@ -252,48 +225,54 @@ function filter_admin_format_form($form, &$form_state, $format) {
'#input' => FALSE,
'#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]['#weight'] = $filters[$name]->weight;
$form['filters']['order'][$name]['#weight'] = $filter->weight;
$form['filters']['order'][$name]['filter'] = array(
'#markup' => $filter['title'],
'#markup' => $filter->getLabel(),
);
$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->getLabel())),
'#title_display' => 'invisible',
'#delta' => 50,
'#default_value' => $filters[$name]->weight,
'#default_value' => $filter->weight,
'#parents' => array('filters', $name, '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.
$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.
$settings_form = $function($form, $form_state, $filters[$name], $format, $filter['default settings'], $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;
}
// Retrieve the settings form of the filter plugin. The plugin should not be
// aware of the text format. Therefore, it only receives a set of minimal
// base properties to allow advanced implementations to work.
$settings_form = array(
'#parents' => array('filters', $name, 'settings'),
'#tree' => TRUE,
);
$settings_form = $filter->settingsForm($settings_form, $form_state);
if (!empty($settings_form)) {
$form['filters']['settings'][$name] = array(
'#type' => 'details',
'#title' => $filter->getLabel(),
'#weight' => $filter->weight,
'#parents' => array('filters', $name, 'settings'),
'#group' => 'filter_settings',
);
$form['filters']['settings'][$name] += $settings_form;
}
}
......@@ -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.
$format = $form['#format'];
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();
......
This diff is collapsed.
This diff is collapsed.
......@@ -10,3 +10,6 @@ services:
class: Drupal\filter\Access\FormatDisableCheck
tags:
- { 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 @@
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()
* @see filter_list_format()
* @param string $instance_id
* (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) {
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) {
// 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.
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']);
// @todo Do not save disabled filters whose properties are identical to
// all default properties.
// Determine whether the format can be cached.
// @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 @@
use Drupal\Core\Entity\Annotation\EntityType;
use Drupal\Core\Annotation\Translation;
use Drupal\filter\FilterFormatInterface;
use Drupal\filter\FilterBag;
/**
* Represents a text format.
......@@ -98,24 +99,30 @@ class FilterFormat extends ConfigEntityBase implements FilterFormatInterface {
* 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:
* 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.
* - 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.
* enabled in the text format. Defaults to FALSE.
* - weight: (optional) The weight of the filter in the text format. Defaults
* to 0.
* - settings: (optional) An array of configured settings for the filter.
* See hook_filter_info() for details.
*
* Use FilterFormat::filters() to access the actual filters.
*
* @var array
*/
public $filters = array();
protected $filters = array();
/**
* Holds the collection of filters that are attached to this format.
*
* @var \Drupal\filter\FilterBag
*/
protected $filterBag;
/**
* Overrides \Drupal\Core\Entity\Entity::id().
* {@inheritdoc}
*/
public function id() {
return $this->format;
......@@ -124,21 +131,40 @@ public function id() {
/**
* {@inheritdoc}
*/
public static function sortFilters($a, $b) {
if ($a['status'] != $b['status']) {
return !empty($a['status']) ? -1 : 1;
public function filters($instance_id = NULL) {
if (!isset($this->filterBag)) {
$this->filterBag = new FilterBag(\Drupal::service('plugin.manager.filter'), $this->filters);
}
if ($a['weight'] != $b['weight']) {
return ($a['weight'] < $b['weight']) ? -1 : 1;
if (isset($instance_id)) {
return $this->filterBag->get($instance_id);
}
if ($a['module'] != $b['module']) {
return strnatcasecmp($a['module'], $b['module']);
return $this->filterBag;
}
/**
* {@inheritdoc}
*/
public function setFilterConfig($instance_id, array $configuration) {
$this->filters[$instance_id] = $configuration;
if (isset($this->filterBag)) {
$this->filterBag->setConfig($instance_id, $configuration);
}
return strnatcasecmp($a['name'], $b['name']);
return $this;
}
/**
* Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::disable().
* {@inheritdoc}
*/
public function getExportProperties() {
$properties = parent::getExportProperties();
// Sort and export the configuration of all filters.
$properties['filters'] = $this->filters()->sort()->export();
return $properties;
}
/**
* {@inheritdoc}
*/
public function disable() {
parent::disable();
......
<?php
/**
* @file
* Contains \Drupal\filter\Plugin\Filter\FilterAutoP.
*/
namespace Drupal\filter\Plugin\Filter;
use Drupal\filter\Annotation\Filter;
use Drupal\Core\Annotation\Translation;
use Drupal\filter\Plugin\FilterBase;
/**
* Provides a filter to conver line breaks to HTML.
*
* @Filter(
* id = "filter_autop",
* module = "filter",
* title = @Translation("Convert line breaks into HTML (i.e. <code>&lt;br&gt;</code> and <code>&lt;p&gt;</code>)"),
* type = FILTER_TYPE_MARKUP_LANGUAGE
* )
*/
class FilterAutoP extends FilterBase {
/**
* {@inheritdoc}
*/
public function process($text, $langcode, $cache, $cache_id) {
return _filter_autop($text);
}
/**
* {@inheritdoc}
*/
public function tips($long = FALSE) {
if ($long) {
return t('Lines and paragraphs are automatically recognized. The &lt;br /&gt; line break, &lt;p&gt; paragraph and &lt;/p&gt; close paragraph tags are inserted automatically. If paragraphs are not recognized simply add a couple blank lines.');
}
else {
return t('Lines and paragraphs break automatically.');
}
}
}
<?php
/**
* @file
* Contains \Drupal\filter\Plugin\Filter\FilterHtml.
*/