diff --git a/core/modules/filter/filter.install b/core/modules/filter/filter.install new file mode 100644 index 0000000..6faf807 --- /dev/null +++ b/core/modules/filter/filter.install @@ -0,0 +1,25 @@ +listAll('filter.format.') as $key) { + list(,, $format) = explode('.', $key); + $roles = user_roles(FALSE, "use text format $format"); + // Filter out the admin role. + $roles = array_filter($roles, function (RoleInterface $role) { + return !$role->isAdmin(); + }); + $factory->getEditable($key)->set('roles', array_keys($roles))->save(TRUE); + } +} diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index 934bda6..2a765aa 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -7,13 +7,14 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; -use Drupal\Component\Utility\Xss; use Drupal\Core\Cache\Cache; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Template\Attribute; +use Drupal\filter\Entity\FilterFormat; use Drupal\filter\FilterFormatInterface; +use Drupal\user\RoleInterface; /** * Implements hook_help(). @@ -444,6 +445,68 @@ function template_preprocess_filter_tips(&$variables) { } /** + * Implements hook_ENTITY_TYPE_insert() + */ +function filter_user_role_insert(RoleInterface $role) { + filter_format_sync_with_role($role); +} + +/** + * Implements hook_ENTITY_TYPE_update() + */ +function filter_user_role_update(RoleInterface $role) { + filter_format_sync_with_role($role); +} + +/** + * Synchronizes the corresponding text format when the permission to use that + * format is granted to, or revoked from a specific user role. + * + * @param \Drupal\user\RoleInterface $role + * The user role being inserted or updated. + */ +function filter_format_sync_with_role(RoleInterface $role) { + $changed = []; + /** @var \Drupal\filter\FilterFormatInterface[] $formats */ + $formats = FilterFormat::loadMultiple(); + // All "use text format {format}" permissions, keyed format ID, except the + // fallback format which is filtered out. + $formats_permissions = array_filter(array_map(function (FilterFormatInterface $format) { + return $format->getPermissionName(); + }, $formats)); + // Get this role permissions referring to usage of text formats. + $permissions = array_intersect($role->getPermissions(), $formats_permissions); + foreach ($permissions as $permission) { + // The format ID corresponding to the permission $permission. + $format_id = array_search($permission, $formats_permissions); + // Add this role to the text format corresponding to $permission, if the + // text format 'roles' property doesn't contain this. + $roles = $formats[$format_id]->get('roles'); + if (!in_array($role->id(), $roles)) { + $roles[] = $role->id(); + $formats[$format_id]->set('roles', $roles); + $changed[] = $format_id; + } + } + // Check if this role has lost the permission to use certain text formats. + if (!$role->isNew()) { + foreach ($formats as $format) { + $roles = $format->get('roles'); + $delta = array_search($role->id(), $roles); + if (($delta !== FALSE) && !in_array($format->getPermissionName(), $permissions)) { + unset($roles[$delta]); + $format->set('roles', $roles); + $changed[] = $format->id(); + } + } + } + // Save changed formats. + foreach ($changed as $format_id) { + $formats[$format_id]->save(); + } +} + +/** * @defgroup standard_filters Standard filters * @{ * Filters implemented by the Filter module. diff --git a/core/modules/filter/src/Entity/FilterFormat.php b/core/modules/filter/src/Entity/FilterFormat.php index 7419a15..65cb46e 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,18 +202,20 @@ 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()) { - foreach (user_roles() as $rid => $name) { - $enabled = in_array($rid, $roles, TRUE); - user_role_change_permissions($rid, array($permission => $enabled)); + if ($permission = $this->getPermissionName()) { + foreach (user_roles() as $rid => $role) { + $enabled = in_array($rid, $this->roles, TRUE); + if ($enabled && !$role->hasPermission($permission)) { + $role->grantPermission($permission)->save(); + } + elseif (!$enabled && $role->hasPermission($permission)) { + $role->revokePermission($permission)->save(); + } } } } @@ -421,12 +409,36 @@ 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. + foreach (Role::loadMultiple($this->get('roles')) 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..74c64a9 100644 --- a/core/modules/filter/src/FilterFormatFormBase.php +++ b/core/modules/filter/src/FilterFormatFormBase.php @@ -7,7 +7,6 @@ namespace Drupal\filter; -use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Form\FormStateInterface; @@ -215,6 +214,11 @@ 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 a 'checkboxes' values format + // into a simple indexed list, having the unchecked values filtered out. + $roles = 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 +249,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/FilterDefaultConfigTest.php b/core/modules/filter/src/Tests/FilterDefaultConfigTest.php index daef17b..8d6d10e 100644 --- a/core/modules/filter/src/Tests/FilterDefaultConfigTest.php +++ b/core/modules/filter/src/Tests/FilterDefaultConfigTest.php @@ -8,6 +8,7 @@ namespace Drupal\filter\Tests; use Drupal\simpletest\KernelTestBase; +use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; /** @@ -46,8 +47,8 @@ function testInstallation() { // Verify that format default property values have been added/injected. $this->assertTrue($format->uuid()); - // Verify that the loaded format does not contain any roles. - $this->assertEqual($format->get('roles'), NULL); + // The loaded format contains the roles supplied in the default config. + $this->assertEqual($format->get('roles'), array(RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID)); // Verify that the defined roles in the default config have been processed. $this->assertEqual(array_keys(filter_get_roles_by_format($format)), array( RoleInterface::ANONYMOUS_ID, @@ -73,7 +74,7 @@ function testInstallation() { } /** - * Tests that changes to FilterFormat::$roles do not have an effect. + * Tests changes to FilterFormat::$roles. */ function testUpdateRoles() { // Verify role permissions declared in default config. @@ -89,10 +90,9 @@ function testUpdateRoles() { )); $format->save(); - // Verify that roles have not been updated. + // Verify if the roles have been updated. $format = entity_load('filter_format', 'filter_test'); $this->assertEqual(array_keys(filter_get_roles_by_format($format)), array( - RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID, )); } diff --git a/core/modules/filter/src/Tests/FilterSettingsTest.php b/core/modules/filter/src/Tests/FilterSettingsTest.php index 9b95336..1649b8f 100644 --- a/core/modules/filter/src/Tests/FilterSettingsTest.php +++ b/core/modules/filter/src/Tests/FilterSettingsTest.php @@ -7,7 +7,11 @@ namespace Drupal\filter\Tests; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Form\FormState; +use Drupal\filter\Entity\FilterFormat; use Drupal\simpletest\KernelTestBase; +use Drupal\user\Entity\Role; /** * Tests filter settings. @@ -17,11 +21,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 +64,80 @@ function testFilterDefaults() { ))); } } + + /** + * Tests dependencies. + */ + public function testDependencies() { + /** @var \Drupal\Core\Config\ConfigFactoryInterface $factory */ + $factory = $this->container->get('config.factory'); + $this->installEntitySchema('user'); + + /** @var \Drupal\user\RoleInterface[] $roles */ + $roles = []; + // Create three arbitrary roles. + for ($i = 0; $i < 3; $i++) { + $roles[$i] = Role::create([ + 'id' => Unicode::strtolower($this->randomMachineName()), + 'label' => $this->randomString(), + ]); + $roles[$i]->save(); + } + + // Create a text format and grant the two roles to use it. + /** @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), count($format->get('roles'))); + $this->assertTrue(in_array($roles[0]->getConfigDependencyName(), $dependencies)); + $this->assertTrue(in_array($roles[1]->getConfigDependencyName(), $dependencies)); + $this->assertIdentical($roles[0]->getConfigDependencyName(), "user.role.{$format->get('roles')[0]}"); + $this->assertIdentical($roles[1]->getConfigDependencyName(), "user.role.{$format->get('roles')[1]}"); + + // The text format exists before deleting the role. + $this->assertNotNull($format = FilterFormat::load($format->id())); + + // Delete the second role. + $roles[1]->delete(); + + // Deleting a role should not delete the filter format. + $this->assertNotNull($format = FilterFormat::load($format->id())); + + // The format should be dependent now only on the first role. + $dependencies = $format->getDependencies()['config']; + $this->assertIdentical(count($dependencies), count($format->get('roles'))); + $this->assertTrue(in_array($roles[0]->getConfigDependencyName(), $dependencies)); + $this->assertFalse(in_array($roles[1]->getConfigDependencyName(), $dependencies)); + $this->assertIdentical($roles[0]->getConfigDependencyName(), "user.role.{$format->get('roles')[0]}"); + + // Test the reverse flow. The role is directly granted with the permission + // to use a text format. The text format 'roles' property and the dependency + // to that role should be updated accordingly. + $roles[2]->grantPermission("use text format {$format->id()}")->save(); + + // Checks if $role[2] has been added to $format->roles and the dependencies + // were updated. + $format = FilterFormat::load($format->id()); + $this->assertTrue(in_array($roles[2]->id(), $format->get('roles'))); + $this->assertTrue(in_array($roles[2]->getConfigDependencyName(), $format->getDependencies()['config'])); + + // Revoke the permission. + $roles[2]->revokePermission("use text format {$format->id()}")->save(); + + // Checks if $role[2] has been removed from $format->roles and the + // dependency were removed. + $format = FilterFormat::load($format->id()); + $this->assertFalse(in_array($roles[2]->id(), $format->get('roles'))); + $this->assertFalse(in_array($roles[2]->getConfigDependencyName(), $format->getDependencies()['config'])); + } + } 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..09ad78f --- /dev/null +++ b/core/modules/filter/src/Tests/Update/FilterUpdateTest.php @@ -0,0 +1,59 @@ +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'); + // The 'basic_html' text format config key. + $key = 'filter.format.basic_html'; + + // Checks that before the update the 'basic_html' text format misses + // the 'roles' property. + $this->assertNull($factory->get($key)->get('roles')); + + // Run updates. + $this->runUpdates(); + + // List of roles granted with 'use text format basic_html' permission. + $roles = user_roles(FALSE, 'use text format basic_html'); + // Filter out the admin role. + $roles = array_keys(array_filter($roles, function (RoleInterface $role) { + return !$role->isAdmin(); + })); + + // The roles allowed to use 'basic_html' text format were copied into the + // config entity, in 'roles' property. + $this->assertIdentical($factory->get($key)->get('roles'), $roles); + } + +}