Make WordPress Core

Changeset 60915

Timestamp:
10/08/2025 07:59:28 PM (2 days ago)
Author:
spacedmonkey
Message:

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.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-user.php

    r60174 r60915  
    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]
     
    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.
    65      */
    66     public $caps = array();
     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.
     68     */
     69    protected $caps = null;
    6770
    6871    /**
     
    8083     * @var string[]
    8184     */
    82     public $roles = array();
     85    protected $roles = array();
    8386
    8487    /**
     
    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.
    90      */
    91     public $allcaps = array();
     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.
     93     */
     94    protected $allcaps = array();
    9295
    9396    /**
     
    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;
     
    320327            );
    321328            return $this->ID;
     329        }
     330
     331        if ( in_array( $key, array( 'caps', 'allcaps', 'roles' ), true ) ) {
     332            $this->load_capability_data();
     333            return $this->$key;
    322334        }
    323335
     
    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    }
     
    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 );
     
    515538
    516539        $wp_roles = wp_roles();
     540
     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        }
    517545
    518546        // Filter out caps that are not role names and assign to $this->roles.
     
    549577            return;
    550578        }
     579        $this->load_capability_data();
    551580
    552581        if ( in_array( $role, $this->roles, true ) ) {
     
    578607     */
    579608    public function remove_role( $role ) {
     609        $this->load_capability_data();
    580610        if ( ! in_array( $role, $this->roles, true ) ) {
    581611            return;
     
    610640     */
    611641    public function set_role( $role ) {
     642        $this->load_capability_data();
    612643        if ( 1 === count( $this->roles ) && current( $this->roles ) === $role ) {
    613644            return;
     
    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 );
     
    725757     */
    726758    public function remove_cap( $cap ) {
     759        $this->load_capability_data();
    727760        if ( ! isset( $this->caps[ $cap ] ) ) {
    728761            return;
     
    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
     
    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.' ) );
     
    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
     
    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}
  • trunk/tests/phpunit/tests/user/capabilities.php

    r60614 r60915  
    994994
    995995    /**
     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
     1031    /**
    9961032     * Test add_role with implied capabilities grant successfully grants capabilities.
    9971033     *
     
    10991135        $this->flush_roles();
    11001136        $this->assertFalse( $wp_roles->is_role( $role_name ) );
     1137    }
     1138
     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        }
    11011172    }
    11021173
  • trunk/tests/phpunit/tests/user/multisite.php

    r60148 r60915  
    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
     
    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
  • trunk/tests/phpunit/tests/user/query.php

    r57750 r60915  
    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,
     
    169169            )
    170170        );
     171
     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        }
    171179
    172180        $args      = $filter->get_args();
Note: See TracChangeset for help on using the changeset viewer.