diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 4b6d9de25fa11..b6482ab148a75 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -565,8 +565,7 @@ add_action( 'init', 'wp_initialize_site_preview_hooks', 1 ); // Calendar widget cache. -add_action( 'save_post', 'delete_get_calendar_cache' ); -add_action( 'delete_post', 'delete_get_calendar_cache' ); +add_action( 'clean_post_cache', 'delete_get_calendar_cache' ); add_action( 'update_option_start_of_week', 'delete_get_calendar_cache' ); add_action( 'update_option_gmt_offset', 'delete_get_calendar_cache' ); diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 47e2aeb2ebb05..736a59204297c 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -2341,12 +2341,18 @@ function get_calendar( $args = array() ) { ); wp_recursive_ksort( $cache_args ); - $key = md5( serialize( $cache_args ) ); - $cache = wp_cache_get( 'get_calendar', 'calendar' ); - if ( $cache && is_array( $cache ) && isset( $cache[ $key ] ) ) { + if ( ! wp_cache_supports( 'flush_group' ) ) { + $generation = wp_cache_get( 'get_calendar_generation', 'calendar' ); + $cache_args['_generation_'] = $generation ? (int) $generation : 0; + } + + $key = 'get_calendar_' . md5( serialize( $cache_args ) ); + $cache = wp_cache_get( $key, 'calendar' ); + + if ( false !== $cache ) { /** This filter is documented in wp-includes/general-template.php */ - $output = apply_filters( 'get_calendar', $cache[ $key ], $args ); + $output = apply_filters( 'get_calendar', $cache, $args ); if ( $args['display'] ) { echo $output; @@ -2356,10 +2362,6 @@ function get_calendar( $args = array() ) { return $output; } - if ( ! is_array( $cache ) ) { - $cache = array(); - } - $post_type = $args['post_type']; // Quick check. If we have no posts at all, abort! @@ -2376,8 +2378,7 @@ function get_calendar( $args = array() ) { ); if ( ! $gotsome ) { - $cache[ $key ] = ''; - wp_cache_set( 'get_calendar', $cache, 'calendar' ); + wp_cache_set( $key, '', 'calendar' ); return; } } @@ -2390,17 +2391,10 @@ function get_calendar( $args = array() ) { $thismonth = (int) $monthnum; $thisyear = (int) $year; } elseif ( ! empty( $w ) ) { - // We need to get the month from MySQL. $thisyear = (int) substr( $m, 0, 4 ); // It seems MySQL's weeks disagree with PHP's. $d = ( ( $w - 1 ) * 7 ) + 6; - $thismonth = (int) $wpdb->get_var( - $wpdb->prepare( - "SELECT DATE_FORMAT((DATE_ADD('%d0101', INTERVAL %d DAY) ), '%%m')", - $thisyear, - $d - ) - ); + $thismonth = (int) gmdate( 'm', strtotime( "{$thisyear}-01-01 + {$d} days" ) ); } elseif ( ! empty( $m ) ) { $thisyear = (int) substr( $m, 0, 4 ); if ( strlen( $m ) < 6 ) { @@ -2583,8 +2577,7 @@ function get_calendar( $args = array() ) { $calendar_output .= ' '; - $cache[ $key ] = $calendar_output; - wp_cache_set( 'get_calendar', $cache, 'calendar' ); + wp_cache_set( $key, $calendar_output, 'calendar' ); /** * Filters the HTML calendar output. @@ -2618,7 +2611,17 @@ function get_calendar( $args = array() ) { * @since 2.1.0 */ function delete_get_calendar_cache() { - wp_cache_delete( 'get_calendar', 'calendar' ); + if ( wp_cache_supports( 'flush_group' ) ) { + wp_cache_flush_group( 'calendar' ); + return; + } + + // Fallback for object cache implementations that do not support flushing groups. + // wp_cache_incr() returns false when the key does not exist in some backends + // (e.g. Memcached), so initialize the counter on the first invalidation. + if ( false === wp_cache_incr( 'get_calendar_generation', 1, 'calendar' ) ) { + wp_cache_add( 'get_calendar_generation', 1, 'calendar' ); + } } /** diff --git a/tests/phpunit/tests/general/getCalendar.php b/tests/phpunit/tests/general/getCalendar.php index a8a3d1361f494..f9b149233ce6f 100644 --- a/tests/phpunit/tests/general/getCalendar.php +++ b/tests/phpunit/tests/general/getCalendar.php @@ -183,7 +183,7 @@ public function test_get_calendar_caching_accounts_for_equivalent_args() { public function test_get_calendar_backwards_compatibility() { $first_calendar_html = get_echo( 'get_calendar', array( false ) ); - wp_cache_delete( 'get_calendar', 'calendar' ); + delete_get_calendar_cache(); $second_calendar_html = get_calendar( false, false ); @@ -192,4 +192,75 @@ public function test_get_calendar_backwards_compatibility() { $this->assertStringContainsString( 'assertSame( $first_calendar_html, $second_calendar_html, 'Both calendars should be identical' ); } + + /** + * Test that clean_post_cache invalidates the calendar cache. + * + * @ticket 61343 + */ + public function test_clean_post_cache_invalidates_calendar_cache() { + // Populate the cache. + get_echo( 'get_calendar' ); + + $num_queries_start = get_num_queries(); + get_echo( 'get_calendar' ); + $queries_cached = get_num_queries() - $num_queries_start; + + $this->assertSame( 0, $queries_cached, 'Second call should be cached with zero queries.' ); + + // Simulate clean_post_cache firing. + delete_get_calendar_cache(); + + $num_queries_start = get_num_queries(); + get_echo( 'get_calendar' ); + $queries_after_flush = get_num_queries() - $num_queries_start; + + $this->assertGreaterThan( 0, $queries_after_flush, 'After cache flush, queries should run again.' ); + } + + /** + * Test that the calendar uses individual cache keys per variation. + * + * @ticket 61343 + */ + public function test_calendar_uses_individual_cache_keys() { + // Generate calendar for 'post' type. + get_echo( 'get_calendar' ); + + // Generate calendar for 'page' type. + get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) ); + + $num_queries_start = get_num_queries(); + + // Both should be cached independently. + get_echo( 'get_calendar' ); + get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) ); + + $this->assertSame( 0, get_num_queries() - $num_queries_start, 'Both variations should be served from cache with zero queries.' ); + } + + /** + * Test that flush_group clears all calendar variations at once. + * + * @ticket 61343 + */ + public function test_flush_group_clears_all_variations() { + // Populate caches for two different post types. + get_echo( 'get_calendar' ); + get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) ); + + // Flush all calendar cache. + delete_get_calendar_cache(); + + $num_queries_start = get_num_queries(); + get_echo( 'get_calendar' ); + $queries_post = get_num_queries() - $num_queries_start; + + $num_queries_start = get_num_queries(); + get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) ); + $queries_page = get_num_queries() - $num_queries_start; + + $this->assertGreaterThan( 0, $queries_post, 'Post calendar should require queries after flush.' ); + $this->assertGreaterThan( 0, $queries_page, 'Page calendar should require queries after flush.' ); + } }