diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index 6d558fe..0508c7f 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; @@ -83,6 +84,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. * * @param \Drupal\Core\Session\AccountInterface|null $account diff --git a/core/modules/filter/src/Entity/FilterFormat.php b/core/modules/filter/src/Entity/FilterFormat.php index 4b00af1..8ac55cd 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,43 @@ 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) { + // Only remove disabled filters. + 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 b5e75ce..b9c558b 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 588546e..8b01fc1 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,64 @@ 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 dependency removal works. + * + * Uninstalling a module that provides a filter plugin should not remove a + * filter format configuration entity that does not have the filter plugin + * enabled. + */ + 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.'); + } + }