Role/Capability: Add a new update_role function.

Until now, changing a user's role involved deleting a user's role then re-adding.  This change creates a new `update_role` function and associated method in `WP_Roles` to consolidate this process.

This commit also introduces new unit tests around `update_role` and adds additional "unhappy path" tests for roles and capabilities in general.

Props maksimkuzmin, peterwilsoncc, NomNom99, costdev, SergeyBiryukov.
Fixes #54572.

git-svn-id: https://develop.svn.wordpress.org/trunk@54213 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
David Baumwald 2022-09-19 20:47:22 +00:00
parent 906ec63c78
commit a647192d7c
3 changed files with 549 additions and 0 deletions

View File

@ -1030,6 +1030,30 @@ function add_role( $role, $display_name, $capabilities = array() ) {
return wp_roles()->add_role( $role, $display_name, $capabilities );
}
/**
* Updates an existing role. Creates a new role if it doesn't exist.
*
* Modifies the display name and/or capabilities for an existing role.
* If the role does not exist then a new role is created.
*
* The capabilities are defined in the following format: `array( 'read' => true )`.
* To explicitly deny the role a capability, set the value for that capability to false.
*
* @since 6.1.0
*
* @param string $role Role name.
* @param string|null $display_name Optional. Role display name. If null, the display name
* is not modified. Default null.
* @param bool[]|null $capabilities Optional. List of capabilities keyed by the capability name,
* e.g. `array( 'edit_posts' => true, 'delete_posts' => false )`.
* If null, don't alter capabilities for the existing role and make
* empty capabilities for the new one. Default null.
* @return WP_Role|void WP_Role object, if the role is updated.
*/
function update_role( $role, $display_name = null, $capabilities = null ) {
return wp_roles()->update_role( $role, $display_name, $capabilities );
}
/**
* Removes a role, if it exists.
*

View File

@ -172,6 +172,76 @@ class WP_Roles {
return $this->role_objects[ $role ];
}
/**
* Updates an existing role. Creates a new role if it doesn't exist.
*
* Modifies the display name and/or capabilities for an existing role.
* If the role does not exist then a new role is created.
*
* The capabilities are defined in the following format: `array( 'read' => true )`.
* To explicitly deny the role a capability, set the value for that capability to false.
*
* @since 6.1.0
*
* @param string $role Role name.
* @param string|null $display_name Optional. Role display name. If null, the display name
* is not modified. Default null.
* @param bool[]|null $capabilities Optional. List of capabilities keyed by the capability name,
* e.g. `array( 'edit_posts' => true, 'delete_posts' => false )`.
* If null, don't alter capabilities for the existing role and make
* empty capabilities for the new one. Default null.
* @return WP_Role|void WP_Role object, if the role is updated.
*/
public function update_role( $role, $display_name = null, $capabilities = null ) {
if ( ! is_string( $role ) || '' === trim( $role ) ) {
return;
}
if ( null !== $display_name && ( ! is_string( $display_name ) || '' === trim( $display_name ) ) ) {
return;
}
if ( null !== $capabilities && ! is_array( $capabilities ) ) {
return;
}
if ( null === $display_name && null === $capabilities ) {
if ( isset( $this->role_objects[ $role ] ) ) {
return $this->role_objects[ $role ];
}
return;
}
if ( null === $display_name ) {
if ( ! isset( $this->role_objects[ $role ] ) ) {
return;
}
$display_name = $this->roles[ $role ]['name'];
}
if ( null === $capabilities ) {
if ( isset( $this->role_objects[ $role ] ) ) {
$capabilities = $this->role_objects[ $role ]->capabilities;
} else {
$capabilities = array();
}
}
if ( isset( $this->roles[ $role ] ) ) {
if ( null === $capabilities ) {
$capabilities = $this->role_objects[ $role ]->capabilities;
}
unset( $this->role_objects[ $role ] );
unset( $this->role_names[ $role ] );
unset( $this->roles[ $role ] );
}
// The roles database option will be updated in ::add_role().
return $this->add_role( $role, $display_name, $capabilities );
}
/**
* Removes a role by name.
*

View File

@ -998,6 +998,461 @@ class Tests_User_Capabilities extends WP_UnitTestCase {
$this->assertFalse( $wp_roles->is_role( $role_name ) );
}
/**
* @dataProvider data_update_role_unhappy_paths
*
* @ticket 54572
*
* @covers WP_Roles::update_role
* @covers ::update_role
*
* @param mixed $role The role to update.
* @param mixed $display_name The display name for the role.
* @param mixed $capabilities The capabilities for the role.
*/
public function test_update_role_unhappy_paths( $role, $display_name, $capabilities ) {
global $wp_roles;
// Create role if it does not exist.
$role_name = 'janitor';
$expected_caps = array(
'edit_posts' => true,
'edit_pages' => true,
'level_0' => true,
'level_1' => true,
'level_2' => true,
);
add_role( $role_name, 'Janitor', $expected_caps );
$this->flush_roles();
$this->assertTrue(
$wp_roles->is_role( $role_name ),
"The $role_name role was not created"
);
$this->assertNotInstanceOf(
'WP_Role',
update_role( $role, $display_name, $capabilities ),
"The $role_name role was updated"
);
// Clean up.
remove_role( $role_name );
$this->flush_roles();
$this->assertFalse(
$wp_roles->is_role( $role_name ),
"The $role_name role was not removed"
);
}
/**
* Data provider.
*
* @return array
*/
public function data_update_role_unhappy_paths() {
return array(
'true as the role' => array(
'role' => true,
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'false as the role' => array(
'role' => false,
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'null as the role' => array(
'role' => null,
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'(int) 1 as the role' => array(
'role' => 1,
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'(float) 1.0 as the role' => array(
'role' => 1.0,
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'(int) 0 as the role' => array(
'role' => 0,
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'(float) 0.0 as the role' => array(
'role' => 0.0,
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'an empty string as the role' => array(
'role' => '',
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'a string with only a space as the role' => array(
'role' => ' ',
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'an empty array as the role' => array(
'role' => array(),
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'a non-empty array as the role' => array(
'role' => array( 'janitor' ),
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'an object as the role' => array(
'role' => (object) array( 'janitor' ),
'display_name' => 'Janitor',
'capabilities' => array(
'level_1' => true,
),
),
'true as the display name' => array(
'role' => 'janitor',
'display_name' => true,
'capabilities' => array(
'level_1' => true,
),
),
'false as the display name' => array(
'role' => 'janitor',
'display_name' => false,
'capabilities' => array(
'level_1' => true,
),
),
'(int) 1 as the display name' => array(
'role' => 'janitor',
'display_name' => 1,
'capabilities' => array(
'level_1' => true,
),
),
'(float) 1.0 as the display name' => array(
'role' => 'janitor',
'display_name' => 1.0,
'capabilities' => array(
'level_1' => true,
),
),
'(int) 0 as the display name' => array(
'role' => 'janitor',
'display_name' => 0,
'capabilities' => array(
'level_1' => true,
),
),
'(float) 0.0 as the display name' => array(
'role' => 'janitor',
'display_name' => 0.0,
'capabilities' => array(
'level_1' => true,
),
),
'an empty string as the display name' => array(
'role' => 'janitor',
'display_name' => '',
'capabilities' => array(
'level_1' => true,
),
),
'a string with only a space as the display name' => array(
'role' => 'janitor',
'display_name' => ' ',
'capabilities' => array(
'level_1' => true,
),
),
'an empty array as the display name' => array(
'role' => 'janitor',
'display_name' => array(),
'capabilities' => array(
'level_1' => true,
),
),
'a non-empty array as the display name' => array(
'role' => 'janitor',
'display_name' => array( 'Janitor' ),
'capabilities' => array(
'level_1' => true,
),
),
'an object as the display name' => array(
'role' => 'janitor',
'display_name' => (object) array( 'Janitor' ),
'capabilities' => array(
'level_1' => true,
),
),
'true as the capabilities' => array(
'role' => 'janitor',
'display_name' => 'Janitor',
'capabilities' => true,
),
'false as the capabilities' => array(
'role' => (object) array( 'janitor' ),
'display_name' => 'Janitor',
'capabilities' => false,
),
'(int) 1 as the capabilities' => array(
'role' => 'janitor',
'display_name' => 'Janitor',
'capabilities' => 1,
),
'(float) 1.0 as the capabilities' => array(
'role' => 'janitor',
'display_name' => 'Janitor',
'capabilities' => 1.0,
),
'(int) 0 as the capabilities' => array(
'role' => 'janitor',
'display_name' => 'Janitor',
'capabilities' => 0,
),
'(float) 0.0 as the capabilities' => array(
'role' => 'janitor',
'display_name' => 'Janitor',
'capabilities' => 0.0,
),
'an empty string as the capabilities' => array(
'role' => 'janitor',
'display_name' => 'Janitor',
'capabilities' => '',
),
'a string with only a space as the capabilities' => array(
'role' => 'janitor',
'display_name' => 'Janitor',
'capabilities' => ' ',
),
'an object as the capabilities' => array(
'role' => 'janitor',
'display_name' => 'Janitor',
'capabilities' => (object) array( 'level_1' => true ),
),
);
}
/**
* @ticket 54572
*
* @covers WP_Roles::update_role
* @covers ::update_role
*/
public function test_update_role_should_create_a_role_if_it_does_not_exist() {
global $wp_roles;
// Create role if it does not exist.
$role_name = 'janitor';
$expected_caps = array(
'edit_posts' => true,
'edit_pages' => true,
'level_0' => true,
'level_1' => true,
'level_2' => true,
);
update_role( $role_name, 'Janitor', $expected_caps );
$this->flush_roles();
$this->assertTrue( $wp_roles->is_role( $role_name ) );
}
/**
* @ticket 54572
*
* @covers WP_Roles::update_role
* @covers ::update_role
*/
public function test_update_role_should_change_the_display_name_of_a_role() {
global $wp_roles;
$role_name = 'janitor';
add_role( $role_name, 'Janitor', array( 'level_1' => true ) );
$this->flush_roles();
$expected_display_name = 'Janitor Executive';
update_role( $role_name, $expected_display_name );
$this->flush_roles();
$this->assertSame(
$expected_display_name,
$wp_roles->roles[ $role_name ]['name'],
'The expected display name was not correct'
);
$this->assertSame(
array( 'level_1' => true ),
$wp_roles->roles[ $role_name ]['capabilities'],
'The expected capabilities were not correct'
);
// Clean up.
remove_role( $role_name );
$this->flush_roles();
$this->assertFalse(
$wp_roles->is_role( $role_name ),
"The $role_name role was not removed"
);
}
/**
* @dataProvider data_update_role_should_change_the_capabilities_of_a_role
*
* @ticket 54572
*
* @covers WP_Roles::update_role
* @covers ::update_role
*
* @param array $capabilities An array of capabilities.
*/
public function test_update_role_should_change_the_capabilities_of_a_role( $capabilities ) {
global $wp_roles;
$role_name = 'janitor';
add_role( $role_name, 'Janitor', array( 'level_1' => true ) );
$this->flush_roles();
update_role( $role_name, null, $capabilities );
$this->flush_roles();
$this->assertSame(
'Janitor',
$wp_roles->roles[ $role_name ]['name'],
'The display name was changed'
);
$this->assertSame(
$capabilities,
$wp_roles->roles[ $role_name ]['capabilities'],
'The expected capabilities were not correct'
);
// Clean up.
remove_role( $role_name );
$this->flush_roles();
$this->assertFalse(
$wp_roles->is_role( $role_name ),
"The $role_name role was not removed"
);
}
/**
* Data provider.
*
* @return array
*/
public function data_update_role_should_change_the_capabilities_of_a_role() {
return array(
'an empty array' => array( 'capabilities' => array() ),
'an array of capabilities' => array( 'capabilities' => array( 'level_2' => true ) ),
);
}
/**
* @ticket 54572
*
* @covers WP_Roles::update_role
* @covers ::update_role
*/
public function test_update_role_should_change_display_name_and_capabilities_of_role() {
global $wp_roles;
$role_name = 'janitor';
add_role( $role_name, 'Janitor', array( 'level_1' => true ) );
$this->flush_roles();
$expected_display_name = 'Janitor Manager';
$new_expected_caps = array(
'level_3' => true,
'level_4' => true,
);
update_role( $role_name, $expected_display_name, $new_expected_caps );
$this->flush_roles();
$this->assertSame(
$expected_display_name,
$wp_roles->roles[ $role_name ]['name'],
'The expected display name was not correct'
);
$this->assertSame(
$new_expected_caps,
$wp_roles->roles[ $role_name ]['capabilities'],
'The expected capabilities were not correct'
);
// Clean up.
remove_role( $role_name );
$this->flush_roles();
$this->assertFalse(
$wp_roles->is_role( $role_name ),
"The $role_name role was not removed"
);
}
/**
* @ticket 54572
*
* @covers WP_Roles::update_role
* @covers ::update_role
*/
public function test_update_role_should_change_capabilities_of_a_user() {
global $wp_roles;
$role_name = 'janitor';
add_role( $role_name, 'Janitor', array( 'level_1' => true ) );
$this->flush_roles();
// Assign a user to the role.
$id = self::factory()->user->create( array( 'role' => $role_name ) );
// Update empty capabilities.
update_role( $role_name, null, array( 'level_2' => true ) );
$this->flush_roles();
$this->assertTrue(
user_can( $id, 'level_2' ),
'The user does not have level_2 capabilities'
);
$this->assertFalse(
user_can( $id, 'level_1' ),
'The user has level_1 capabilities'
);
// Clean up.
remove_role( $role_name );
$this->flush_roles();
$this->assertFalse(
$wp_roles->is_role( $role_name ),
"The $role_name role was not removed"
);
}
/**
* Change the capabilites associated with a role and make sure the change
* is reflected in has_cap().