Skip to content

Commit 2de7ed3

Browse files
committed
Users: Lazy load user capabilities in WP_User object.
Convert the WP_User object properties caps, roles, and allcaps to protected, and introduce lazy loading for capabilities. These properties are now populated only when first accessed. The existing magic methods (__get, __set, and __unset) have been updated to maintain backward compatibility, ensuring that reading or modifying these formerly public properties continues to work as expected. Ensure that these properties are initialised when calling remove_all_caps(), remove_cap(), has_cap(), add_role(), and set_role() methods. Props spacedmonkey, flixos90, peterwilsoncc, mukesh27, westonruter, swissspidy, prettyboymp. Fixes #58001. git-svn-id: https://develop.svn.wordpress.org/trunk@60915 602fd350-edb4-49c9-b593-d223f7449a82
1 parent e11e9f2 commit 2de7ed3

File tree

4 files changed

+140
-15
lines changed

4 files changed

+140
-15
lines changed

src/wp-includes/class-wp-user.php

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
* @property string $rich_editing
3838
* @property string $syntax_highlighting
3939
* @property string $use_ssl
40+
* @property array<string, bool> $caps
41+
* @property string[] $roles
42+
* @property array<string, bool> $allcaps
4043
*/
4144
#[AllowDynamicProperties]
4245
class WP_User {
@@ -60,10 +63,10 @@ class WP_User {
6063
* Capabilities that the individual user has been granted outside of those inherited from their role.
6164
*
6265
* @since 2.0.0
63-
* @var bool[] Array of key/value pairs where keys represent a capability name
64-
* and boolean values represent whether the user has that capability.
66+
* @var array<string, bool>|null Array of key/value pairs where keys represent a capability name
67+
* and boolean values represent whether the user has that capability.
6568
*/
66-
public $caps = array();
69+
protected $caps = null;
6770

6871
/**
6972
* User metadata option name.
@@ -79,16 +82,16 @@ class WP_User {
7982
* @since 2.0.0
8083
* @var string[]
8184
*/
82-
public $roles = array();
85+
protected $roles = array();
8386

8487
/**
8588
* All capabilities the user has, including individual and role based.
8689
*
8790
* @since 2.0.0
88-
* @var bool[] Array of key/value pairs where keys represent a capability name
89-
* and boolean values represent whether the user has that capability.
91+
* @var array<string, bool> Array of key/value pairs where keys represent a capability name
92+
* and boolean values represent whether the user has that capability.
9093
*/
91-
public $allcaps = array();
94+
protected $allcaps = array();
9295

9396
/**
9497
* The filter context applied to user data fields.
@@ -288,6 +291,10 @@ public function __isset( $key ) {
288291
$key = 'ID';
289292
}
290293

294+
if ( in_array( $key, array( 'caps', 'allcaps', 'roles' ), true ) ) {
295+
return true;
296+
}
297+
291298
if ( isset( $this->data->$key ) ) {
292299
return true;
293300
}
@@ -321,6 +328,11 @@ public function __get( $key ) {
321328
return $this->ID;
322329
}
323330

331+
if ( in_array( $key, array( 'caps', 'allcaps', 'roles' ), true ) ) {
332+
$this->load_capability_data();
333+
return $this->$key;
334+
}
335+
324336
if ( isset( $this->data->$key ) ) {
325337
$value = $this->data->$key;
326338
} else {
@@ -363,6 +375,13 @@ public function __set( $key, $value ) {
363375
return;
364376
}
365377

378+
// Ensure capability data is loaded before setting related properties.
379+
if ( in_array( $key, array( 'caps', 'allcaps', 'roles' ), true ) ) {
380+
$this->load_capability_data();
381+
$this->$key = $value;
382+
return;
383+
}
384+
366385
$this->data->$key = $value;
367386
}
368387

@@ -386,6 +405,10 @@ public function __unset( $key ) {
386405
);
387406
}
388407

408+
if ( in_array( $key, array( 'caps', 'allcaps', 'roles' ), true ) ) {
409+
$this->$key = null;
410+
}
411+
389412
if ( isset( $this->data->$key ) ) {
390413
unset( $this->data->$key );
391414
}
@@ -515,6 +538,11 @@ public function get_role_caps() {
515538

516539
$wp_roles = wp_roles();
517540

541+
// Edge case: In case someone calls this method before lazy initialization, we need to initialize on demand.
542+
if ( ! isset( $this->caps ) ) {
543+
$this->caps = $this->get_caps_data();
544+
}
545+
518546
// Filter out caps that are not role names and assign to $this->roles.
519547
if ( is_array( $this->caps ) ) {
520548
$this->roles = array_filter( array_keys( $this->caps ), array( $wp_roles, 'is_role' ) );
@@ -548,6 +576,7 @@ public function add_role( $role ) {
548576
if ( empty( $role ) ) {
549577
return;
550578
}
579+
$this->load_capability_data();
551580

552581
if ( in_array( $role, $this->roles, true ) ) {
553582
return;
@@ -577,6 +606,7 @@ public function add_role( $role ) {
577606
* @param string $role Role name.
578607
*/
579608
public function remove_role( $role ) {
609+
$this->load_capability_data();
580610
if ( ! in_array( $role, $this->roles, true ) ) {
581611
return;
582612
}
@@ -609,6 +639,7 @@ public function remove_role( $role ) {
609639
* @param string $role Role name.
610640
*/
611641
public function set_role( $role ) {
642+
$this->load_capability_data();
612643
if ( 1 === count( $this->roles ) && current( $this->roles ) === $role ) {
613644
return;
614645
}
@@ -710,6 +741,7 @@ public function update_user_level_from_caps() {
710741
* @param bool $grant Whether to grant capability to user.
711742
*/
712743
public function add_cap( $cap, $grant = true ) {
744+
$this->load_capability_data();
713745
$this->caps[ $cap ] = $grant;
714746
update_user_meta( $this->ID, $this->cap_key, $this->caps );
715747
$this->get_role_caps();
@@ -724,6 +756,7 @@ public function add_cap( $cap, $grant = true ) {
724756
* @param string $cap Capability name.
725757
*/
726758
public function remove_cap( $cap ) {
759+
$this->load_capability_data();
727760
if ( ! isset( $this->caps[ $cap ] ) ) {
728761
return;
729762
}
@@ -742,10 +775,10 @@ public function remove_cap( $cap ) {
742775
*/
743776
public function remove_all_caps() {
744777
global $wpdb;
745-
$this->caps = array();
778+
$this->caps = null;
746779
delete_user_meta( $this->ID, $this->cap_key );
747780
delete_user_meta( $this->ID, $wpdb->get_blog_prefix() . 'user_level' );
748-
$this->get_role_caps();
781+
$this->load_capability_data();
749782
}
750783

751784
/**
@@ -776,6 +809,8 @@ public function remove_all_caps() {
776809
* the given capability for that object.
777810
*/
778811
public function has_cap( $cap, ...$args ) {
812+
$this->load_capability_data();
813+
779814
if ( is_numeric( $cap ) ) {
780815
_deprecated_argument( __FUNCTION__, '2.0.0', __( 'Usage of user levels is deprecated. Use capabilities instead.' ) );
781816
$cap = $this->translate_level_to_cap( $cap );
@@ -877,10 +912,7 @@ public function for_site( $site_id = 0 ) {
877912
}
878913

879914
$this->cap_key = $wpdb->get_blog_prefix( $this->site_id ) . 'capabilities';
880-
881-
$this->caps = $this->get_caps_data();
882-
883-
$this->get_role_caps();
915+
$this->caps = null;
884916
}
885917

886918
/**
@@ -911,4 +943,17 @@ private function get_caps_data() {
911943

912944
return $caps;
913945
}
946+
947+
/**
948+
* Loads capability data if it has not been loaded yet.
949+
*
950+
* @since 6.9.0
951+
*/
952+
private function load_capability_data() {
953+
if ( isset( $this->caps ) ) {
954+
return;
955+
}
956+
$this->caps = $this->get_caps_data();
957+
$this->get_role_caps();
958+
}
914959
}

tests/phpunit/tests/user/capabilities.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,42 @@ public function test_add_role() {
992992
$this->assertFalse( $wp_roles->is_role( $role_name ) );
993993
}
994994

995+
/**
996+
* Test adding capabilities, roles, and allcaps manually to a user.
997+
*
998+
* @ticket 58001
999+
*
1000+
* @dataProvider data_add_user_properties_manually
1001+
*
1002+
* @param string $property_name The property name to set.
1003+
* @param array $property_value The property value to set.
1004+
* @param bool $check_null Whether to check that the property is null after unsetting it.
1005+
*/
1006+
public function test_add_user_properties_manually( $property_name, $property_value, $check_null ) {
1007+
$id = self::factory()->user->create();
1008+
$user = new WP_User( $id );
1009+
$user->{$property_name} = $property_value;
1010+
1011+
$this->assertSameSets( $property_value, $user->{$property_name}, "User property {$property_name} was not set correctly." );
1012+
unset( $user->{$property_name} );
1013+
if ( $check_null ) {
1014+
$this->assertNull( $user->{$property_name}, "User property {$property_name} should be null after unsetting it." );
1015+
}
1016+
}
1017+
1018+
/**
1019+
* Data provider for test_add_user_properties_manually.
1020+
*
1021+
* @return array<string, array{0:string,1:array}>
1022+
*/
1023+
public function data_add_user_properties_manually() {
1024+
return array(
1025+
'caps' => array( 'caps', array( 'foo' => true ), false ),
1026+
'roles' => array( 'roles', array( 'foo' => true ), true ),
1027+
'allcaps' => array( 'allcaps', array( 'foo' => true ), true ),
1028+
);
1029+
}
1030+
9951031
/**
9961032
* Test add_role with implied capabilities grant successfully grants capabilities.
9971033
*
@@ -1100,6 +1136,41 @@ public function test_role_remove_cap() {
11001136
$this->assertFalse( $wp_roles->is_role( $role_name ) );
11011137
}
11021138

1139+
/**
1140+
* @ticket 58001
1141+
*/
1142+
public function test_get_role_caps() {
1143+
$id_1 = self::$users['contributor']->ID;
1144+
$user_1 = new WP_User( $id_1 );
1145+
1146+
$role_caps = $user_1->get_role_caps();
1147+
$this->assertIsArray( $role_caps, 'User role capabilities should be an array' );
1148+
$this->assertArrayHasKey( 'edit_posts', $role_caps, 'User role capabilities should contain the edit_posts capability' );
1149+
}
1150+
1151+
/**
1152+
* @ticket 58001
1153+
*/
1154+
public function test_user_lazy_capabilities() {
1155+
$id_1 = self::$users['contributor']->ID;
1156+
$user_1 = new WP_User( $id_1 );
1157+
1158+
$this->assertTrue( isset( $user_1->roles ), 'User roles should be set' );
1159+
$this->assertTrue( isset( $user_1->allcaps ), 'User all capabilities should be set' );
1160+
$this->assertTrue( isset( $user_1->caps ), 'User capabilities should be set' );
1161+
$this->assertIsArray( $user_1->roles, 'User roles should be an array' );
1162+
$this->assertSame( array( 'contributor' ), $user_1->roles, 'User roles should match' );
1163+
$this->assertIsArray( $user_1->allcaps, 'User allcaps should be an array' );
1164+
$this->assertIsArray( $user_1->caps, 'User caps should be an array' );
1165+
1166+
$caps = $this->getAllCapsAndRoles();
1167+
foreach ( $caps as $cap => $roles ) {
1168+
if ( in_array( 'contributor', $roles, true ) ) {
1169+
$this->assertTrue( $user_1->has_cap( $cap ), "User should have the {$cap} capability" );
1170+
}
1171+
}
1172+
}
1173+
11031174
/**
11041175
* Add an extra capability to a user.
11051176
*/

tests/phpunit/tests/user/multisite.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,12 +369,13 @@ public function test_add_user_to_blog_subscriber() {
369369

370370
switch_to_blog( $site_id );
371371
$user = get_user_by( 'id', $user_id );
372+
$this->assertContains( 'subscriber', $user->roles, 'User should have subscriber role' );
372373
restore_current_blog();
373374

374375
wp_delete_site( $site_id );
375376
wpmu_delete_user( $user_id );
376377

377-
$this->assertContains( 'subscriber', $user->roles );
378+
$this->assertContains( 'subscriber', $user->roles, 'User should still have subscriber role' );
378379
}
379380

380381
/**

tests/phpunit/tests/user/query.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,21 @@ public function test_get_all_primed_users() {
162162
$filter = new MockAction();
163163
add_filter( 'update_user_metadata_cache', array( $filter, 'filter' ), 10, 2 );
164164

165-
new WP_User_Query(
165+
$query = new WP_User_Query(
166166
array(
167167
'include' => self::$author_ids,
168168
'fields' => 'all',
169169
)
170170
);
171171

172+
$users = $query->get_results();
173+
foreach ( $users as $user ) {
174+
$this->assertIsArray( $user->roles );
175+
foreach ( $user->roles as $role ) {
176+
$this->assertIsString( $role );
177+
}
178+
}
179+
172180
$args = $filter->get_args();
173181
$last_args = end( $args );
174182
$this->assertIsArray( $last_args[1] );

0 commit comments

Comments
 (0)