diff --git a/core/modules/filter/filter.install b/core/modules/filter/filter.install new file mode 100644 index 0000000..0d670dc --- /dev/null +++ b/core/modules/filter/filter.install @@ -0,0 +1,26 @@ +get('filter.settings')->get('fallback_format'); + + foreach ($factory->listAll('filter.format.') as $key) { + list(,, $format) = explode('.', $key); + if ($format != $fallback_format) { + $roles = array_keys(user_roles(FALSE, "use text format $format")); + } + else { + $roles = []; + } + $factory->getEditable($key)->set('roles', $roles)->save(TRUE); + } +} diff --git a/core/modules/filter/src/Entity/FilterFormat.php b/core/modules/filter/src/Entity/FilterFormat.php index 7419a15..1df794e 100644 --- a/core/modules/filter/src/Entity/FilterFormat.php +++ b/core/modules/filter/src/Entity/FilterFormat.php @@ -14,6 +14,7 @@ use Drupal\filter\FilterFormatInterface; use Drupal\filter\FilterPluginCollection; use Drupal\filter\Plugin\FilterInterface; +use Drupal\user\Entity\Role; /** * Represents a text format. @@ -86,19 +87,15 @@ class FilterFormat extends ConfigEntityBase implements FilterFormatInterface, En protected $weight = 0; /** - * List of user role IDs to grant access to use this format on initial creation. + * List of user role IDs granted with "use text format {$this->id()}" + * permission. * - * This property is always empty and unused for existing text formats. + * If this text format is the fallback format, this property is an empty + * array. * - * Default configuration objects of modules and installation profiles are - * allowed to specify a list of user role IDs to grant access to. - * - * This property only has an effect when a new text format is created and the - * list is not empty. By default, no user role is allowed to use a new format. - * - * @var array + * @var string[] */ - protected $roles; + protected $roles = []; /** * Configured filters for this text format. @@ -168,17 +165,6 @@ public function setFilterConfig($instance_id, array $configuration) { /** * {@inheritdoc} */ - public function toArray() { - $properties = parent::toArray(); - // The 'roles' property is only used during install and should never - // actually be saved. - unset($properties['roles']); - return $properties; - } - - /** - * {@inheritdoc} - */ public function disable() { if ($this->isFallbackFormat()) { throw new \LogicException("The fallback text format '{$this->id()}' cannot be disabled."); @@ -216,17 +202,14 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { // Clear the static caches of filter_formats() and others. filter_formats_reset(); - if (!$update && !$this->isSyncing()) { - // Default configuration of modules and installation profiles is allowed - // to specify a list of user roles to grant access to for the new format; - // apply the defined user role permissions when a new format is inserted - // and has a non-empty $roles property. + if (!$this->isSyncing()) { + // Synchronize user roles permissions on this text format. // Note: user_role_change_permissions() triggers a call chain back into // \Drupal\filter\FilterPermissions::permissions() and lastly // filter_formats(), so its cache must be reset upfront. - if (($roles = $this->get('roles')) && $permission = $this->getPermissionName()) { + if ($permission = $this->getPermissionName()) { foreach (user_roles() as $rid => $name) { - $enabled = in_array($rid, $roles, TRUE); + $enabled = in_array($rid, $this->roles, TRUE); user_role_change_permissions($rid, array($permission => $enabled)); } } @@ -421,12 +404,40 @@ public function onDependencyRemoval(array $dependencies) { $changed = TRUE; } } + + // Remove the dependencies of deleted user roles. + foreach ($dependencies['config'] as $key => $config) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $config */ + if ($config->getEntityTypeId() == 'user_role') { + if (($delta = array_search($config->id(), $this->roles)) !== FALSE) { + unset($this->roles[$delta]); + $changed = TRUE; + } + } + } + return $changed; } /** * {@inheritdoc} */ + public function calculateDependencies() { + parent::calculateDependencies(); + // Add user role config entities as dependencies. The permission name is + // FALSE when this is a fallback text format and that doesn't create any + // dependency on user roles. + if (($rids = $this->get('roles')) && $permission = $this->getPermissionName()) { + foreach (Role::loadMultiple($rids) as $role) { + $this->addDependency($role->getConfigDependencyKey(), $role->getConfigDependencyName()); + } + } + return $this->dependencies; + } + + /** + * {@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 diff --git a/core/modules/filter/src/FilterFormatFormBase.php b/core/modules/filter/src/FilterFormatFormBase.php index e1222b4..0d64c8a 100644 --- a/core/modules/filter/src/FilterFormatFormBase.php +++ b/core/modules/filter/src/FilterFormatFormBase.php @@ -215,6 +215,12 @@ public function validateForm(array &$form, FormStateInterface $form_state) { $form_state->setValueForElement($form['format'], $format_format); $form_state->setValueForElement($form['name'], $format_name); + // Normalize roles by converting the list from 'checkboxes' values format to + // a simple indexed list, having unchecked values filtered out. + $is_fallback = $format_format == $this->config('filter.settings')->get('fallback_format'); + $roles = $is_fallback ? [] : array_values(array_filter($form_state->getValue('roles'))); + $form_state->setValueForElement($form['roles'], $roles); + $format_exists = $this->queryFactory ->get('filter_format') ->condition('format', $format_format, '<>') @@ -245,13 +251,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } $format->save(); - // Save user permissions. - if ($permission = $format->getPermissionName()) { - foreach ($form_state->getValue('roles') as $rid => $enabled) { - user_role_change_permissions($rid, array($permission => $enabled)); - } - } - $form_state->setRedirect('filter.admin_overview'); return $this->entity; diff --git a/core/modules/filter/src/Tests/FilterSettingsTest.php b/core/modules/filter/src/Tests/FilterSettingsTest.php index 9b95336..37b20a4 100644 --- a/core/modules/filter/src/Tests/FilterSettingsTest.php +++ b/core/modules/filter/src/Tests/FilterSettingsTest.php @@ -7,7 +7,10 @@ namespace Drupal\filter\Tests; +use Drupal\Component\Utility\Unicode; +use Drupal\filter\Entity\FilterFormat; use Drupal\simpletest\KernelTestBase; +use Drupal\user\Entity\Role; /** * Tests filter settings. @@ -17,11 +20,9 @@ class FilterSettingsTest extends KernelTestBase { /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ - public static $modules = array('filter'); + public static $modules = ['filter', 'user', 'system']; /** * Tests explicit and implicit default settings for filters. @@ -62,4 +63,80 @@ function testFilterDefaults() { ))); } } + + /** + * Tests dependencies. + */ + public function testDependencies() { + /** @var \Drupal\Core\Config\ConfigFactoryInterface $factory */ + $factory = $this->container->get('config.factory'); + $this->installEntitySchema('user'); + + // Create 2 arbitrary roles allowed to use the text format $format. + /** @var \Drupal\user\RoleInterface[] $roles */ + $roles = []; + for ($i = 0; $i < 2; $i++) { + $roles[$i] = Role::create([ + 'id' => Unicode::strtolower($this->randomMachineName()), + 'label' => $this->randomString(), + ]); + $roles[$i]->save(); + } + + // Create a text formats. + /** @var \Drupal\filter\FilterFormatInterface $format */ + $format = FilterFormat::create([ + 'format' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + 'roles' => [$roles[0]->id(), $roles[1]->id()], + ]); + + // Save the format. + $format->save(); + + // The format should be dependent on both role config entities. + $dependencies = $format->getDependencies()['config']; + $this->assertIdentical(count($dependencies), 2); + $this->assertTrue(in_array($roles[0]->getConfigDependencyName(), $dependencies)); + $this->assertTrue(in_array($roles[1]->getConfigDependencyName(), $dependencies)); + + // Mark this text format as fallback format. + $settings = $factory->getEditable('filter.settings'); + $settings->set('fallback_format', $format->id())->save(); + + // Recalculate dependencies by saving the text format. + $format->save(); + + // The format should not depend anymore on the two roles because now this + // text format is the fallback format. + $this->assertTrue(empty($format->getDependencies()['config'])); + + // Set the fallback format to different format. + $settings + ->set('fallback_format', Unicode::strtolower($this->randomMachineName())) + ->save(); + $format->save(); + + // The format should be again dependent on both role config entities. + $dependencies = $format->getDependencies()['config']; + $this->assertIdentical(count($dependencies), 2); + $this->assertTrue(in_array($roles[0]->getConfigDependencyName(), $dependencies)); + $this->assertTrue(in_array($roles[1]->getConfigDependencyName(), $dependencies)); + + // The text format exists before deleting the role. + $this->assertTrue($format = FilterFormat::load($format->id())); + + // Delete the second role. + $roles[1]->delete(); + + // Deleting a role should not delete the filter format. + $this->assertTrue($format = FilterFormat::load($format->id())); + + // The format should be dependent only on first role. + $dependencies = $format->getDependencies()['config']; + $this->assertIdentical(count($dependencies), 1); + $this->assertTrue(in_array($roles[0]->getConfigDependencyName(), $dependencies)); + $this->assertFalse(in_array($roles[1]->getConfigDependencyName(), $dependencies)); + } + } diff --git a/core/modules/filter/src/Tests/Update/FilterUpdateTest.php b/core/modules/filter/src/Tests/Update/FilterUpdateTest.php new file mode 100644 index 0000000..6eaec35 --- /dev/null +++ b/core/modules/filter/src/Tests/Update/FilterUpdateTest.php @@ -0,0 +1,62 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + ]; + } + + /** + * Tests filter_update_8001(). + * + * @see filter_update_8001(). + */ + public function testFilterUpdate8001() { + /** @var \Drupal\Core\Config\ConfigFactoryInterface $factory */ + $factory = $this->container->get('config.factory'); + + // Load a common text format. + $full_html = $factory->get('filter.format.full_html'); + $full_html_roles = array_keys(user_roles(FALSE, "use text format {$full_html->get('format')}")); + + // Load the fallback text format. + $plain_text = $factory->get('filter.format.plain_text'); + + // The text formats are missing the 'roles' property. + $this->assertNull($full_html->get('roles')); + $this->assertNull($plain_text->get('roles')); + + // Run updates. + $this->runUpdates(); + + // Reload text formats. + $full_html = $factory->get('filter.format.full_html'); + $plain_text = $factory->get('filter.format.plain_text'); + + // The roles allowed to use 'full_html' text format were copied in the + // config entity. + $this->assertIdentical($full_html->get('roles'), $full_html_roles); + // The fallback text format has no roles. + $this->assertIdentical($plain_text->get('roles'), []); + } + +}