From 19bd759db57b4f1cc72157a446915ffbafdd838e Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 18 Apr 2023 11:48:46 +0000 Subject: [PATCH] Users: Cache database queries within `WP_User_Query` class. Cache the results of database queries within `WP_User_Query` class. Only cache queries that are requesting 3 or less fields so that caches are not storing full user objects. Cache results are stored in a new global cache group named `users-queries`. Add a new parameter to `WP_User_Query` called `cache_results` to allow developers to opt out of a receiving cached results. `cache_results` parameter defaults to true. Also add a new helper function called `wp_cache_set_users_last_changed`, similar to `wp_cache_set_posts_last_changed` that incroments last changed value in cache group `users`. Ensure that `wp_cache_set_users_last_changed` is called whenever user / user meta is modified for proper cache invalidation. Props johnjamesjacoby, spacedmonkey, westi, dd32, strategio, srikanthmeenakshi, OllieJones, khoipro, rjasdfiii, flixos90, mukesh27, peterwilsoncc. Fixes #40613. git-svn-id: https://develop.svn.wordpress.org/trunk@55657 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-user-query.php | 113 ++- src/wp-includes/default-filters.php | 8 + src/wp-includes/load.php | 1 + src/wp-includes/ms-blogs.php | 2 + src/wp-includes/ms-functions.php | 1 + src/wp-includes/user.php | 10 + tests/phpunit/includes/abstract-testcase.php | 1 + tests/phpunit/tests/user/queryCache.php | 781 +++++++++++++++++++ 8 files changed, 898 insertions(+), 19 deletions(-) create mode 100644 tests/phpunit/tests/user/queryCache.php diff --git a/src/wp-includes/class-wp-user-query.php b/src/wp-includes/class-wp-user-query.php index 446cc746fa..c39d4bf552 100644 --- a/src/wp-includes/class-wp-user-query.php +++ b/src/wp-includes/class-wp-user-query.php @@ -119,6 +119,7 @@ class WP_User_Query { 'login' => '', 'login__in' => array(), 'login__not_in' => array(), + 'cache_results' => true, ); return wp_parse_args( $args, $defaults ); @@ -140,6 +141,7 @@ class WP_User_Query { * @since 5.1.0 Introduced the 'meta_compare_key' parameter. * @since 5.3.0 Introduced the 'meta_type_key' parameter. * @since 5.9.0 Added 'capability', 'capability__in', and 'capability__not_in' parameters. + * @since 6.3.0 Added 'cache_results' parameter. * * @global wpdb $wpdb WordPress database abstraction object. * @global WP_Roles $wp_roles WordPress role management object. @@ -254,6 +256,7 @@ class WP_User_Query { * logins will be included in results. Default empty array. * @type string[] $login__not_in An array of logins to exclude. Users matching one of these * logins will not be included in results. Default empty array. + * @type bool $cache_results Whether to cache user information. Default true. * } */ public function prepare_query( $query = array() ) { @@ -790,6 +793,11 @@ class WP_User_Query { $qv =& $this->query_vars; + // Do not cache results if more than 3 fields are requested. + if ( is_array( $qv['fields'] ) && count( $qv['fields'] ) > 3 ) { + $qv['cache_results'] = false; + } + /** * Filters the users array before the query takes place. * @@ -816,28 +824,47 @@ class WP_User_Query { {$this->query_orderby} {$this->query_limit} "; - - if ( is_array( $qv['fields'] ) ) { - $this->results = $wpdb->get_results( $this->request ); - } else { - $this->results = $wpdb->get_col( $this->request ); + $cache_value = false; + $cache_key = $this->generate_cache_key( $qv, $this->request ); + $cache_group = 'users-queries'; + if ( $qv['cache_results'] ) { + $cache_value = wp_cache_get( $cache_key, $cache_group ); } + if ( false !== $cache_value ) { + $this->results = $cache_value['user_data']; + $this->total_users = $cache_value['total_users']; + } else { - if ( isset( $qv['count_total'] ) && $qv['count_total'] ) { - /** - * Filters SELECT FOUND_ROWS() query for the current WP_User_Query instance. - * - * @since 3.2.0 - * @since 5.1.0 Added the `$this` parameter. - * - * @global wpdb $wpdb WordPress database abstraction object. - * - * @param string $sql The SELECT FOUND_ROWS() query for the current WP_User_Query. - * @param WP_User_Query $query The current WP_User_Query instance. - */ - $found_users_query = apply_filters( 'found_users_query', 'SELECT FOUND_ROWS()', $this ); + if ( is_array( $qv['fields'] ) ) { + $this->results = $wpdb->get_results( $this->request ); + } else { + $this->results = $wpdb->get_col( $this->request ); + } - $this->total_users = (int) $wpdb->get_var( $found_users_query ); + if ( isset( $qv['count_total'] ) && $qv['count_total'] ) { + /** + * Filters SELECT FOUND_ROWS() query for the current WP_User_Query instance. + * + * @since 3.2.0 + * @since 5.1.0 Added the `$this` parameter. + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param string $sql The SELECT FOUND_ROWS() query for the current WP_User_Query. + * @param WP_User_Query $query The current WP_User_Query instance. + */ + $found_users_query = apply_filters( 'found_users_query', 'SELECT FOUND_ROWS()', $this ); + + $this->total_users = (int) $wpdb->get_var( $found_users_query ); + } + + if ( $qv['cache_results'] ) { + $cache_value = array( + 'user_data' => $this->results, + 'total_users' => $this->total_users, + ); + wp_cache_add( $cache_key, $cache_value, $cache_group ); + } } } @@ -1010,6 +1037,54 @@ class WP_User_Query { return $_orderby; } + /** + * Generate cache key. + * + * @since 6.3.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param array $args Query arguments. + * @param string $sql SQL statement. + * @return string Cache key. + */ + protected function generate_cache_key( array $args, $sql ) { + global $wpdb; + + // Replace wpdb placeholder in the SQL statement used by the cache key. + $sql = $wpdb->remove_placeholder_escape( $sql ); + + $key = md5( $sql ); + $last_changed = wp_cache_get_last_changed( 'users' ); + + if ( empty( $args['orderby'] ) ) { + // Default order is by 'user_login'. + $ordersby = array( 'user_login' => '' ); + } elseif ( is_array( $args['orderby'] ) ) { + $ordersby = $args['orderby']; + } else { + // 'orderby' values may be a comma- or space-separated list. + $ordersby = preg_split( '/[,\s]+/', $args['orderby'] ); + } + + $blog_id = 0; + if ( isset( $args['blog_id'] ) ) { + $blog_id = absint( $args['blog_id'] ); + } + if ( ( $args['has_published_posts'] && $blog_id ) || in_array( 'post_count', $ordersby, true ) ) { + $switch = get_current_blog_id() !== $blog_id; + if ( $switch ) { + switch_to_blog( $blog_id ); + } + $last_changed .= wp_cache_get_last_changed( 'posts' ); + if ( $switch ) { + restore_current_blog(); + } + } + + return "get_users:$key:$last_changed"; + } + /** * Parses an 'order' query variable and casts it to ASC or DESC as necessary. * diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index beddde2687..92c4c643f9 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -115,6 +115,14 @@ add_action( 'added_post_meta', 'wp_cache_set_posts_last_changed' ); add_action( 'updated_post_meta', 'wp_cache_set_posts_last_changed' ); add_action( 'deleted_post_meta', 'wp_cache_set_posts_last_changed' ); +// User meta. +add_action( 'added_user_meta', 'wp_cache_set_users_last_changed' ); +add_action( 'updated_user_meta', 'wp_cache_set_users_last_changed' ); +add_action( 'deleted_user_meta', 'wp_cache_set_users_last_changed' ); +add_action( 'add_user_role', 'wp_cache_set_users_last_changed' ); +add_action( 'set_user_role', 'wp_cache_set_users_last_changed' ); +add_action( 'remove_user_role', 'wp_cache_set_users_last_changed' ); + // Term meta. add_action( 'added_term_meta', 'wp_cache_set_terms_last_changed' ); add_action( 'updated_term_meta', 'wp_cache_set_terms_last_changed' ); diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 4ad4bad207..6f5a4502c7 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -776,6 +776,7 @@ function wp_start_object_cache() { 'usermeta', 'user_meta', 'userslugs', + 'users-queries', ) ); diff --git a/src/wp-includes/ms-blogs.php b/src/wp-includes/ms-blogs.php index 0f5ff5d510..e6cfcc2296 100644 --- a/src/wp-includes/ms-blogs.php +++ b/src/wp-includes/ms-blogs.php @@ -573,6 +573,7 @@ function switch_to_blog( $new_blog_id, $deprecated = null ) { 'usermeta', 'user_meta', 'userslugs', + 'users-queries', ) ); } @@ -666,6 +667,7 @@ function restore_current_blog() { 'usermeta', 'user_meta', 'userslugs', + 'users-queries', ) ); } diff --git a/src/wp-includes/ms-functions.php b/src/wp-includes/ms-functions.php index 9f39266231..3ecbf7691f 100644 --- a/src/wp-includes/ms-functions.php +++ b/src/wp-includes/ms-functions.php @@ -295,6 +295,7 @@ function remove_user_from_blog( $user_id, $blog_id = 0, $reassign = 0 ) { } } + clean_user_cache( $user_id ); restore_current_blog(); return true; diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index ee7c87f8a6..7495004d25 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -1907,6 +1907,7 @@ function clean_user_cache( $user ) { } wp_cache_delete( $user->ID, 'user_meta' ); + wp_cache_set_users_last_changed(); /** * Fires immediately after the given user's cache is cleaned. @@ -5016,3 +5017,12 @@ function wp_register_persisted_preferences_meta() { ) ); } + +/** + * Sets the last changed time for the 'users' cache group. + * + * @since 6.3.0 + */ +function wp_cache_set_users_last_changed() { + wp_cache_set( 'last_changed', microtime(), 'users' ); +} diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index 2c1b4a1d58..6db307f256 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -412,6 +412,7 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase { 'usermeta', 'user_meta', 'userslugs', + 'users-queries', ) ); diff --git a/tests/phpunit/tests/user/queryCache.php b/tests/phpunit/tests/user/queryCache.php new file mode 100644 index 0000000000..91e84c4e01 --- /dev/null +++ b/tests/phpunit/tests/user/queryCache.php @@ -0,0 +1,781 @@ +user->create_many( + 4, + array( + 'role' => 'author', + ) + ); + + self::$sub_ids = $factory->user->create_many( + 2, + array( + 'role' => 'subscriber', + ) + ); + + self::$editor_ids = $factory->user->create_many( + 3, + array( + 'role' => 'editor', + ) + ); + + self::$contrib_id = $factory->user->create( + array( + 'role' => 'contributor', + ) + ); + + self::$admin_ids = $factory->user->create_many( + 2, + array( + 'role' => 'administrator', + ) + ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_different_count() { + $args = array( + 'count_total' => true, + ); + + $query1 = new WP_User_Query( $args ); + $users1 = wp_list_pluck( $query1->get_results(), 'ID' ); + $users_total1 = $query1->get_total(); + + $queries_before = get_num_queries(); + + $args = array( + 'count_total' => false, + ); + + $query2 = new WP_User_Query( $args ); + $users2 = wp_list_pluck( $query2->get_results(), 'ID' ); + $users_total2 = $query2->get_total(); + $queries_after = get_num_queries(); + + $this->assertNotSame( $queries_before, $queries_after, 'Assert that the number of queries is not equal' ); + $this->assertNotSame( $users_total1, $users_total2, 'Assert that totals do not match' ); + $this->assertSameSets( $users1, $users2, 'Results of the query are expected to match.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_results() { + $args = array( + 'cache_results' => true, + ); + + $query1 = new WP_User_Query( $args ); + $users1 = wp_list_pluck( $query1->get_results(), 'ID' ); + + $queries_before = get_num_queries(); + + $args = array( + 'cache_results' => false, + ); + + $query2 = new WP_User_Query( $args ); + $users2 = wp_list_pluck( $query2->get_results(), 'ID' ); + $queries_after = get_num_queries(); + + $this->assertNotSame( $queries_before, $queries_after, 'Assert that queries are run' ); + $this->assertSameSets( $users1, $users2, 'Results of the query are expected to match.' ); + } + + /** + * @ticket 40613 + * @covers ::query + * @expectedDeprecated WP_User_Query + */ + public function test_query_cache_who() { + $args = array( + 'who' => 'authors', + 'fields' => array( 'ID' ), + ); + + $query1 = new WP_User_Query( $args ); + $users1 = $query1->get_results(); + $users_total1 = $query1->get_total(); + + $queries_before = get_num_queries(); + $query2 = new WP_User_Query( $args ); + $users2 = $query2->get_results(); + $users_total2 = $query2->get_total(); + $queries_after = get_num_queries(); + + $this->assertSame( $queries_before, $queries_after, 'No queries are expected run.' ); + $this->assertSame( $users_total1, $users_total2, 'Number of users returned us expected to match.' ); + $this->assertSameSets( $users1, $users2, 'Results of the query are expected to match.' ); + } + + /** + * @ticket 40613 + * @covers ::query + * @dataProvider data_query_cache + * @param array $args Optional. See WP_User_Query::prepare_query() + */ + public function test_query_cache( array $args ) { + $query1 = new WP_User_Query( $args ); + $users1 = $query1->get_results(); + $users_total1 = $query1->get_total(); + + $queries_before = get_num_queries(); + $query2 = new WP_User_Query( $args ); + $users2 = $query2->get_results(); + $users_total2 = $query2->get_total(); + $queries_after = get_num_queries(); + + $this->assertSame( 0, $queries_after - $queries_before, 'Assert that no queries are run' ); + $this->assertSame( $users_total1, $users_total2, 'Assert that totals do match' ); + $this->assertSameSets( $users1, $users2, 'Asset that results of query match' ); + } + + /** + * Data provider + * + * @return array + */ + public function data_query_cache() { + $data = array( + 'id' => array( + 'args' => array( 'fields' => array( 'id' ) ), + + ), + 'ID' => array( + 'args' => array( 'fields' => array( 'ID' ) ), + ), + 'user_login' => array( + 'args' => array( 'fields' => array( 'user_login' ) ), + ), + 'user_nicename' => array( + 'args' => array( 'fields' => array( 'user_nicename' ) ), + ), + 'user_email' => array( + 'args' => array( 'fields' => array( 'user_email' ) ), + ), + 'user_url' => array( + 'args' => array( 'fields' => array( 'user_url' ) ), + ), + 'user_status' => array( + 'args' => array( 'fields' => array( 'user_status' ) ), + ), + 'display_name' => array( + 'args' => array( 'fields' => array( 'display_name' ) ), + ), + 'invalid_field' => array( + 'args' => array( 'fields' => array( 'invalid_field' ) ), + ), + 'valid array inc id' => array( + 'args' => array( 'fields' => array( 'display_name', 'user_email', 'id' ) ), + ), + 'valid array inc ID' => array( + 'args' => array( 'fields' => array( 'display_name', 'user_email', 'ID' ) ), + ), + 'partly valid array' => array( + 'args' => array( 'fields' => array( 'display_name', 'invalid_field' ) ), + ), + 'orderby' => array( + 'args' => array( + 'fields' => array( 'ID' ), + 'orderby' => array( 'login', 'nicename' ), + ), + ), + 'meta query' => array( + 'args' => array( + 'fields' => array( 'ID' ), + 'meta_query' => array( + 'foo_key' => array( + 'key' => 'foo', + 'compare' => 'EXISTS', + ), + ), + 'orderby' => 'foo_key', + 'order' => 'DESC', + ), + ), + 'meta query LIKE' => array( + 'args' => array( + 'fields' => array( 'ID' ), + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => '00', + 'compare' => 'LIKE', + ), + ), + 'orderby' => 'foo_key', + 'order' => 'DESC', + ), + ), + 'published posts' => array( + 'args' => array( + 'has_published_posts' => true, + 'fields' => array( 'ID' ), + ), + ), + 'published posts order' => array( + 'args' => array( + 'orderby' => 'post_count', + 'fields' => array( 'ID' ), + ), + ), + 'published count_total' => array( + 'args' => array( + + 'count_total' => false, + 'fields' => array( 'ID' ), + ), + ), + 'capability' => array( + 'args' => array( + 'capability' => 'install_plugins', + 'fields' => array( 'ID' ), + ), + ), + 'include' => array( + 'args' => array( + 'includes' => self::$author_ids, + 'fields' => array( 'ID' ), + ), + ), + 'exclude' => array( + 'args' => array( + 'exclude' => self::$author_ids, + 'fields' => array( 'ID' ), + ), + ), + 'search' => array( + 'args' => array( + 'search' => 'User', + 'fields' => array( 'ID' ), + ), + ), + ); + + if ( is_multisite() ) { + $data['spam'] = array( + 'args' => array( 'fields' => array( 'spam' ) ), + ); + $data['deleted'] = array( + 'args' => array( 'fields' => array( 'deleted' ) ), + ); + } + + return $data; + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_remove_user_role() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + + $q1 = new WP_User_Query( + array( + 'role' => 'author', + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + + $this->assertContains( $user_id, $found, 'Expected to find author in returned values.' ); + + $user = get_user_by( 'id', $user_id ); + $user->remove_role( 'author' ); + + $q2 = new WP_User_Query( + array( + 'role' => 'author', + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( $user_id, $found, 'Expected not to find author in returned values.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_set_user_role() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + + $q1 = new WP_User_Query( + array( + 'role' => 'author', + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + + $this->assertContains( $user_id, $found, 'Expected to find author in returned values.' ); + + $user = get_user_by( 'id', $user_id ); + $user->set_role( 'editor' ); + + $q2 = new WP_User_Query( + array( + 'role' => 'author', + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( $user_id, $found, 'Expected not to find author in returned values.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_delete_user() { + $user_id = self::factory()->user->create(); + + $q1 = new WP_User_Query( + array( + 'include' => array( $user_id ), + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + $expected = array( $user_id ); + + $this->assertSameSets( $expected, $found, 'Find author in returned values' ); + + wp_delete_user( $user_id ); + + $q2 = new WP_User_Query( + array( + 'include' => array( $user_id ), + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( $user_id, $found, 'Expected not to find author in returned values.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_do_not_cache() { + $user_id = self::factory()->user->create(); + + $args = array( + 'fields' => array( + 'user_login', + 'user_nicename', + 'user_email', + 'user_url', + 'user_status', + 'display_name', + ), + 'include' => array( $user_id ), + ); + + $q1 = new WP_User_Query( $args ); + $found1 = $q1->get_results(); + $callback = static function( $user ) { + return (array) $user; + }; + + $found1 = array_map( $callback, $found1 ); + + $queries_before = get_num_queries(); + $q2 = new WP_User_Query( $args ); + $found2 = $q2->get_results(); + $found2 = array_map( $callback, $found2 ); + $queries_after = get_num_queries(); + + $this->assertSame( $queries_after - $queries_before, 2, 'Ensure that query is not cached' ); + $this->assertSameSets( $found1, $found2, 'Expected results to match.', 'Ensure that to results match' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_update_user() { + $user_id = end( self::$admin_ids ); + + wp_update_user( + array( + 'ID' => $user_id, + 'user_nicename' => 'paul', + ) + ); + + $args = array( + 'nicename__in' => array( 'paul' ), + ); + + $q1 = new WP_User_Query( $args ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + $expected = array( $user_id ); + + $this->assertSameSets( $expected, $found, 'Find author in returned values' ); + + wp_update_user( + array( + 'ID' => $user_id, + 'user_nicename' => 'linda', + ) + ); + + $q2 = new WP_User_Query( $args ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( $user_id, $found, 'Expected not to find author in returned values.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_create_user() { + $user_id = end( self::$admin_ids ); + + $args = array( 'blog_id' => get_current_blog_id() ); + + $q1 = new WP_User_Query( $args ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + + $this->assertContains( $user_id, $found, 'Expected to find author in returned values.' ); + + $user_id_2 = self::factory()->user->create(); + + $q2 = new WP_User_Query( $args ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertContains( $user_id_2, $found, 'Find author in returned values' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_has_published_posts_delete_post() { + register_post_type( 'wptests_pt_public', array( 'public' => true ) ); + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$author_ids[2], + 'post_status' => 'publish', + 'post_type' => 'wptests_pt_public', + ) + ); + + $q1 = new WP_User_Query( + array( + 'has_published_posts' => true, + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + $expected = array( self::$author_ids[2] ); + + $this->assertSameSets( $expected, $found, 'Find author in returned values' ); + + wp_delete_post( $post_id, true ); + + $q2 = new WP_User_Query( + array( + 'has_published_posts' => true, + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertSameSets( array(), $found, 'Not to find author in returned values' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_has_published_posts_delete_post_order() { + register_post_type( 'wptests_pt_public', array( 'public' => true ) ); + + $user_id = self::factory()->user->create(); + + $post_id = self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_status' => 'publish', + 'post_type' => 'wptests_pt_public', + ) + ); + + $q1 = new WP_User_Query( + array( + 'orderby' => 'post_count', + ) + ); + + $found1 = wp_list_pluck( $q1->get_results(), 'ID' ); + $this->assertContains( $user_id, $found1, 'Find author in returned values in first run of WP_User_Query' ); + + wp_delete_post( $post_id, true ); + + $q2 = new WP_User_Query( + array( + 'orderby' => 'post_count', + ) + ); + + $found2 = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertContains( $user_id, $found1, 'Find author in returned values in second run of WP_User_Query' ); + $this->assertSameSets( $found1, $found2, 'Not same order' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_meta_query_cache_invalidation() { + add_user_meta( self::$author_ids[0], 'foo', 'bar' ); + add_user_meta( self::$author_ids[1], 'foo', 'bar' ); + + $q1 = new WP_User_Query( + array( + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + $expected = array( self::$author_ids[0], self::$author_ids[1] ); + + $this->assertSameSets( $expected, $found, 'Asset that results contain authors' ); + + delete_user_meta( self::$author_ids[1], 'foo' ); + + $q2 = new WP_User_Query( + array( + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $expected = array( self::$author_ids[0] ); + + $this->assertSameSets( $expected, $found, 'Asset that results do not contain author without meta' ); + } + + /** + * @ticket 40613 + * @group ms-required + * @covers ::query + */ + public function test_get_single_capability_multisite_blog_id() { + $blog_id = self::factory()->blog->create(); + + add_user_to_blog( $blog_id, self::$author_ids[0], 'subscriber' ); + add_user_to_blog( $blog_id, self::$author_ids[1], 'author' ); + add_user_to_blog( $blog_id, self::$author_ids[2], 'editor' ); + + $q1 = new WP_User_Query( + array( + 'capability' => 'publish_posts', + 'blog_id' => $blog_id, + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + + $this->assertNotContains( self::$author_ids[0], $found, 'Asset that results do not contain author 0 without capability on site on first run' ); + $this->assertContains( self::$author_ids[1], $found, 'Asset that results do contain author with capability on site on first run' ); + $this->assertContains( self::$author_ids[2], $found, 'Asset that results do contain author with capability on site on first run' ); + + remove_user_from_blog( self::$author_ids[2], $blog_id ); + + $q2 = new WP_User_Query( + array( + 'capability' => 'publish_posts', + 'blog_id' => $blog_id, + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( self::$author_ids[0], $found, 'Asset that results do not contain author 0 without capability on site on second run' ); + $this->assertContains( self::$author_ids[1], $found, 'Asset that results do contain author with capability on site on second run' ); + $this->assertNotContains( self::$author_ids[2], $found, 'Asset that results do not contain author 1 without capability on site on second run' ); + } + + /** + * @ticket 40613 + * @group ms-required + * @covers ::query + */ + public function test_query_should_respect_blog_id() { + $blogs = self::factory()->blog->create_many( 2 ); + + add_user_to_blog( $blogs[0], self::$author_ids[0], 'author' ); + add_user_to_blog( $blogs[0], self::$author_ids[1], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[0], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[1], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[2], 'author' ); + + $q = new WP_User_Query( + array( + 'fields' => 'ids', + 'blog_id' => $blogs[0], + ) + ); + + $expected = array( (string) self::$author_ids[0], (string) self::$author_ids[1] ); + + $this->assertSameSets( $expected, $q->get_results(), 'Asset that expected users return' ); + + $q = new WP_User_Query( + array( + 'fields' => 'ids', + 'blog_id' => $blogs[1], + ) + ); + + $expected = array( (string) self::$author_ids[0], (string) self::$author_ids[1], (string) self::$author_ids[2] ); + + $this->assertSameSets( $expected, $q->get_results(), 'Asset that expected users return from different blog' ); + } + + /** + * @ticket 40613 + * @group ms-required + * @covers ::query + */ + public function test_has_published_posts_should_respect_blog_id() { + $blogs = self::factory()->blog->create_many( 2 ); + + add_user_to_blog( $blogs[0], self::$author_ids[0], 'author' ); + add_user_to_blog( $blogs[0], self::$author_ids[1], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[0], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[1], 'author' ); + + switch_to_blog( $blogs[0] ); + self::factory()->post->create( + array( + 'post_author' => self::$author_ids[0], + 'post_status' => 'publish', + 'post_type' => 'post', + ) + ); + restore_current_blog(); + + switch_to_blog( $blogs[1] ); + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$author_ids[1], + 'post_status' => 'publish', + 'post_type' => 'post', + ) + ); + restore_current_blog(); + + $q = new WP_User_Query( + array( + 'has_published_posts' => array( 'post' ), + 'blog_id' => $blogs[1], + ) + ); + + $found = wp_list_pluck( $q->get_results(), 'ID' ); + $expected = array( self::$author_ids[1] ); + + $this->assertSameSets( $expected, $found, 'Asset that expected users returned with posts on this site' ); + switch_to_blog( $blogs[1] ); + wp_delete_post( $post_id, true ); + restore_current_blog(); + + $q = new WP_User_Query( + array( + 'has_published_posts' => array( 'post' ), + 'blog_id' => $blogs[1], + ) + ); + + $found = wp_list_pluck( $q->get_results(), 'ID' ); + + $this->assertSameSets( array(), $found, 'Asset that no users returned with posts on this site as posts have been deleted' ); + } + + /** + * Ensure cache keys are generated without WPDB placeholders. + * + * @ticket 40613 + * + * @covers ::generate_cache_key + */ + public function test_generate_cache_key_placeholder() { + global $wpdb; + $query1 = new WP_User_Query( array( 'capability' => 'edit_posts' ) ); + + $query_vars = $query1->query_vars; + $request_with_placeholder = $query1->request; + $request_without_placeholder = $wpdb->remove_placeholder_escape( $query1->request ); + + $reflection = new ReflectionMethod( $query1, 'generate_cache_key' ); + $reflection->setAccessible( true ); + + $cache_key_1 = $reflection->invoke( $query1, $query_vars, $request_with_placeholder ); + $cache_key_2 = $reflection->invoke( $query1, $query_vars, $request_without_placeholder ); + + $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs when using wpdb placeholder.' ); + } +}