diff --git a/core/modules/user/migrations/d6_user_role.yml b/core/modules/user/migrations/d6_user_role.yml index 03e30ade613aac11e88cf8b6515b9bf1e875895d..a0504851d98aa5f76980177fb918ac65c8991408 100644 --- a/core/modules/user/migrations/d6_user_role.yml +++ b/core/modules/user/migrations/d6_user_role.yml @@ -35,13 +35,15 @@ process: - plugin: node_update_7008 - plugin: flatten - plugin: filter_format_permission - # A special flag so we can migrate permissions that do not exist yet. - # @todo Remove in https://www.drupal.org/project/drupal/issues/2953111. - skip_missing_permission_deprecation: - plugin: default_value - default_value: true destination: plugin: entity:user_role migration_dependencies: required: - d6_filter_format + optional: + - block_content_type + - contact_category + - d6_comment_type + - d6_node_type + - d6_taxonomy_vocabulary + - d6_taxonomy_vocabulary_translation diff --git a/core/modules/user/migrations/d7_user_role.yml b/core/modules/user/migrations/d7_user_role.yml index 4aaf8891a09150a18f6dcfff08121dae13bf53fe..09fd6f1a7f4d88cf28158e20698e9b262d810df4 100644 --- a/core/modules/user/migrations/d7_user_role.yml +++ b/core/modules/user/migrations/d7_user_role.yml @@ -33,13 +33,15 @@ process: 'edit own forum topics': 'edit own forum content' - plugin: flatten weight: weight - # A special flag so we can migrate permissions that do not exist yet. - # @todo Remove in https://www.drupal.org/project/drupal/issues/2953111. - skip_missing_permission_deprecation: - plugin: default_value - default_value: true destination: plugin: entity:user_role migration_dependencies: optional: + - block_content_type + - contact_category + - d7_comment_type - d7_filter_format + - d7_node_type + - d7_shortcut_set + - d7_taxonomy_vocabulary + - d7_taxonomy_vocabulary_translation diff --git a/core/modules/user/src/Entity/Role.php b/core/modules/user/src/Entity/Role.php index af05336658b2438d1939fda287ce4dd7e025ffd9..7e38a43e5a2a868c00d2a9f3c0bcebe6ccfe57a0 100644 --- a/core/modules/user/src/Entity/Role.php +++ b/core/modules/user/src/Entity/Role.php @@ -202,7 +202,7 @@ public function calculateDependencies() { $permission_definitions = \Drupal::service('user.permissions')->getPermissions(); $valid_permissions = array_intersect($this->permissions, array_keys($permission_definitions)); $invalid_permissions = array_diff($this->permissions, $valid_permissions); - if (!empty($invalid_permissions) && !$this->get('skip_missing_permission_deprecation')) { + if (!empty($invalid_permissions)) { throw new \RuntimeException('Adding non-existent permissions to a role is not allowed. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '".'); } foreach ($valid_permissions as $permission) { diff --git a/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php new file mode 100644 index 0000000000000000000000000000000000000000..a70a3d99597769205bd93464f9d3d93a02d15437 --- /dev/null +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUserRole.php @@ -0,0 +1,95 @@ +<?php + +namespace Drupal\user\Plugin\migrate\destination; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a destination plugin for migrating user role entities. + * + * @MigrateDestination( + * id = "entity:user_role" + * ) + */ +class EntityUserRole extends EntityConfigBase { + + /** + * All permissions on the destination site. + * + * @var string[] + */ + protected $destinationPermissions = []; + + /** + * Builds a user role entity destination. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The storage for this entity type. + * @param array $bundles + * The list of bundles this entity type has. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The configuration factory. + * @param array $destination_permissions + * All available permissions. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, array $destination_permissions) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $language_manager, $config_factory); + $this->destinationPermissions = $destination_permissions; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + $entity_type_id = static::getEntityTypeId($plugin_id); + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->get('entity_type.manager')->getStorage($entity_type_id), + array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id)), + $container->get('language_manager'), + $container->get('config.factory'), + array_keys($container->get('user.permissions')->getPermissions()), + ); + } + + /** + * {@inheritdoc} + */ + public function import(Row $row, array $old_destination_id_values = []): array|bool { + $permissions = $row->getDestinationProperty('permissions') ?? []; + + // Get permissions that do not exist on the destination. + $invalid_permissions = array_diff($permissions, $this->destinationPermissions); + if ($invalid_permissions) { + sort($invalid_permissions); + // Log the message in the migration message table. + $message = "Permission(s) '" . implode("', '", $invalid_permissions) . "' not found."; + $this->migration->getIdMap() + ->saveMessage($row->getSourceIdValues(), $message, MigrationInterface::MESSAGE_WARNING); + } + + $valid_permissions = array_intersect($permissions, $this->destinationPermissions); + $row->setDestinationProperty('permissions', $valid_permissions); + return parent::import($row, $old_destination_id_values); + } + +} diff --git a/core/modules/user/tests/src/Functional/Update/UserUpdateRoleMigrateTest.php b/core/modules/user/tests/src/Functional/Update/UserUpdateRoleMigrateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0e449bcaccdfd9ae3be17b382a9c06a81190d800 --- /dev/null +++ b/core/modules/user/tests/src/Functional/Update/UserUpdateRoleMigrateTest.php @@ -0,0 +1,65 @@ +<?php + +namespace Drupal\Tests\user\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; +use Drupal\user\Entity\Role; + +/** + * Tests user_post_update_update_roles_followup() upgrade path. + * + * @group Update + * @group legacy + */ +class UserUpdateRoleMigrateTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz', + ]; + } + + /** + * Tests that roles have only existing permissions. + */ + public function testRolePermissions() { + /** @var \Drupal\Core\Database\Connection $connection */ + $connection = \Drupal::service('database'); + + // Edit the authenticated role to have a non-existent permission. + $authenticated = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'user.role.authenticated') + ->execute() + ->fetchField(); + $authenticated = unserialize($authenticated); + $authenticated['permissions'][] = 'does_not_exist'; + $connection->update('config') + ->fields([ + 'data' => serialize($authenticated), + ]) + ->condition('collection', '') + ->condition('name', 'user.role.authenticated') + ->execute(); + + $authenticated = Role::load('authenticated'); + $this->assertTrue($authenticated->hasPermission('does_not_exist'), 'Authenticated role has a permission that does not exist'); + + $this->runUpdates(); + + $this->assertSession()->pageTextContains('The role Authenticated user has had non-existent permissions removed. Check the logs for details.'); + + $authenticated = Role::load('authenticated'); + $this->assertFalse($authenticated->hasPermission('does_not_exist'), 'Authenticated role does not have a permission that does not exist'); + + $this->drupalLogin($this->createUser(['access site reports'])); + $this->drupalGet('admin/reports/dblog', ['query' => ['type[]' => 'update']]); + $this->clickLink('The role Authenticated user has had the following non-…'); + $this->assertSession()->pageTextContains('The role Authenticated user has had the following non-existent permission(s) removed: does_not_exist.'); + } + +} diff --git a/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php index ea6f946358aec23e029bd4b0816c9cf2f4687643..7f2e4bd636f2c60b80342d19362bdb093ec06090 100644 --- a/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php +++ b/core/modules/user/tests/src/Kernel/Migrate/d6/MigrateUserRoleTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\user\Kernel\Migrate\d6; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase; @@ -19,7 +20,43 @@ class MigrateUserRoleTest extends MigrateDrupal6TestBase { */ protected function setUp(): void { parent::setUp(); - $this->executeMigrations(['d6_filter_format', 'd6_user_role']); + $this->startCollectingMessages(); + } + + /** + * Assert the logged migrate messages. + * + * @param string[][] $role_data + * An array of role data keyed by the destination role id. The role data + * contains the source role id, an array of valid permissions and an array + * of invalid permissions. + * @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map + * The migration ID map plugin. + */ + public function assertMessages(array $role_data, MigrateIdMapInterface $id_map) { + foreach ($id_map->getMessages() as $message) { + $permissions = implode("', '", $role_data[$message->dest_id]['invalid']); + $expected_message = "Permission(s) '" . $permissions . "' not found."; + $this->assertSame($expected_message, $message->message); + $this->assertSame(MigrationInterface::MESSAGE_WARNING, (int) $message->level); + } + + } + + /** + * Asserts there are no duplicate roles. + */ + public function assertNoDuplicateRoles() { + $roles = [ + 'anonymous1', + 'authenticated1', + 'administrator1', + 'migrate_test_role_11', + 'migrate_test_role_21', + 'migrate_test_role_3_that_is_longer_than_thirty_two_characters1', + 'migrate_test_role_41', + ]; + $this->assertEmpty(Role::loadMultiple($roles)); } /** @@ -40,7 +77,6 @@ protected function assertRole(string $id, array $permissions, int $lookupId, Mig /** @var \Drupal\user\RoleInterface $role */ $role = Role::load($id); $this->assertInstanceOf(RoleInterface::class, $role); - sort($permissions); $this->assertSame($permissions, $role->getPermissions()); $this->assertSame([[$id]], $id_map->lookupDestinationIds(['rid' => $lookupId])); } @@ -49,81 +85,217 @@ protected function assertRole(string $id, array $permissions, int $lookupId, Mig * Helper function to test the migration of the user roles. The user roles * will be re-imported and the tests here will be repeated. * + * @param array $permissions + * Contains the valid and invalid permissions. * @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map * The map table plugin. * * @internal */ - protected function assertRoles(MigrateIdMapInterface $id_map): void { - - // The permissions for each role are found in the two tables in the Drupal 6 - // source database. One is the permission table and the other is the - // filter_format table. - $permissions = [ - // From permission table. - 'access content', - 'migrate test anonymous permission', - // From filter_format tables. - 'use text format filtered_html', - ]; - $this->assertRole('anonymous', $permissions, 1, $id_map); - - $permissions = [ - // From permission table. - 'access comments', - 'access content', - 'migrate test authenticated permission', - 'post comments', - 'skip comment approval', - // From filter_format. - 'use text format filtered_html', - ]; - $this->assertRole('authenticated', $permissions, 2, $id_map); - - $permissions = [ - // From permission table. - 'migrate test role 1 test permission', - // From filter format. - 'use text format full_html', - 'use text format php_code', - ]; - $this->assertRole('migrate_test_role_1', $permissions, 3, $id_map); - - $permissions = [ - // From permission table. - 'migrate test role 2 test permission', - 'use PHP for settings', - 'administer contact forms', - 'skip comment approval', - 'edit own blog content', - 'edit any blog content', - 'delete own blog content', - 'delete any blog content', - 'create forum content', - 'delete any forum content', - 'delete own forum content', - 'edit any forum content', - 'edit own forum content', - 'administer nodes', - 'access content overview', - // From filter format. - 'use text format php_code', - ]; - $this->assertRole('migrate_test_role_2', $permissions, 4, $id_map); + protected function assertRoles(array $permissions, MigrateIdMapInterface $id_map): void { + foreach ($permissions as $rid => $datum) { + $this->assertRole($rid, $datum['valid'], $datum['rid'], $id_map); + } + } - // The only permission for this role is a filter format. - $permissions = ['use text format php_code']; - $this->assertRole('migrate_test_role_3_that_is_longer_than_thirty_two_characters', $permissions, 5, $id_map); + /** + * Data provider for user role migration tests. + */ + public function providerTestUserRole() { + return [ + 'filter only' => [ + 'modules' => [], + 'migrations' => [ + 'd6_filter_format', + 'd6_user_role', + ], + 'role_data' => [ + 'anonymous' => [ + 'rid' => '1', + 'valid' => [ + 'access content', + 'use text format filtered_html', + ], + 'invalid' => [ + 'migrate test anonymous permission', + ], + ], + 'authenticated' => [ + 'rid' => '2', + 'valid' => [ + 'access content', + 'use text format filtered_html', + ], + 'invalid' => [ + 'access comments', + 'migrate test authenticated permission', + 'post comments', + 'skip comment approval', + ], + ], + 'migrate_test_role_1' => [ + 'rid' => '3', + 'valid' => [ + 'use text format full_html', + 'use text format php_code', + ], + 'invalid' => [ + 'migrate test role 1 test permission', + ], + ], + 'migrate_test_role_2' => [ + 'rid' => '4', + 'valid' => [ + 'access content overview', + 'administer nodes', + 'use text format php_code', + ], + 'invalid' => [ + 'administer contact forms', + 'create forum content', + 'delete any blog content', + 'delete any forum content', + 'delete own blog content', + 'delete own forum content', + 'edit any blog content', + 'edit any forum content', + 'edit own blog content', + 'edit own forum content', + 'migrate test role 2 test permission', + 'skip comment approval', + 'use PHP for settings', + ], + ], + 'migrate_test_role_3_that_is_longer_than_thirty_two_characters' => [ + 'rid' => '5', + 'valid' => [ + 'use text format php_code', + ], + 'invalid' => [], + ], + ], + ], + 'all dependent migrations' => [ + 'modules' => [ + 'block', + 'block_content', + 'comment', + 'contact', + 'config_translation', + 'language', + 'link', + 'menu_ui', + 'node', + 'taxonomy', + 'text', + ], + 'migrations' => [ + 'language', + 'd6_comment_type', + 'block_content_type', + 'contact_category', + 'd6_filter_format', + 'd6_taxonomy_vocabulary', + 'd6_taxonomy_vocabulary_translation', + 'd6_user_role', + ], + 'role_data' => [ + 'anonymous' => [ + 'rid' => '1', + 'valid' => [ + 'access content', + 'use text format filtered_html', + ], + 'invalid' => [ + 'migrate test anonymous permission', + ], + ], + 'authenticated' => [ + 'rid' => '2', + 'valid' => [ + 'access comments', + 'access content', + 'post comments', + 'skip comment approval', + 'use text format filtered_html', + ], + 'invalid' => [ + 'migrate test authenticated permission', + ], + ], + 'migrate_test_role_1' => [ + 'rid' => '3', + 'valid' => [ + 'use text format full_html', + 'use text format php_code', + ], + 'invalid' => [ + 'migrate test role 1 test permission', + ], + ], + 'migrate_test_role_2' => [ + 'rid' => '4', + 'valid' => [ + 'access content overview', + 'administer contact forms', + 'administer nodes', + 'create forum content', + 'delete any forum content', + 'delete own forum content', + 'edit any forum content', + 'edit own forum content', + 'skip comment approval', + 'use text format php_code', + ], + 'invalid' => [ + 'delete any blog content', + 'delete own blog content', + 'edit any blog content', + 'edit own blog content', + 'migrate test role 2 test permission', + 'use PHP for settings', + ], + ], + 'migrate_test_role_3_that_is_longer_than_thirty_two_characters' => [ + 'rid' => '5', + 'valid' => [ + 'use text format php_code', + ], + 'invalid' => [], + ], + ], + ], + ]; } /** * Tests user role migration. + * + * @param string[] $modules + * A list of modules to install. + * @param string[] $migrations + * A list of migrations to execute. + * @param string[][] $role_data + * An array of role data keyed by the destination role id. The role data + * contains the source role id, an array of valid permissions and an array + * of invalid permissions. + * + * @dataProvider providerTestUserRole() */ - public function testUserRole() { + public function testUserRole(array $modules, array $migrations, array $role_data) { + if ($modules) { + // Install modules that have migrations that may provide permissions. + \Drupal::service('module_installer')->install($modules); + $this->installEntitySchema('block_content'); + $this->installConfig(['block_content', 'comment']); + $this->migrateContentTypes(); + } + $this->executeMigrations($migrations); $id_map = $this->getMigration('d6_user_role')->getIdMap(); - $this->assertRoles($id_map); - // Test there are no duplicated roles. + // After all the migrations are run, there are changes to the permissions. + $this->assertRoles($role_data, $id_map); + $roles = [ 'anonymous1', 'authenticated1', @@ -134,6 +306,9 @@ public function testUserRole() { ]; $this->assertEmpty(Role::loadMultiple($roles)); + $this->assertMessages($role_data, $id_map); + $this->assertSame(4, $id_map->messageCount()); + // Remove the map row for the migrate_test_role_1 role and rerun the // migration. This will re-import the migrate_test_role_1 role migration // again. @@ -157,11 +332,10 @@ public function testUserRole() { $this->executeMigration('d6_user_role'); // Test there are no duplicated roles. - $roles[] = 'migrate_test_role_41'; - $this->assertEmpty(Role::loadMultiple($roles)); + $this->assertNoDuplicateRoles(); // Test that the existing roles have not changed. - $this->assertRoles($id_map); + $this->assertRoles($role_data, $id_map); // Test the migration of the new role, migrate_test_role_4. $permissions = ['access content']; diff --git a/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php b/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php index 4d7fcd66b73729fd0e1fee20b5e8379972db7330..a4012f8a052b0c2ccfb3af60ae6f166660640b25 100644 --- a/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php +++ b/core/modules/user/tests/src/Kernel/Migrate/d7/MigrateUserRoleTest.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\user\Kernel\Migrate\d7; -use Drupal\Core\Database\Database; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; @@ -29,36 +29,46 @@ protected function setUp(): void { * The role ID. * @param string $label * The role's expected label. - * @param int $original_rid - * The original (integer) ID of the role, to check permissions. + * @param string[] $permissions + * The expected permissions. * * @internal */ - protected function assertEntity(string $id, string $label, int $original_rid): void { + protected function assertEntity(string $id, string $label, array $permissions): void { /** @var \Drupal\user\RoleInterface $entity */ $entity = Role::load($id); $this->assertInstanceOf(RoleInterface::class, $entity); $this->assertSame($label, $entity->label()); - - if (isset($original_rid)) { - $permissions = Database::getConnection('default', 'migrate') - ->select('role_permission', 'rp') - ->fields('rp', ['permission']) - ->condition('rid', $original_rid) - ->execute() - ->fetchCol(); - sort($permissions); - $this->assertSame($permissions, $entity->getPermissions()); - } + $this->assertSame($permissions, $entity->getPermissions()); } /** * Tests user role migration. */ public function testUserRole() { - $this->assertEntity('anonymous', 'anonymous user', 1); - $this->assertEntity('authenticated', 'authenticated user', 2); - $this->assertEntity('administrator', 'administrator', 3); + $anonymous_permissions = ['access content']; + $this->assertEntity('anonymous', 'anonymous user', $anonymous_permissions); + $this->assertEntity('authenticated', 'authenticated user', $anonymous_permissions); + $admin_permissions = [ + 'access administration pages', + 'access content', + 'access site in maintenance mode', + 'access site reports', + 'access user profiles', + 'administer menu', + 'administer modules', + 'administer permissions', + 'administer site configuration', + 'administer software updates', + 'administer themes', + 'administer users', + 'cancel account', + 'change own username', + 'select account cancellation method', + 'view the administration theme', + ]; + $this->assertEntity('administrator', 'administrator', $admin_permissions); + // Test there are no duplicated roles. $roles = [ 'anonymous1', @@ -97,12 +107,107 @@ public function testUserRole() { $this->assertEmpty(Role::loadMultiple($roles)); // Test that the existing roles have not changed. - $this->assertEntity('administrator', 'administrator', 3); - $this->assertEntity('anonymous', 'anonymous user', 1); - $this->assertEntity('authenticated', 'authenticated user', 2); + $this->assertEntity('administrator', 'administrator', $admin_permissions); + $this->assertEntity('anonymous', 'anonymous user', $anonymous_permissions); + $this->assertEntity('authenticated', 'authenticated user', $anonymous_permissions); // Test the migration of the new role, test role. - $this->assertEntity('test_role', 'test role', 4); + $this->assertEntity('test_role', 'test role', $anonymous_permissions); + + // Tests the migration log contains an error message. + // User role Authenticated. + $permissions[1] = [ + 'access comments', + 'use text format filtered_html', + ]; + // User role test_role. + $permissions[2] = [ + 'access comments', + 'post comments', + 'skip comment approval', + 'use text format custom_text_format', + 'use text format filtered_html', + ]; + // User role administrator. + $permissions[3] = [ + 'access all views', + 'access comments', + 'access content overview', + 'access contextual links', + 'access dashboard', + 'access news feeds', + 'access overlay', + 'access printer-friendly version', + 'access site-wide contact form', + 'access statistics', + 'access toolbar', + 'access user contact forms', + 'add content to books', + 'administer actions', + 'administer blocks', + 'administer book outlines', + 'administer comments', + 'administer contact forms', + 'administer content types', + 'administer filters', + 'administer forums', + 'administer image styles', + 'administer languages', + 'administer news feeds', + 'administer nodes', + 'administer search', + 'administer shortcuts', + 'administer statistics', + 'administer taxonomy', + 'administer unit tests', + 'administer url aliases', + 'administer views', + 'block IP addresses', + 'bypass node access', + 'create article content', + 'create new books', + 'create page content', + 'create url aliases', + 'customize shortcut links', + 'delete any article content', + 'delete any page content', + 'delete own article content', + 'delete own page content', + 'delete revisions', + 'delete terms in 1', + 'edit any article content', + 'edit any page content', + 'edit own article content', + 'edit own comments', + 'edit own page content', + 'edit terms in 1', + 'post comments', + 'revert revisions', + 'search content', + 'skip comment approval', + 'switch shortcut sets', + 'translate admin strings', + 'translate blocks', + 'translate content', + 'translate interface', + 'translate user-defined strings', + 'use PHP for settings', + 'use advanced search', + 'use text format custom_text_format', + 'use text format filtered_html', + 'use text format full_html', + 'view own unpublished content', + 'view post access counter', + 'view revisions', + ]; + + foreach ($id_map->getMessages() as $message) { + $expected_permissions = implode("', '", $permissions[$message->src_rid]); + $expected_message = "Permission(s) '" . $expected_permissions . "' not found."; + $this->assertSame($expected_message, $message->message); + $this->assertSame(MigrationInterface::MESSAGE_WARNING, (int) $message->level); + } + $this->assertSame(3, $id_map->messageCount()); } } diff --git a/core/modules/user/user.post_update.php b/core/modules/user/user.post_update.php index f1b6da9bc9c16d85c1711001838b1d741f805b1f..8dd6cf5b474317d2db742d64cb85fd86a2841efe 100644 --- a/core/modules/user/user.post_update.php +++ b/core/modules/user/user.post_update.php @@ -5,6 +5,10 @@ * Post update functions for User module. */ +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\Core\StringTranslation\PluralTranslatableMarkup; +use Drupal\user\Entity\Role; + /** * Implements hook_removed_post_updates(). */ @@ -14,3 +18,39 @@ function user_removed_post_updates() { 'user_post_update_update_roles' => '10.0.0', ]; } + +/** + * Remove non-existent permissions created by migrations. + */ +function user_post_update_update_migrated_roles_followup(&$sandbox = NULL) { + $cleaned_roles = []; + $existing_permissions = array_keys(\Drupal::service('user.permissions') + ->getPermissions()); + \Drupal::classResolver(ConfigEntityUpdater::class) + ->update($sandbox, 'user_role', function (Role $role) use ($existing_permissions, &$cleaned_roles) { + $removed_permissions = array_diff($role->getPermissions(), $existing_permissions); + if (!empty($removed_permissions)) { + $cleaned_roles[] = $role->label(); + \Drupal::logger('update')->notice( + 'The role %role has had the following non-existent permission(s) removed: %permissions.', + [ + '%role' => $role->label(), + '%permissions' => implode(', ', $removed_permissions), + ] + ); + $permissions = array_intersect($role->getPermissions(), $existing_permissions); + $role->set('permissions', $permissions); + return TRUE; + } + }); + + if (!empty($cleaned_roles)) { + return new PluralTranslatableMarkup( + count($cleaned_roles), + 'The role %role_list has had non-existent permissions removed. Check the logs for details.', + 'The roles %role_list have had non-existent permissions removed. Check the logs for details.', + ['%role_list' => implode(', ', $cleaned_roles)] + ); + } + +}