diff --git a/src/wp-includes/class-wp-user.php b/src/wp-includes/class-wp-user.php index a5312b664ba28..738fb7bdca94c 100644 --- a/src/wp-includes/class-wp-user.php +++ b/src/wp-includes/class-wp-user.php @@ -36,6 +36,9 @@ * @property string $rich_editing * @property string $syntax_highlighting * @property string $use_ssl + * @property array $caps + * @property array $roles + * @property array $allcaps */ #[AllowDynamicProperties] class WP_User { @@ -62,7 +65,7 @@ class WP_User { * @var bool[] Array of key/value pairs where keys represent a capability name * and boolean values represent whether the user has that capability. */ - public $caps = array(); + protected $caps = array(); /** * User metadata option name. @@ -78,7 +81,7 @@ class WP_User { * @since 2.0.0 * @var string[] */ - public $roles = array(); + protected $roles = array(); /** * All capabilities the user has, including individual and role based. @@ -87,7 +90,7 @@ class WP_User { * @var bool[] Array of key/value pairs where keys represent a capability name * and boolean values represent whether the user has that capability. */ - public $allcaps = array(); + protected $allcaps = array(); /** * The filter context applied to user data fields. @@ -105,6 +108,15 @@ class WP_User { */ private $site_id = 0; + + /** + * Flag for if capability is loaded. + * + * @since 6.8.0 + * @var bool + */ + private $loaded_caps = false; + /** * @since 3.3.0 * @var array @@ -287,6 +299,10 @@ public function __isset( $key ) { $key = 'ID'; } + if ( in_array( $key, array( 'caps', 'allcaps', 'roles' ), true ) ) { + return isset( $this->$key ); + } + if ( isset( $this->data->$key ) ) { return true; } @@ -320,6 +336,11 @@ public function __get( $key ) { return $this->ID; } + if ( in_array( $key, array( 'caps', 'allcaps', 'roles' ), true ) ) { + $this->load_capability_data(); + return $this->$key; + } + if ( isset( $this->data->$key ) ) { $value = $this->data->$key; } else { @@ -514,6 +535,10 @@ public function get_role_caps() { $wp_roles = wp_roles(); + if ( ! $this->loaded_caps ) { + $this->caps = $this->get_caps_data(); + } + // Filter out caps that are not role names and assign to $this->roles. if ( is_array( $this->caps ) ) { $this->roles = array_filter( array_keys( $this->caps ), array( $wp_roles, 'is_role' ) ); @@ -547,6 +572,7 @@ public function add_role( $role ) { if ( empty( $role ) ) { return; } + $this->load_capability_data(); if ( in_array( $role, $this->roles, true ) ) { return; @@ -576,6 +602,7 @@ public function add_role( $role ) { * @param string $role Role name. */ public function remove_role( $role ) { + $this->load_capability_data(); if ( ! in_array( $role, $this->roles, true ) ) { return; } @@ -608,6 +635,7 @@ public function remove_role( $role ) { * @param string $role Role name. */ public function set_role( $role ) { + $this->load_capability_data(); if ( 1 === count( $this->roles ) && current( $this->roles ) === $role ) { return; } @@ -709,6 +737,7 @@ public function update_user_level_from_caps() { * @param bool $grant Whether to grant capability to user. */ public function add_cap( $cap, $grant = true ) { + $this->load_capability_data(); $this->caps[ $cap ] = $grant; update_user_meta( $this->ID, $this->cap_key, $this->caps ); $this->get_role_caps(); @@ -723,6 +752,7 @@ public function add_cap( $cap, $grant = true ) { * @param string $cap Capability name. */ public function remove_cap( $cap ) { + $this->load_capability_data(); if ( ! isset( $this->caps[ $cap ] ) ) { return; } @@ -741,10 +771,10 @@ public function remove_cap( $cap ) { */ public function remove_all_caps() { global $wpdb; - $this->caps = array(); delete_user_meta( $this->ID, $this->cap_key ); delete_user_meta( $this->ID, $wpdb->get_blog_prefix() . 'user_level' ); - $this->get_role_caps(); + $this->loaded_caps = false; + $this->load_capability_data(); } /** @@ -775,6 +805,8 @@ public function remove_all_caps() { * the given capability for that object. */ public function has_cap( $cap, ...$args ) { + $this->load_capability_data(); + if ( is_numeric( $cap ) ) { _deprecated_argument( __FUNCTION__, '2.0.0', __( 'Usage of user levels is deprecated. Use capabilities instead.' ) ); $cap = $this->translate_level_to_cap( $cap ); @@ -875,11 +907,8 @@ public function for_site( $site_id = '' ) { $this->site_id = get_current_blog_id(); } - $this->cap_key = $wpdb->get_blog_prefix( $this->site_id ) . 'capabilities'; - - $this->caps = $this->get_caps_data(); - - $this->get_role_caps(); + $this->cap_key = $wpdb->get_blog_prefix( $this->site_id ) . 'capabilities'; + $this->loaded_caps = false; } /** @@ -910,4 +939,18 @@ private function get_caps_data() { return $caps; } + + /** + * Loads capability data if it has not been loaded yet. + * + * @since 6.8.0 + */ + private function load_capability_data() { + if ( $this->loaded_caps ) { + return; + } + $this->caps = $this->get_caps_data(); + $this->get_role_caps(); + $this->loaded_caps = true; + } } diff --git a/tests/phpunit/tests/user/capabilities.php b/tests/phpunit/tests/user/capabilities.php index 65942dca27544..10dfe3a3c8ba5 100644 --- a/tests/phpunit/tests/user/capabilities.php +++ b/tests/phpunit/tests/user/capabilities.php @@ -1077,6 +1077,41 @@ public function test_role_remove_cap() { $this->assertFalse( $wp_roles->is_role( $role_name ) ); } + /** + * @ticket 58001 + */ + public function test_get_role_caps() { + $id_1 = self::$users['contributor']->ID; + $user_1 = new WP_User( $id_1 ); + + $role_caps = $user_1->get_role_caps(); + $this->assertIsArray( $role_caps, 'User role capabilities should be an array' ); + $this->assertArrayHasKey( 'edit_posts', $role_caps, 'User role capabilities should contain the edit_posts capability' ); + } + + /** + * @ticket 58001 + */ + public function test_user_lazy_capabilities() { + $id_1 = self::$users['contributor']->ID; + $user_1 = new WP_User( $id_1 ); + + $this->assertTrue( isset( $user_1->roles ), 'User roles should be set' ); + $this->assertTrue( isset( $user_1->allcaps ), 'User all capabilities should be set' ); + $this->assertTrue( isset( $user_1->caps ), 'User capabilities should be set' ); + $this->assertIsArray( $user_1->roles, 'User roles should be an array' ); + $this->assertSame( array( 'contributor' ), $user_1->roles, 'User roles should match' ); + $this->assertIsArray( $user_1->allcaps, 'User allcaps should be an array' ); + $this->assertIsArray( $user_1->caps, 'User caps should be an array' ); + + $caps = $this->getAllCapsAndRoles(); + foreach ( $caps as $cap => $roles ) { + if ( in_array( 'contributor', $roles, true ) ) { + $this->assertTrue( $user_1->has_cap( $cap ), "User should have the {$cap} capability" ); + } + } + } + /** * Add an extra capability to a user. */ diff --git a/tests/phpunit/tests/user/multisite.php b/tests/phpunit/tests/user/multisite.php index 08d08bbe75bb3..93c09be9c4b88 100644 --- a/tests/phpunit/tests/user/multisite.php +++ b/tests/phpunit/tests/user/multisite.php @@ -370,12 +370,13 @@ public function test_add_user_to_blog_subscriber() { switch_to_blog( $site_id ); $user = get_user_by( 'id', $user_id ); + $this->assertContains( 'subscriber', $user->roles, 'User should have subscriber role' ); restore_current_blog(); wp_delete_site( $site_id ); wpmu_delete_user( $user_id ); - $this->assertContains( 'subscriber', $user->roles ); + $this->assertContains( 'subscriber', $user->roles, 'User should still have subscriber role' ); } /** diff --git a/tests/phpunit/tests/user/query.php b/tests/phpunit/tests/user/query.php index 887064b8ca8de..0c2317a400204 100644 --- a/tests/phpunit/tests/user/query.php +++ b/tests/phpunit/tests/user/query.php @@ -162,13 +162,18 @@ public function test_get_all_primed_users() { $filter = new MockAction(); add_filter( 'update_user_metadata_cache', array( $filter, 'filter' ), 10, 2 ); - new WP_User_Query( + $query = new WP_User_Query( array( 'include' => self::$author_ids, 'fields' => 'all', ) ); + $users = $query->get_results(); + foreach ( $users as $user ) { + $user->roles; + } + $args = $filter->get_args(); $last_args = end( $args ); $this->assertIsArray( $last_args[1] );