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 );
+
+ }
+
+}