diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 6faeab2c27de3..b4ca1092f22cc 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -1007,8 +1007,22 @@ function redirect_guess_404_permalink() { $where .= $wpdb->prepare( ' AND DAYOFMONTH(post_date) = %d', get_query_var( 'day' ) ); } - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $post_id = $wpdb->get_var( "SELECT ID FROM $wpdb->posts WHERE $where AND post_status IN ('" . implode( "', '", esc_sql( $publicly_viewable_statuses ) ) . "')" ); + $query = "SELECT ID FROM $wpdb->posts WHERE $where AND post_status IN ('" . implode( "', '", esc_sql( $publicly_viewable_statuses ) ) . "')"; + + $key = md5( $query ); + $last_changed = wp_cache_get_last_changed( 'posts' ); + $cache_key = "redirect_guess_404_permalink:$key"; + $cache = wp_cache_get_salted( $cache_key, 'post-queries', $last_changed ); + + if ( false !== $cache ) { + $post_id = $cache; + } else { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $post_id = (int) $wpdb->get_var( $query ); + + // We cache misses as well as hits. + wp_cache_set_salted( $cache_key, $post_id, 'post-queries', $last_changed ); + } if ( ! $post_id ) { return false; diff --git a/tests/phpunit/tests/canonical.php b/tests/phpunit/tests/canonical.php index 886b09312910e..d8bc76c6d87c6 100644 --- a/tests/phpunit/tests/canonical.php +++ b/tests/phpunit/tests/canonical.php @@ -416,6 +416,121 @@ public function data_redirect_guess_404_permalink_post_types() { ); } + /** + * @ticket 64250 + * + * @covers ::redirect_guess_404_permalink + */ + public function test_redirect_guess_404_permalink_cache() { + $post = self::factory()->post->create( + array( + 'post_title' => 'redirect-guess-404-permalink-cache', + ) + ); + + $this->go_to( 'redirect-guess-404-permalink-cach' ); + + $first_run = redirect_guess_404_permalink(); + $this->assertSame( get_permalink( $post ), $first_run, 'Did not guess the correct permalink on first run.' ); + + $num_queries = get_num_queries(); + $second_run = redirect_guess_404_permalink(); + $this->assertSame( $first_run, $second_run, 'Result changed between cached and uncached run.' ); + $this->assertSame( $num_queries, get_num_queries(), 'A cached lookup performed an additional database query.' ); + } + + /** + * @ticket 64250 + * + * @covers ::redirect_guess_404_permalink + */ + public function test_redirect_guess_404_permalink_cache_misses_are_cached() { + $this->go_to( 'redirect-guess-404-permalink-no-such-post' ); + + $this->assertFalse( redirect_guess_404_permalink(), 'Expected no match for a nonexistent slug.' ); + + $num_queries = get_num_queries(); + $this->assertFalse( redirect_guess_404_permalink() ); + $this->assertSame( $num_queries, get_num_queries(), 'A cached "not found" result performed an additional database query.' ); + } + + /** + * @ticket 64250 + * + * @covers ::redirect_guess_404_permalink + */ + public function test_redirect_guess_404_permalink_cache_invalidated_on_new_matching_post() { + $this->go_to( 'redirect-guess-404-permalink-new-post' ); + + // Prime a "not found" cache entry. + $this->assertFalse( redirect_guess_404_permalink() ); + + $post = self::factory()->post->create( + array( + 'post_title' => 'redirect-guess-404-permalink-new-post', + ) + ); + + $num_queries = get_num_queries(); + $this->assertSame( get_permalink( $post ), redirect_guess_404_permalink(), 'Newly created matching post was not found after cache invalidation.' ); + $this->assertSame( 1, get_num_queries() - $num_queries, 'Expected exactly one new query after the posts cache was invalidated.' ); + } + + /** + * @ticket 64250 + * + * @covers ::redirect_guess_404_permalink + */ + public function test_redirect_guess_404_permalink_cache_invalidated_on_post_delete() { + $post = self::factory()->post->create( + array( + 'post_title' => 'redirect-guess-404-permalink-delete-me', + ) + ); + + $this->go_to( 'redirect-guess-404-permalink-delete-m' ); + + $this->assertSame( get_permalink( $post ), redirect_guess_404_permalink() ); + + wp_delete_post( $post, true ); + + $num_queries = get_num_queries(); + $this->assertFalse( redirect_guess_404_permalink(), 'Deleted post should no longer be guessed after cache invalidation.' ); + $this->assertSame( 1, get_num_queries() - $num_queries, 'Expected exactly one new query after the posts cache was invalidated by deletion.' ); + } + + /** + * @ticket 64250 + * + * @covers ::redirect_guess_404_permalink + */ + public function test_redirect_guess_404_permalink_cache_keys_do_not_collide() { + $post_post = self::factory()->post->create( + array( + 'post_title' => 'redirect-guess-collision', + 'post_type' => 'post', + ) + ); + $page_post = self::factory()->post->create( + array( + 'post_title' => 'redirect-guess-collision', + 'post_type' => 'page', + ) + ); + + $this->go_to( '/?name=redirect-guess-collisio&post_type=post' ); + $this->assertSame( get_permalink( $post_post ), redirect_guess_404_permalink() ); + + $this->go_to( '/?name=redirect-guess-collisio&post_type=page' ); + $this->assertSame( get_permalink( $page_post ), redirect_guess_404_permalink(), 'Different post_type query var produced a colliding cached result.' ); + + // Re-run both to confirm both are independently cached and correct. + $this->go_to( '/?name=redirect-guess-collisio&post_type=post' ); + $num_queries = get_num_queries(); + $this->assertSame( get_permalink( $post_post ), redirect_guess_404_permalink() ); + $this->assertSame( $num_queries, get_num_queries() ); + } + /** * @ticket 43745 */