diff --git a/src/wp-admin/includes/class-wp-users-list-table.php b/src/wp-admin/includes/class-wp-users-list-table.php index 852552c703..cf530057f0 100644 --- a/src/wp-admin/includes/class-wp-users-list-table.php +++ b/src/wp-admin/includes/class-wp-users-list-table.php @@ -87,13 +87,23 @@ class WP_Users_List_Table extends WP_List_Table { $paged = $this->get_pagenum(); - $args = array( - 'number' => $users_per_page, - 'offset' => ( $paged-1 ) * $users_per_page, - 'role' => $role, - 'search' => $usersearch, - 'fields' => 'all_with_meta' - ); + if ( 'none' === $role ) { + $args = array( + 'number' => $users_per_page, + 'offset' => ( $paged-1 ) * $users_per_page, + 'include' => wp_get_users_with_no_role(), + 'search' => $usersearch, + 'fields' => 'all_with_meta' + ); + } else { + $args = array( + 'number' => $users_per_page, + 'offset' => ( $paged-1 ) * $users_per_page, + 'role' => $role, + 'search' => $usersearch, + 'fields' => 'all_with_meta' + ); + } if ( '' !== $args['search'] ) $args['search'] = '*' . $args['search'] . '*'; @@ -166,6 +176,7 @@ class WP_Users_List_Table extends WP_List_Table { $url = 'users.php'; $users_of_blog = count_users(); } + $total_users = $users_of_blog['total_users']; $avail_roles =& $users_of_blog['avail_roles']; unset($users_of_blog); @@ -189,6 +200,21 @@ class WP_Users_List_Table extends WP_List_Table { $role_links[$this_role] = "$name"; } + if ( ! empty( $avail_roles['none' ] ) ) { + + $class = ''; + + if ( 'none' === $role ) { + $class = ' class="current"'; + } + + $name = __( 'No role' ); + /* translators: User role name with count */ + $name = sprintf( __('%1$s (%2$s)'), $name, number_format_i18n( $avail_roles['none' ] ) ); + $role_links['none'] = "$name"; + + } + return $role_links; } diff --git a/src/wp-includes/user-functions.php b/src/wp-includes/user-functions.php index 3ec4cd856f..db87cd49a8 100644 --- a/src/wp-includes/user-functions.php +++ b/src/wp-includes/user-functions.php @@ -735,6 +735,7 @@ function update_user_meta($user_id, $meta_key, $meta_value, $prev_value = '') { * Using $strategy = 'memory' this is memory-intensive and should handle around 10^5 users, but see WP Bug #12257. * * @since 3.0.0 + * @since 4.4.0 The number of users with no role is now included in the `none` element. * * @global wpdb $wpdb * @@ -775,10 +776,14 @@ function count_users($strategy = 'time') { // Get the meta_value index from the end of the result set. $total_users = (int) $row[$col]; + $role_counts['none'] = ( $total_users - array_sum( $role_counts ) ); + $result['total_users'] = $total_users; $result['avail_roles'] =& $role_counts; } else { - $avail_roles = array(); + $avail_roles = array( + 'none' => 0, + ); $users_of_blog = $wpdb->get_col( "SELECT meta_value FROM $wpdb->usermeta WHERE meta_key = '{$blog_prefix}capabilities'" ); @@ -786,6 +791,9 @@ function count_users($strategy = 'time') { $b_roles = maybe_unserialize($caps_meta); if ( ! is_array( $b_roles ) ) continue; + if ( empty( $b_roles ) ) { + $avail_roles['none']++; + } foreach ( $b_roles as $b_role => $val ) { if ( isset($avail_roles[$b_role]) ) { $avail_roles[$b_role]++; @@ -799,6 +807,10 @@ function count_users($strategy = 'time') { $result['avail_roles'] =& $avail_roles; } + if ( is_multisite() ) { + $result['avail_roles']['none'] = 0; + } + return $result; } @@ -2231,3 +2243,32 @@ function wp_destroy_all_sessions() { $manager = WP_Session_Tokens::get_instance( get_current_user_id() ); $manager->destroy_all(); } + +/** + * Get the user IDs of all users with no role on this site. + * + * This function returns an empty array when used on Multisite. + * + * @since 4.4.0 + * + * @return array Array of user IDs. + */ +function wp_get_users_with_no_role() { + global $wpdb; + + if ( is_multisite() ) { + return array(); + } + + $prefix = $wpdb->get_blog_prefix(); + $regex = implode( '|', wp_roles()->get_names() ); + $regex = preg_replace( '/[^a-zA-Z_\|-]/', '', $regex ); + $users = $wpdb->get_col( $wpdb->prepare( " + SELECT user_id + FROM $wpdb->usermeta + WHERE meta_key = '{$prefix}capabilities' + AND meta_value NOT REGEXP %s + ", $regex ) ); + + return $users; +} diff --git a/tests/phpunit/tests/user/countUsers.php b/tests/phpunit/tests/user/countUsers.php new file mode 100644 index 0000000000..6d783debb8 --- /dev/null +++ b/tests/phpunit/tests/user/countUsers.php @@ -0,0 +1,154 @@ +markTestSkipped( 'Test does not run on multisite' ); + } + + // Setup users + $admin = $this->factory->user->create( array( + 'role' => 'administrator', + ) ); + $editor = $this->factory->user->create( array( + 'role' => 'editor', + ) ); + $author = $this->factory->user->create( array( + 'role' => 'author', + ) ); + $contributor = $this->factory->user->create( array( + 'role' => 'contributor', + ) ); + $subscriber = $this->factory->user->create( array( + 'role' => 'subscriber', + ) ); + $none = $this->factory->user->create( array( + 'role' => '', + ) ); + $nobody = $this->factory->user->create( array( + 'role' => '', + ) ); + + // Test user counts + $count = count_users( $strategy ); + + $this->assertEquals( 8, $count['total_users'] ); + $this->assertEquals( array( + 'administrator' => 2, + 'editor' => 1, + 'author' => 1, + 'contributor' => 1, + 'subscriber' => 1, + 'none' => 2, + ), $count['avail_roles'] ); + + } + + /** + * @ticket 22993 + * @group multisite + * + * @dataProvider data_count_users_strategies + */ + public function test_count_users_multisite_is_accurate( $strategy ) { + + if ( ! is_multisite() ) { + $this->markTestSkipped( 'Test requires multisite' ); + } + + // Setup users + $admin = $this->factory->user->create( array( + 'role' => 'administrator', + ) ); + $editor = $this->factory->user->create( array( + 'role' => 'editor', + ) ); + $author = $this->factory->user->create( array( + 'role' => 'author', + ) ); + $contributor = $this->factory->user->create( array( + 'role' => 'contributor', + ) ); + $subscriber = $this->factory->user->create( array( + 'role' => 'subscriber', + ) ); + $none = $this->factory->user->create( array( + 'role' => '', + ) ); + $nobody = $this->factory->user->create( array( + 'role' => '', + ) ); + + // Setup blogs + $blog_1 = (int) $this->factory->blog->create( array( + 'user_id' => $editor, + ) ); + $blog_2 = (int) $this->factory->blog->create( array( + 'user_id' => $author, + ) ); + + // Add users to blogs + add_user_to_blog( $blog_1, $subscriber, 'editor' ); + add_user_to_blog( $blog_2, $none, 'contributor' ); + + // Test users counts on root site + $count = count_users( $strategy ); + + $this->assertEquals( 8, $count['total_users'] ); + $this->assertEquals( array( + 'administrator' => 2, + 'editor' => 1, + 'author' => 1, + 'contributor' => 1, + 'subscriber' => 1, + 'none' => 0, + ), $count['avail_roles'] ); + + // Test users counts on blog 1 + switch_to_blog( $blog_1 ); + $count = count_users( $strategy ); + restore_current_blog(); + + $this->assertEquals( 2, $count['total_users'] ); + $this->assertEquals( array( + 'administrator' => 1, + 'editor' => 1, + 'none' => 0, + ), $count['avail_roles'] ); + + // Test users counts on blog 2 + switch_to_blog( $blog_2 ); + $count = count_users( $strategy ); + restore_current_blog(); + + $this->assertEquals( 2, $count['total_users'] ); + $this->assertEquals( array( + 'administrator' => 1, + 'contributor' => 1, + 'none' => 0, + ), $count['avail_roles'] ); + + } + + function data_count_users_strategies() { + return array( + array( + 'time', + ), + array( + 'memory', + ), + ); + } + +} diff --git a/tests/phpunit/tests/user/wpGetUsersWithNoRole.php b/tests/phpunit/tests/user/wpGetUsersWithNoRole.php new file mode 100644 index 0000000000..ff01fabc67 --- /dev/null +++ b/tests/phpunit/tests/user/wpGetUsersWithNoRole.php @@ -0,0 +1,84 @@ +markTestSkipped( 'Test does not run on multisite' ); + } + + // Setup users + $admin = $this->factory->user->create( array( + 'role' => 'administrator', + ) ); + $editor = $this->factory->user->create( array( + 'role' => 'editor', + ) ); + $nobody = $this->factory->user->create( array( + 'role' => '', + ) ); + $nobody_else = $this->factory->user->create( array( + 'role' => '', + ) ); + + // Test users + $users = wp_get_users_with_no_role(); + + $this->assertEquals( array( + $nobody, + $nobody_else, + ), $users ); + + } + + /** + * @ticket 22993 + * @group multisite + */ + public function test_get_users_with_no_role_multisite_is_accurate() { + + if ( ! is_multisite() ) { + $this->markTestSkipped( 'Test requires multisite' ); + } + + // Setup users + $admin = $this->factory->user->create( array( + 'role' => 'administrator', + ) ); + $editor = $this->factory->user->create( array( + 'role' => 'editor', + ) ); + $nobody = $this->factory->user->create( array( + 'role' => '', + ) ); + + // Setup blogs + $blog_1 = (int) $this->factory->blog->create( array( + 'user_id' => $editor, + ) ); + + // Add users to blogs + add_user_to_blog( $blog_1, $editor, 'editor' ); + + // Test users on root site + $users = wp_get_users_with_no_role(); + $this->assertSame( array(), $users ); + + // Test users counts on blog 1 + switch_to_blog( $blog_1 ); + $users = wp_get_users_with_no_role(); + restore_current_blog(); + + // Test users on root site + $this->assertSame( array(), $users ); + + } + +}