diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index 6d558fee6cd00d04bce0381fea95fcea195dc2b8..25fb2749dd145debab6975482849fe8940989a88 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -10,6 +10,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\Xss; use Drupal\Core\Cache\Cache; +use Drupal\Core\Extension\Extension; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; @@ -82,6 +83,38 @@ function filter_theme() { ); } +/** + * Implements hook_system_info_alter(). + * + * Prevents uninstallation of modules that provide filter plugins that are being + * used in a filter format. + */ +function filter_system_info_alter(&$info, Extension $file, $type) { + // It is not safe to call filter_formats() during maintenance mode. + if ($type == 'module' && !defined('MAINTENANCE_MODE')) { + // Get filter plugins supplied by this module. + $filter_plugins = array_filter(\Drupal::service('plugin.manager.filter')->getDefinitions(), function ($definition) use ($file) { + return $definition['provider'] == $file->getName(); + }); + if (!empty($filter_plugins)) { + $used_in = []; + // Find out if any filter formats have the plugin enabled. + foreach (filter_formats() as $filter_format) { + foreach ($filter_plugins as $filter_plugin) { + if ($filter_format->filters($filter_plugin['id'])->status) { + $used_in[] = $filter_format->label(); + $info['required'] = TRUE; + break; + } + } + } + if (!empty($used_in)) { + $info['explanation'] = t('Provides a filter plugin that is in use in the following filter formats: %formats', array('%formats' => implode(', ', $used_in))); + } + } + } +} + /** * Retrieves a list of enabled text formats, ordered by weight. * diff --git a/core/modules/filter/src/Entity/FilterFormat.php b/core/modules/filter/src/Entity/FilterFormat.php index 4b00af187788ee8a6851681a85cf61fcd3f8f4c5..0b7c6afee95e545bca6250c973e383d32042ef78 100644 --- a/core/modules/filter/src/Entity/FilterFormat.php +++ b/core/modules/filter/src/Entity/FilterFormat.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Entity; +use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; use Drupal\Core\Entity\EntityStorageInterface; @@ -387,4 +388,44 @@ public function getHtmlRestrictions() { } } + /** + * {@inheritdoc} + */ + public function removeFilter($instance_id) { + unset($this->filters[$instance_id]); + $this->filterCollection->removeInstanceId($instance_id); + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = FALSE; + $filters = $this->filters(); + foreach ($filters as $filter) { + // Remove disabled filters, so that this FilterFormat config entity can + // continue to exist. + if (!$filter->status && in_array($filter->provider, $dependencies['module'])) { + $this->removeFilter($filter->getPluginId()); + $changed = TRUE; + } + } + if ($changed) { + $this->save(); + } + } + + /** + * {@inheritdoc} + */ + protected function calculatePluginDependencies(PluginInspectionInterface $instance) { + // Only add dependencies for plugins that are actually configured. This is + // necessary because the filter plugin collection will return all available + // filter plugins. + // @see \Drupal\filter\FilterPluginCollection::getConfiguration() + if (isset($this->filters[$instance->getPluginId()])) { + parent::calculatePluginDependencies($instance); + } + } + } diff --git a/core/modules/filter/src/FilterFormatInterface.php b/core/modules/filter/src/FilterFormatInterface.php index b5e75ce78b928c3086ec9a807166784ff16d0aa3..b9c558b9738be0e2d675007e6afa38250bfe8166 100644 --- a/core/modules/filter/src/FilterFormatInterface.php +++ b/core/modules/filter/src/FilterFormatInterface.php @@ -84,4 +84,12 @@ public function getFilterTypes(); */ public function getHtmlRestrictions(); + /** + * Removes a filter. + * + * @param string $instance_id + * The ID of a filter plugin to be removed. + */ + public function removeFilter($instance_id); + } diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php index 588546e147fdfd134efb648d1c137ce16c068b9d..2808fc169f6b06d8d554455ec073a477279876ce 100644 --- a/core/modules/filter/src/Tests/FilterAPITest.php +++ b/core/modules/filter/src/Tests/FilterAPITest.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Tests; +use Drupal\Component\Utility\String; use Drupal\Core\Session\AnonymousUserSession; use Drupal\Core\TypedData\OptionsProviderInterface; use Drupal\Core\TypedData\DataDefinition; @@ -386,4 +387,68 @@ public function assertFilterFormatViolation(ConstraintViolationListInterface $vi } $this->assertTrue($filter_format_violation_found, format_string('Validation violation for invalid value "%invalid_value" found', array('%invalid_value' => $invalid_value))); } + + /** + * Tests that filter format dependency removal works. + * + * Ensure that modules providing filter plugins are required when the plugin + * is in use, and that only disabled plugins are removed from format + * configuration entities rather than the configuration entities being + * deleted. + * + * @see \Drupal\filter\Entity\FilterFormat::onDependencyRemoval() + * @see filter_system_info_alter() + */ + public function testDependencyRemoval() { + $this->installSchema('user', array('users_data')); + $filter_format = \Drupal\filter\Entity\FilterFormat::load('filtered_html'); + + // Enable the filter_test_restrict_tags_and_attributes filter plugin on the + // filtered_html filter format. + $filter_config = [ + 'weight' => 10, + 'status' => 1, + ]; + $filter_format->setFilterConfig('filter_test_restrict_tags_and_attributes', $filter_config)->save(); + + $module_data = _system_rebuild_module_data(); + $this->assertTrue($module_data['filter_test']->info['required'], 'The filter_test module is required.'); + $this->assertEqual($module_data['filter_test']->info['explanation'], String::format('Provides a filter plugin that is in use in the following filter formats: %formats', array('%formats' => $filter_format->label()))); + + // Disable the filter_test_restrict_tags_and_attributes filter plugin but + // have custom configuration so that the filter plugin is still configured + // in filtered_html the filter format. + $filter_config = [ + 'weight' => 20, + 'status' => 0, + ]; + $filter_format->setFilterConfig('filter_test_restrict_tags_and_attributes', $filter_config)->save(); + // Use the get method to match the assert after the module has been + // uninstalled. + $filters = $filter_format->get('filters'); + $this->assertTrue(isset($filters['filter_test_restrict_tags_and_attributes']), 'The filter plugin filter_test_restrict_tags_and_attributes is configured by the filtered_html filter format.'); + + drupal_static_reset('filter_formats'); + \Drupal::entityManager()->getStorage('filter_format')->resetCache(); + $module_data = _system_rebuild_module_data(); + $this->assertFalse(isset($module_data['filter_test']->info['required']), 'The filter_test module is required.'); + + // Verify that a dependency exists on the module that provides the filter + // plugin since it has configuration for the disabled plugin. + $this->assertEqual(['module' => ['filter_test']], $filter_format->getDependencies()); + + // Uninstall the module. + \Drupal::service('module_installer')->uninstall(array('filter_test')); + + // Verify the filter format still exists but the dependency and filter is + // gone. + \Drupal::entityManager()->getStorage('filter_format')->resetCache(); + $filter_format = \Drupal\filter\Entity\FilterFormat::load('filtered_html'); + $this->assertEqual([], $filter_format->getDependencies()); + // Use the get method since the FilterFormat::filters() method only returns + // existing plugins. + $filters = $filter_format->get('filters'); + $this->assertFalse(isset($filters['filter_test_restrict_tags_and_attributes']), 'The filter plugin filter_test_restrict_tags_and_attributes is not configured by the filtered_html filter format.'); + } + }