diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 601ff077a2..511c4a7eec 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3106,41 +3106,8 @@ class WP_Query { */ $id_query_is_cacheable = ! str_contains( strtoupper( $orderby ), ' RAND(' ); if ( $q['cache_results'] && $id_query_is_cacheable ) { - $cache_args = $q; - - unset( - $cache_args['suppress_filters'], - $cache_args['cache_results'], - $cache_args['fields'], - $cache_args['update_post_meta_cache'], - $cache_args['update_post_term_cache'], - $cache_args['lazy_load_term_meta'], - $cache_args['update_menu_item_cache'], - $cache_args['search_orderby_title'] - ); - $new_request = str_replace( $fields, "{$wpdb->posts}.*", $this->request ); - $new_request = $wpdb->remove_placeholder_escape( $new_request ); - $key = md5( serialize( $cache_args ) . $new_request ); - - $last_changed = wp_cache_get_last_changed( 'posts' ); - if ( ! empty( $this->tax_query->queries ) ) { - $last_changed .= wp_cache_get_last_changed( 'terms' ); - } - - $cache_key = "wp_query:$key:$last_changed"; - - /** - * Filters query cache key. - * - * @since 6.1.0 - * - * @param string $cache_key Cache key. - * @param array $cache_args Query args used to generate the cache key. - * @param string $new_request SQL Query. - * @param WP_Query $query The WP_Query instance. - */ - $cache_key = apply_filters( 'wp_query_cache_key', $cache_key, $cache_args, $new_request, $this ); + $cache_key = $this->generate_cache_key( $q, $new_request ); $cache_found = false; if ( null === $this->posts ) { @@ -4755,6 +4722,62 @@ class WP_Query { return $elements; } + + /** + * Generate cache key. + * + * @since 6.1.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; + + unset( + $args['cache_results'], + $args['fields'], + $args['lazy_load_term_meta'], + $args['update_post_meta_cache'], + $args['update_post_term_cache'], + $args['update_menu_item_cache'], + $args['suppress_filters'] + ); + + $placeholder = $wpdb->placeholder_escape(); + array_walk_recursive( + $args, + /* + * Replace wpdb placeholders with the string used in the database + * query to avoid unreachable cache keys. This is necessary because + * the placeholder is randomly generated in each request. + * + * $value is passed by reference to allow it to be modified. + * array_walk_recursive() does not return an array. + */ + function ( &$value ) use ( $wpdb, $placeholder ) { + if ( is_string( $value ) && str_contains( $value, $placeholder ) ) { + $value = $wpdb->remove_placeholder_escape( $value ); + } + } + ); + + // Replace wpdb placeholder in the SQL statement used by the cache key. + $sql = $wpdb->remove_placeholder_escape( $sql ); + $key = md5( serialize( $args ) . $sql ); + + $last_changed = wp_cache_get_last_changed( 'posts' ); + if ( ! empty( $this->tax_query->queries ) ) { + $last_changed .= wp_cache_get_last_changed( 'terms' ); + } + + return "wp_query:$key:$last_changed"; + } + /** * After looping through a nested query, this function * restores the $post global to the current post in this query. diff --git a/tests/phpunit/tests/query/cacheResults.php b/tests/phpunit/tests/query/cacheResults.php index 43c8fc695a..d133dd862c 100644 --- a/tests/phpunit/tests/query/cacheResults.php +++ b/tests/phpunit/tests/query/cacheResults.php @@ -34,14 +34,20 @@ class Test_Query_CacheResults extends WP_UnitTestCase { public static $author_id; /** - * @var array + * For testing test_generate_cache_key() includes a test containing the + * placeholder within the generated SQL query. + * + * @var bool */ - protected $cache_args; + public static $sql_placeholder_cache_key_tested = false; /** - * @var string + * For testing test_generate_cache_key() includes a test containing the + * placeholder within the generated WP_Query variables. + * + * @var bool */ - protected $new_request; + public static $wp_query_placeholder_cache_key_tested = false; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { // Make some post objects. @@ -67,11 +73,102 @@ class Test_Query_CacheResults extends WP_UnitTestCase { ); } - function set_up() { - parent::set_up(); - $this->cache_args = null; - $this->new_request = null; - add_filter( 'wp_query_cache_key', array( $this, 'filter_wp_query_cache_key' ), 15, 3 ); + /** + * Ensure cache keys are generated without WPDB placeholders. + * + * @ticket 56802 + * + * @covers WP_Query::generate_cache_key + * + * @dataProvider data_query_cache + */ + public function test_generate_cache_key( $args ) { + global $wpdb; + $query1 = new WP_Query(); + $query1->query( $args ); + + $query_vars = $query1->query_vars; + $request = $query1->request; + $request_no_placeholder = $wpdb->remove_placeholder_escape( $request ); + + $this->assertStringNotContainsString( $wpdb->placeholder_escape(), $request_no_placeholder, 'Placeholder escape should be removed from the modified request.' ); + + if ( str_contains( $request, $wpdb->placeholder_escape() ) ) { + self::$sql_placeholder_cache_key_tested = true; + } + + if ( str_contains( serialize( $query_vars ), $wpdb->placeholder_escape() ) ) { + self::$wp_query_placeholder_cache_key_tested = true; + } + + $reflection = new ReflectionMethod( $query1, 'generate_cache_key' ); + $reflection->setAccessible( true ); + + $cache_key_1 = $reflection->invoke( $query1, $query_vars, $request ); + $cache_key_2 = $reflection->invoke( $query1, $query_vars, $request_no_placeholder ); + + $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs when using wpdb placeholder.' ); + } + + /** + * Ensure cache keys tests include WPDB placeholder in SQL Query. + * + * @ticket 56802 + * + * @covers WP_Query::generate_cache_key + * + * @depends test_generate_cache_key + */ + public function test_sql_placeholder_cache_key_tested() { + $this->assertTrue( self::$sql_placeholder_cache_key_tested, 'Cache key containing WPDB placeholder in SQL query was not tested.' ); + } + + /** + * Ensure cache keys tests include WPDB placeholder in WP_Query arguments. + * + * This test mainly covers the search query which generates the `search_orderby_title` + * query_var in WP_Query. + * + * @ticket 56802 + * + * @covers WP_Query::generate_cache_key + * + * @depends test_generate_cache_key + */ + public function test_wp_query_placeholder_cache_key_tested() { + $this->assertTrue( self::$wp_query_placeholder_cache_key_tested, 'Cache key containing WPDB placeholder in WP_Query arguments was not tested.' ); + } + + /** + * Ensure cache keys are generated without WPDB placeholders. + * + * @ticket 56802 + * + * @covers WP_Query::generate_cache_key + */ + public function test_generate_cache_key_placeholder() { + global $wpdb; + $query1 = new WP_Query(); + $query1->query( array() ); + + $query_vars = $query1->query_vars; + $request = $query1->request; + $query_vars['test']['nest'] = '%'; + $query_vars['test2']['nest']['nest']['nest'] = '%'; + $this->assertStringNotContainsString( $wpdb->placeholder_escape(), serialize( $query_vars ), 'Query vars should not contain the wpdb placeholder.' ); + + $reflection = new ReflectionMethod( $query1, 'generate_cache_key' ); + $reflection->setAccessible( true ); + + $cache_key_1 = $reflection->invoke( $query1, $query_vars, $request ); + + $query_vars['test']['nest'] = $wpdb->placeholder_escape(); + $query_vars['test2']['nest']['nest']['nest'] = $wpdb->placeholder_escape(); + $this->assertStringContainsString( $wpdb->placeholder_escape(), serialize( $query_vars ), 'Query vars should not contain the wpdb placeholder.' ); + + $cache_key_2 = $reflection->invoke( $query1, $query_vars, $request ); + + $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs when using wpdb placeholder.' ); } /** @@ -79,16 +176,9 @@ class Test_Query_CacheResults extends WP_UnitTestCase { * @ticket 22176 */ public function test_query_cache( $args ) { - global $wpdb; - $query1 = new WP_Query(); $posts1 = $query1->query( $args ); - $placeholder = $wpdb->placeholder_escape(); - $this->assertNotEmpty( $this->new_request, 'Check new request is not empty' ); - $this->assertStringNotContainsString( $placeholder, $this->new_request, 'Check if request does not contain placeholder' ); - $this->assertStringNotContainsString( $placeholder, wp_json_encode( $this->cache_args ), 'Check if cache arrays does not contain placeholder' ); - $queries_before = get_num_queries(); $query2 = new WP_Query(); $posts2 = $query2->query( $args ); @@ -227,6 +317,40 @@ class Test_Query_CacheResults extends WP_UnitTestCase { ), ), ), + 'cache nested meta query search' => array( + 'args' => array( + 'cache_results' => true, + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => 'color', + 'value' => '00', + 'compare' => 'LIKE', + ), + array( + 'relation' => 'OR', + array( + 'key' => 'color', + 'value' => '00', + 'compare' => 'LIKE', + ), + array( + 'relation' => 'AND', + array( + 'key' => 'wp_test_suite', + 'value' => '56802', + 'compare' => 'LIKE', + ), + array( + 'key' => 'wp_test_suite_too', + 'value' => '56802', + 'compare' => 'LIKE', + ), + ), + ), + ), + ), + ), 'cache meta query not search' => array( 'args' => array( 'cache_results' => true, @@ -887,13 +1011,6 @@ class Test_Query_CacheResults extends WP_UnitTestCase { $this->assertNotSame( $query1->found_posts, $query2->found_posts ); } - public function filter_wp_query_cache_key( $cache_key, $cache_args, $new_request ) { - $this->cache_args = $cache_args; - $this->new_request = $new_request; - - return $cache_key; - } - /** * @ticket 22176 */