WordPress.org

Make WordPress Core

Changeset 41624


Ignore:
Timestamp:
09/27/2017 09:09:11 PM (2 years ago)
Author:
flixos90
Message:

Multisite: Initialize a user's roles correctly when setting them up for a different site.

While it has always been possible to initialize a user's roles and capabilities for another site than the current one in a multisite, the actual roles available were not switched prior to this change, possibly causing invalid roles to show up or actually valid capabilities not being available.

In order to fix this bug in a clean way, relevant parts of the WP_User class have been refactored. The ID of the site for which capabilities are currently initialized are now stored in a private property WP_User::$site_id. The WP_User::for_blog( $blog_id ) and WP_User::_init_caps( $cap_key ) methods have been deprecated in favor of WP_User::for_site( $site_id ). In addition, a new method WP_User::get_site_id() has been introduced to retrieve the site ID for which the user's capabilities are currently initialized.

Props ryanduff, jeremyfelt, flixos90.
Fixes #36961.

Location:
trunk
Files:
4 edited

Legend:

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

    r41376 r41624  
    9494
    9595    /**
     96     * The site ID the capabilities of this user are initialized for.
     97     *
     98     * @since 4.9.0
     99     * @var int
     100     */
     101    private $site_id = 0;
     102
     103    /**
    96104     * @static
    97105     * @since 3.3.0
     
    111119     * @param int|string|stdClass|WP_User $id User's ID, a WP_User object, or a user object from the DB.
    112120     * @param string $name Optional. User's username
    113      * @param int $blog_id Optional Site ID, defaults to current site.
    114      */
    115     public function __construct( $id = 0, $name = '', $blog_id = '' ) {
     121     * @param int $site_id Optional Site ID, defaults to current site.
     122     */
     123    public function __construct( $id = 0, $name = '', $site_id = '' ) {
    116124        if ( ! isset( self::$back_compat_keys ) ) {
    117125            $prefix = $GLOBALS['wpdb']->prefix;
     
    127135
    128136        if ( $id instanceof WP_User ) {
    129             $this->init( $id->data, $blog_id );
     137            $this->init( $id->data, $site_id );
    130138            return;
    131139        } elseif ( is_object( $id ) ) {
    132             $this->init( $id, $blog_id );
     140            $this->init( $id, $site_id );
    133141            return;
    134142        }
     
    146154
    147155        if ( $data ) {
    148             $this->init( $data, $blog_id );
     156            $this->init( $data, $site_id );
    149157        } else {
    150158            $this->data = new stdClass;
     
    158166     *
    159167     * @param object $data    User DB row object.
    160      * @param int    $blog_id Optional. The site ID to initialize for.
    161      */
    162     public function init( $data, $blog_id = '' ) {
     168     * @param int    $site_id Optional. The site ID to initialize for.
     169     */
     170    public function init( $data, $site_id = '' ) {
    163171        $this->data = $data;
    164172        $this->ID = (int) $data->ID;
    165173
    166         $this->for_blog( $blog_id );
     174        $this->for_site( $site_id );
    167175    }
    168176
     
    242250
    243251    /**
    244      * Makes private/protected methods readable for backward compatibility.
    245      *
    246      * @since 4.3.0
    247      *
    248      * @param callable $name      Method to call.
    249      * @param array    $arguments Arguments to pass when calling.
    250      * @return mixed|false Return value of the callback, false otherwise.
    251      */
    252     public function __call( $name, $arguments ) {
    253         if ( '_init_caps' === $name ) {
    254             return call_user_func_array( array( $this, $name ), $arguments );
    255         }
    256         return false;
    257     }
    258 
    259     /**
    260252     * Magic method for checking the existence of a certain custom field.
    261253     *
     
    426418
    427419    /**
     420     * Makes private/protected methods readable for backward compatibility.
     421     *
     422     * @since 4.3.0
     423     *
     424     * @param callable $name      Method to call.
     425     * @param array    $arguments Arguments to pass when calling.
     426     * @return mixed|false Return value of the callback, false otherwise.
     427     */
     428    public function __call( $name, $arguments ) {
     429        if ( '_init_caps' === $name ) {
     430            return call_user_func_array( array( $this, $name ), $arguments );
     431        }
     432        return false;
     433    }
     434
     435    /**
    428436     * Set up capability object properties.
    429437     *
     
    434442     *
    435443     * @since 2.1.0
     444     * @deprecated 4.9.0 Use WP_User::for_site()
    436445     *
    437446     * @global wpdb $wpdb WordPress database abstraction object.
     
    442451        global $wpdb;
    443452
    444         if ( empty($cap_key) )
    445             $this->cap_key = $wpdb->get_blog_prefix() . 'capabilities';
    446         else
     453        _deprecated_function( __METHOD__, '4.9.0', 'WP_User::for_site()' );
     454
     455        if ( empty( $cap_key ) ) {
     456            $this->cap_key = $wpdb->get_blog_prefix( $this->site_id ) . 'capabilities';
     457        } else {
    447458            $this->cap_key = $cap_key;
    448 
    449         $this->caps = get_user_meta( $this->ID, $this->cap_key, true );
    450 
    451         if ( ! is_array( $this->caps ) )
    452             $this->caps = array();
     459        }
     460
     461        $this->caps = $this->get_caps_data();
    453462
    454463        $this->get_role_caps();
     
    468477     */
    469478    public function get_role_caps() {
     479        $switch_site = false;
     480        if ( is_multisite() && $this->site_id != get_current_blog_id() ) {
     481            $switch_site = true;
     482
     483            switch_to_blog( $this->site_id );
     484        }
     485
    470486        $wp_roles = wp_roles();
    471487
     
    481497        }
    482498        $this->allcaps = array_merge( (array) $this->allcaps, (array) $this->caps );
     499
     500        if ( $switch_site ) {
     501            restore_current_blog();
     502        }
    483503
    484504        return $this->allcaps;
     
    755775     *
    756776     * @since 3.0.0
     777     * @deprecated 4.9.0 Use WP_User::for_site()
    757778     *
    758779     * @global wpdb $wpdb WordPress database abstraction object.
     
    761782     */
    762783    public function for_blog( $blog_id = '' ) {
     784        _deprecated_function( __METHOD__, '4.9.0', 'WP_User::for_site()' );
     785
     786        $this->for_site( $blog_id );
     787    }
     788
     789    /**
     790     * Sets the site to operate on. Defaults to the current site.
     791     *
     792     * @since 4.9.0
     793     *
     794     * @global wpdb $wpdb WordPress database abstraction object.
     795     *
     796     * @param int $site_id Site ID to initialize user capabilities for. Default is the current site.
     797     */
     798    public function for_site( $site_id = '' ) {
    763799        global $wpdb;
    764         if ( ! empty( $blog_id ) )
    765             $cap_key = $wpdb->get_blog_prefix( $blog_id ) . 'capabilities';
    766         else
    767             $cap_key = '';
    768         $this->_init_caps( $cap_key );
     800
     801        if ( ! empty( $site_id ) ) {
     802            $this->site_id = absint( $site_id );
     803        } else {
     804            $this->site_id = get_current_blog_id();
     805        }
     806
     807        $this->cap_key = $wpdb->get_blog_prefix( $this->site_id ) . 'capabilities';
     808
     809        $this->caps = $this->get_caps_data();
     810
     811        $this->get_role_caps();
     812    }
     813
     814    /**
     815     * Gets the ID of the site for which the user's capabilities are currently initialized.
     816     *
     817     * @since 4.9.0
     818     *
     819     * @return int Site ID.
     820     */
     821    public function get_site_id() {
     822        return $this->site_id;
     823    }
     824
     825    /**
     826     * Gets the available user capabilities data.
     827     *
     828     * @since 4.9.0
     829     *
     830     * @return array User capabilities array.
     831     */
     832    private function get_caps_data() {
     833        $caps = get_user_meta( $this->ID, $this->cap_key, true );
     834
     835        if ( ! is_array( $caps ) ) {
     836            return array();
     837        }
     838
     839        return $caps;
    769840    }
    770841}
  • trunk/src/wp-includes/ms-blogs.php

    r41373 r41624  
    851851        $wp_roles = new WP_Roles();
    852852        $current_user = wp_get_current_user();
    853         $current_user->for_blog( $new_blog );
     853        $current_user->for_site( $new_blog );
    854854    }
    855855
     
    925925        $wp_roles = new WP_Roles();
    926926        $current_user = wp_get_current_user();
    927         $current_user->for_blog( $blog );
     927        $current_user->for_site( $blog );
    928928    }
    929929
  • trunk/tests/phpunit/tests/user.php

    r41376 r41624  
    181181        $this->assertEquals( 'foo', $user->data->$key );  // This will fail with WP < 3.3
    182182
    183         foreach ( (array) $user as $key => $value ) {
     183        foreach ( get_object_vars( $user ) as $key => $value ) {
    184184            $this->assertEquals( $value, $user->$key );
    185185        }
  • trunk/tests/phpunit/tests/user/capabilities.php

    r41290 r41624  
    18491849        $this->assertFalse( user_can( self::$users['subscriber']->ID,    'remove_user', self::$users['subscriber']->ID ) );
    18501850    }
     1851
     1852    /**
     1853     * @ticket 36961
     1854     * @group ms-required
     1855     */
     1856    function test_init_user_caps_for_different_site() {
     1857        global $wpdb;
     1858
     1859        $site_id = self::factory()->blog->create( array( 'user_id' => self::$users['administrator']->ID ) );
     1860
     1861        switch_to_blog( $site_id );
     1862
     1863        $role_name = 'uploader';
     1864        add_role( $role_name, 'Uploader', array(
     1865            'read'         => true,
     1866            'upload_files' => true,
     1867        ) );
     1868        add_user_to_blog( $site_id, self::$users['subscriber']->ID, $role_name );
     1869
     1870        restore_current_blog();
     1871
     1872        $user = new WP_User( self::$users['subscriber']->ID, '', $site_id );
     1873        $this->assertTrue( $user->has_cap( 'upload_files' ) );
     1874    }
     1875
     1876    /**
     1877     * @ticket 36961
     1878     * @group ms-required
     1879     */
     1880    function test_init_user_caps_for_different_site_by_user_switch() {
     1881        global $wpdb;
     1882
     1883        $user = new WP_User( self::$users['subscriber']->ID );
     1884
     1885        $site_id = self::factory()->blog->create( array( 'user_id' => self::$users['administrator']->ID ) );
     1886
     1887        switch_to_blog( $site_id );
     1888
     1889        $role_name = 'uploader';
     1890        add_role( $role_name, 'Uploader', array(
     1891            'read'         => true,
     1892            'upload_files' => true,
     1893        ) );
     1894        add_user_to_blog( $site_id, self::$users['subscriber']->ID, $role_name );
     1895
     1896        restore_current_blog();
     1897
     1898        $user->for_site( $site_id );
     1899        $this->assertTrue( $user->has_cap( 'upload_files' ) );
     1900    }
     1901
     1902    /**
     1903     * @ticket 36961
     1904     */
     1905    function test_get_caps_data() {
     1906        global $wpdb;
     1907
     1908        $custom_caps = array(
     1909            'do_foo' => true,
     1910            'do_bar' => false,
     1911        );
     1912
     1913        // Test `WP_User::get_caps_data()` by manually setting capabilities metadata.
     1914        update_user_meta( self::$users['subscriber']->ID, $wpdb->get_blog_prefix( get_current_blog_id() ) . 'capabilities', $custom_caps );
     1915
     1916        $user = new WP_User( self::$users['subscriber']->ID );
     1917        $this->assertSame( $custom_caps, $user->caps );
     1918    }
     1919
     1920    /**
     1921     * @ticket 36961
     1922     */
     1923    function test_user_get_site_id_default() {
     1924        $user = new WP_User( self::$users['subscriber']->ID );
     1925        $this->assertSame( get_current_blog_id(), $user->get_site_id() );
     1926    }
     1927
     1928    /**
     1929     * @ticket 36961
     1930     */
     1931    function test_user_get_site_id() {
     1932        global $wpdb;
     1933
     1934        // Suppressing errors here allows to get around creating an actual site,
     1935        // which is unnecessary for this test.
     1936        $suppress = $wpdb->suppress_errors();
     1937        $user = new WP_User( self::$users['subscriber']->ID, '', 333 );
     1938        $wpdb->suppress_errors( $suppress );
     1939
     1940        $this->assertSame( 333, $user->get_site_id() );
     1941    }
    18511942}
Note: See TracChangeset for help on using the changeset viewer.