diff --git a/src/wp-includes/class-wp-site-query.php b/src/wp-includes/class-wp-site-query.php index 344b27ff08..0e76b7705b 100644 --- a/src/wp-includes/class-wp-site-query.php +++ b/src/wp-includes/class-wp-site-query.php @@ -39,6 +39,22 @@ class WP_Site_Query { 'limits' => '', ); + /** + * Metadata query container. + * + * @since 5.0.0 + * @var WP_Meta_Query + */ + public $meta_query = false; + + /** + * Metadata query clauses. + * + * @since 5.0.0 + * @var array + */ + protected $meta_query_clauses; + /** * Date query container. * @@ -92,7 +108,8 @@ class WP_Site_Query { * * @since 4.6.0 * @since 4.8.0 Introduced the 'lang_id', 'lang__in', and 'lang__not_in' parameters. - * @since 5.0.0 Introduced the 'update_site_meta_cache' parameter. + * @since 5.0.0 Introduced the 'update_site_meta_cache', 'meta_query', 'meta_key', + * 'meta_value', 'meta_type' and 'meta_compare' parameters. * * @param string|array $query { * Optional. Array or query string of site query parameters. Default empty. @@ -139,6 +156,15 @@ class WP_Site_Query { * Default empty array. * @type bool $update_site_cache Whether to prime the cache for found sites. Default true. * @type bool $update_site_meta_cache Whether to prime the metadata cache for found sites. Default true. + * @type array $meta_query Meta query clauses to limit retrieved sites by. See `WP_Meta_Query`. + * Default empty. + * @type string $meta_key Limit sites to those matching a specific metadata key. + * Can be used in conjunction with `$meta_value`. Default empty. + * @type string $meta_value Limit sites to those matching a specific metadata value. + * Usually used in conjunction with `$meta_key`. Default empty. + * @type string $meta_type Data type that the `$meta_value` column will be CAST to for + * comparisons. Default empty. + * @type string $meta_compare Comparison operator to test the `$meta_value`. Default empty. * } */ public function __construct( $query = '' ) { @@ -175,6 +201,11 @@ class WP_Site_Query { 'date_query' => null, // See WP_Date_Query 'update_site_cache' => true, 'update_site_meta_cache' => true, + 'meta_query' => '', + 'meta_key' => '', + 'meta_value' => '', + 'meta_type' => '', + 'meta_compare' => '', ); if ( ! empty( $query ) ) { @@ -228,12 +259,20 @@ class WP_Site_Query { * * @since 4.6.0 * + * @global wpdb $wpdb WordPress database abstraction object. + * * @return array|int List of WP_Site objects, a list of site ids when 'fields' is set to 'ids', * or the number of sites when 'count' is passed as a query var. */ public function get_sites() { + global $wpdb; + $this->parse_query(); + // Parse meta query. + $this->meta_query = new WP_Meta_Query(); + $this->meta_query->parse_query_vars( $this->query_vars ); + /** * Fires before sites are retrieved. * @@ -243,6 +282,12 @@ class WP_Site_Query { */ do_action_ref_array( 'pre_get_sites', array( &$this ) ); + // Reparse query vars, in case they were modified in a 'pre_get_sites' callback. + $this->meta_query->parse_query_vars( $this->query_vars ); + if ( ! empty( $this->meta_query->queries ) ) { + $this->meta_query_clauses = $this->meta_query->get_sql( 'blog', $wpdb->blogs, 'blog_id', $this ); + } + // $args can include anything. Only use the args defined in the query_var_defaults to compute the key. $_args = wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) ); @@ -370,7 +415,7 @@ class WP_Site_Query { $orderby = implode( ', ', $orderby_array ); } else { - $orderby = "blog_id $order"; + $orderby = "{$wpdb->blogs}.blog_id $order"; } $number = absint( $this->query_vars['number'] ); @@ -387,23 +432,23 @@ class WP_Site_Query { if ( $this->query_vars['count'] ) { $fields = 'COUNT(*)'; } else { - $fields = 'blog_id'; + $fields = "{$wpdb->blogs}.blog_id"; } // Parse site IDs for an IN clause. $site_id = absint( $this->query_vars['ID'] ); if ( ! empty( $site_id ) ) { - $this->sql_clauses['where']['ID'] = $wpdb->prepare( 'blog_id = %d', $site_id ); + $this->sql_clauses['where']['ID'] = $wpdb->prepare( "{$wpdb->blogs}.blog_id = %d", $site_id ); } // Parse site IDs for an IN clause. if ( ! empty( $this->query_vars['site__in'] ) ) { - $this->sql_clauses['where']['site__in'] = 'blog_id IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['site__in'] ) ) . ' )'; + $this->sql_clauses['where']['site__in'] = "{$wpdb->blogs}.blog_id IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['site__in'] ) ) . ' )'; } // Parse site IDs for a NOT IN clause. if ( ! empty( $this->query_vars['site__not_in'] ) ) { - $this->sql_clauses['where']['site__not_in'] = 'blog_id NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['site__not_in'] ) ) . ' )'; + $this->sql_clauses['where']['site__not_in'] = "{$wpdb->blogs}.blog_id NOT IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['site__not_in'] ) ) . ' )'; } $network_id = absint( $this->query_vars['network_id'] ); @@ -526,6 +571,17 @@ class WP_Site_Query { $join = ''; + if ( ! empty( $this->meta_query_clauses ) ) { + $join .= $this->meta_query_clauses['join']; + + // Strip leading 'AND'. + $this->sql_clauses['where']['meta_query'] = preg_replace( '/^\s*AND\s*/', '', $this->meta_query_clauses['where'] ); + + if ( ! $this->query_vars['count'] ) { + $groupby = "{$wpdb->blogs}.blog_id"; + } + } + $where = implode( ' AND ', $this->sql_clauses['where'] ); $pieces = array( 'fields', 'join', 'where', 'orderby', 'limits', 'groupby' ); @@ -675,10 +731,42 @@ class WP_Site_Query { $parsed = 'CHAR_LENGTH(path)'; break; case 'id': - $parsed = 'blog_id'; + $parsed = "{$wpdb->blogs}.blog_id"; break; } + if ( ! empty( $parsed ) || empty( $this->meta_query_clauses ) ) { + return $parsed; + } + + $meta_clauses = $this->meta_query->get_clauses(); + if ( empty( $meta_clauses ) ) { + return $parsed; + } + + $primary_meta_query = reset( $meta_clauses ); + if ( ! empty( $primary_meta_query['key'] ) && $primary_meta_query['key'] === $orderby ) { + $orderby = 'meta_value'; + } + + switch ( $orderby ) { + case 'meta_value': + if ( ! empty( $primary_meta_query['type'] ) ) { + $parsed = "CAST({$primary_meta_query['alias']}.meta_value AS {$primary_meta_query['cast']})"; + } else { + $parsed = "{$primary_meta_query['alias']}.meta_value"; + } + break; + case 'meta_value_num': + $parsed = "{$primary_meta_query['alias']}.meta_value+0"; + break; + default: + if ( isset( $meta_clauses[ $orderby ] ) ) { + $meta_clause = $meta_clauses[ $orderby ]; + $parsed = "CAST({$meta_clause['alias']}.meta_value AS {$meta_clause['cast']})"; + } + } + return $parsed; } diff --git a/tests/phpunit/tests/multisite/siteMeta.php b/tests/phpunit/tests/multisite/siteMeta.php index c313f63218..fec436b839 100644 --- a/tests/phpunit/tests/multisite/siteMeta.php +++ b/tests/phpunit/tests/multisite/siteMeta.php @@ -256,6 +256,122 @@ class Tests_Multisite_Site_Meta extends WP_UnitTestCase { get_site_meta( self::$site_id, 'foo', true ); $this->assertSame( $num_queries + 1, $wpdb->num_queries); } + + /** + * @ticket 40229 + */ + public function test_add_site_meta_should_bust_get_sites_cache() { + if ( ! is_site_meta_supported() ) { + $this->markTestSkipped( 'Tests only runs with the blogmeta database table installed' ); + } + + add_site_meta( self::$site_id, 'foo', 'bar' ); + + // Prime cache. + $found = get_sites( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) ); + + $this->assertEqualSets( array( self::$site_id ), $found ); + + add_site_meta( self::$site_id2, 'foo', 'bar' ); + + $found = get_sites( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) ); + + $this->assertEqualSets( array( self::$site_id, self::$site_id2 ), $found ); + } + + /** + * @ticket 40229 + */ + public function test_update_site_meta_should_bust_get_sites_cache() { + if ( ! is_site_meta_supported() ) { + $this->markTestSkipped( 'Tests only runs with the blogmeta database table installed' ); + } + + add_site_meta( self::$site_id, 'foo', 'bar' ); + add_site_meta( self::$site_id2, 'foo', 'baz' ); + + // Prime cache. + $found = get_sites( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) ); + + $this->assertEqualSets( array( self::$site_id ), $found ); + + update_site_meta( self::$site_id2, 'foo', 'bar' ); + + $found = get_sites( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) ); + + $this->assertEqualSets( array( self::$site_id, self::$site_id2 ), $found ); + } + + /** + * @ticket 40229 + */ + public function test_delete_site_meta_should_bust_get_sites_cache() { + if ( ! is_site_meta_supported() ) { + $this->markTestSkipped( 'Tests only runs with the blogmeta database table installed' ); + } + + add_site_meta( self::$site_id, 'foo', 'bar' ); + add_site_meta( self::$site_id2, 'foo', 'bar' ); + + // Prime cache. + $found = get_sites( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) ); + + $this->assertEqualSets( array( self::$site_id, self::$site_id2 ), $found ); + + delete_site_meta( self::$site_id2, 'foo', 'bar' ); + + $found = get_sites( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) ); + + $this->assertEqualSets( array( self::$site_id ), $found ); + } } endif; diff --git a/tests/phpunit/tests/multisite/siteQuery.php b/tests/phpunit/tests/multisite/siteQuery.php index b70de48baa..bac9269ff4 100644 --- a/tests/phpunit/tests/multisite/siteQuery.php +++ b/tests/phpunit/tests/multisite/siteQuery.php @@ -878,6 +878,193 @@ if ( is_multisite() ) : ); $this->assertEquals( $number_of_queries + 1, $wpdb->num_queries ); } + + /** + * @ticket 40229 + * @dataProvider data_wp_site_query_meta_query + */ + public function test_wp_site_query_meta_query( $query, $expected, $strict ) { + if ( ! is_site_meta_supported() ) { + $this->markTestSkipped( 'Tests only runs with the blogmeta database table installed' ); + } + + add_site_meta( self::$site_ids['wordpress.org/'], 'foo', 'foo' ); + add_site_meta( self::$site_ids['wordpress.org/foo/'], 'foo', 'bar' ); + add_site_meta( self::$site_ids['wordpress.org/foo/bar/'], 'foo', 'baz' ); + add_site_meta( self::$site_ids['make.wordpress.org/'], 'bar', 'baz' ); + add_site_meta( self::$site_ids['wordpress.org/'], 'numberfoo', 1 ); + add_site_meta( self::$site_ids['wordpress.org/foo/'], 'numberfoo', 2 ); + + $query['fields'] = 'ids'; + + $q = new WP_Site_Query(); + $found = $q->query( $query ); + + foreach ( $expected as $index => $domain_path ) { + $expected[ $index ] = self::$site_ids[ $domain_path ]; + } + + if ( $strict ) { + $this->assertEquals( $expected, $found ); + } else { + $this->assertEqualSets( $expected, $found ); + } + } + + public function data_wp_site_query_meta_query() { + return array( + array( + array( + 'meta_key' => 'foo', + ), + array( + 'wordpress.org/', + 'wordpress.org/foo/', + 'wordpress.org/foo/bar/', + ), + false, + ), + array( + array( + 'meta_key' => 'foo', + 'meta_value' => 'bar', + ), + array( + 'wordpress.org/foo/', + ), + false, + ), + array( + array( + 'meta_key' => 'foo', + 'meta_value' => array( 'bar', 'baz' ), + 'meta_compare' => 'IN', + ), + array( + 'wordpress.org/foo/', + 'wordpress.org/foo/bar/', + ), + false, + ), + array( + array( + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + array( + 'key' => 'numberfoo', + 'value' => 2, + 'type' => 'NUMERIC', + ), + ), + ), + array( + 'wordpress.org/foo/', + ), + false, + ), + array( + array( + 'meta_key' => 'foo', + 'orderby' => 'meta_value', + 'order' => 'ASC', + ), + array( + 'wordpress.org/foo/', + 'wordpress.org/foo/bar/', + 'wordpress.org/', + ), + true, + ), + array( + array( + 'meta_key' => 'foo', + 'orderby' => 'foo', + 'order' => 'ASC', + ), + array( + 'wordpress.org/foo/', + 'wordpress.org/foo/bar/', + 'wordpress.org/', + ), + true, + ), + array( + array( + 'meta_key' => 'numberfoo', + 'orderby' => 'meta_value_num', + 'order' => 'DESC', + ), + array( + 'wordpress.org/foo/', + 'wordpress.org/', + ), + true, + ), + array( + array( + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => array( 'foo', 'bar' ), + 'compare' => 'IN', + ), + array( + 'key' => 'numberfoo', + ), + ), + 'orderby' => array( 'meta_value' => 'ASC' ), + ), + array( + 'wordpress.org/foo/', + 'wordpress.org/', + ), + true, + ), + array( + array( + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => array( 'foo', 'bar' ), + 'compare' => 'IN', + ), + array( + 'key' => 'numberfoo', + ), + ), + 'orderby' => array( 'foo' => 'ASC' ), + ), + array( + 'wordpress.org/foo/', + 'wordpress.org/', + ), + true, + ), + array( + array( + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => array( 'foo', 'bar' ), + 'compare' => 'IN', + ), + 'my_subquery' => array( + 'key' => 'numberfoo', + ), + ), + 'orderby' => array( 'my_subquery' => 'DESC' ), + ), + array( + 'wordpress.org/foo/', + 'wordpress.org/', + ), + true, + ), + ); + } } endif;