This issue was generated by AI.
The "Delete Cache" button in the admin bar only deletes supercache files. It never touches the legacy wp-cache files, so on any URL those are what serve the page, clicking it appears to do nothing and the visitor keeps getting the stale copy.
Easiest way to see it: load a cached post with a tracking parameter on the end, say /some-post/?utm_source=test, and click Delete Cache. Nothing is deleted, and the debug log shows the page served from cache again on the next request. It looks like the button is broken, and from the user's point of view it is.
Why
Two things combine.
wp_cache_serve_cache_file() refuses to serve a supercache file for any URL carrying a query string, at wp-cache-phase2.php:198:
} elseif ( wpsc_is_get_query() ) {
wp_cache_debug( 'GET array not empty. Cannot serve a supercache file. ' . wpsc_dump_get_request() );
return false;
}
So /some-post/?utm_source=test was never being served out of supercache/<host>/some-post/. What serves it is the legacy wp-cache file, keyed on the full request URI including the query.
And wpsc_delete_cache_directory() in inc/delete-cache-button.php:146 only ever resolves a supercache directory and calls wpsc_delete_files() on it. There is no code path in it that deletes a legacy wp-cache file. So the one thing actually serving that request is the one thing the button cannot clear.
This is not specific to query strings. A plain permalink normally has both a supercache file and a legacy wp-cache file, and the button clears the first and leaves the second. It only becomes visible when the legacy file is the one being served, which is what a query string guarantees.
What it should do
Delete every cache file for that one URL, and nothing below it.
- Deleting the cache on the front page deletes the front page only, not the whole site.
- Deleting the cache on a category page does not delete child categories.
The supercache half already behaves correctly. wpsc_delete_files() at wp-cache-phase2.php:1132 iterates with is_file() and never recurses, and its trailing @rmdir() fails harmlessly when subdirectories exist, so children survive. That behaviour should be preserved, not replaced with prune_super_cache(), which does recurse.
The missing half is the legacy files.
Suggested fix
The plugin already knows how to do this. wp_cache_post_change() at wp-cache-phase2.php:3594 opens $blog_cache_dir, decodes each meta file, and matches on the metadata before unlinking the pair:
$meta = json_decode( wp_cache_get_legacy_cache( $blog_cache_dir . 'meta/' . $file ), true );
...
@unlink( $blog_cache_dir . 'meta/' . $file );
@unlink( $blog_cache_dir . $file );
wpsc_delete_cache_directory() wants the same scan, matched on the URI rather than on a post ID. $wp_cache_meta['uri'] is written at wp-cache-phase2.php:3059 as the host followed by the full request URI:
$wp_cache_meta['uri'] = $WPSC_HTTP_HOST . preg_replace( '/[ <>\'\"\r\n\t\(\)]/', '', $wp_cache_request_uri );
Because that includes the query string, one path can have many legacy entries: the bare URL, ?utm_source=a, ?utm_source=b, and so on. All of them are the same page and all should go.
So the match is on the path component only, compared for equality after the query is removed:
/some-post/ matches /some-post/, /some-post/?utm_source=a and /some-post/?fbclid=b
/some-post/ does not match /some-post/child/, which is what keeps children out of it
Worth checking while implementing:
$blog_cache_dir is not $cache_path on multisite, so use the global rather than rebuilding the path.
- The gzipped variants and any
.html-suffixed legacy files in that directory belong to the same URL and should go with it.
- The existing
$_POST['admin'] == 1 branch is the separate "delete the whole site cache" button and should be left alone.
- The nonce is verified against the raw posted path, so it already covers the URL including its query string.
wp_cache_confirm_delete() and the check that the resolved path sits inside the supercache directory guard the directory half. The legacy half needs its own guard, since it is matching on metadata rather than resolving a path, and the value being matched comes from the request.
Where this came from
Found while testing #1084, which fixed cache clearing for non-ASCII slugs. Not a regression from that PR: the behaviour is identical in 3.1.1 and on trunk, because the old sanitize_text_field() call left ? and = alone and realpath() failed on the same directory that does not exist. It is a long-standing gap rather than anything new.
The "Delete Cache" button in the admin bar only deletes supercache files. It never touches the legacy wp-cache files, so on any URL those are what serve the page, clicking it appears to do nothing and the visitor keeps getting the stale copy.
Easiest way to see it: load a cached post with a tracking parameter on the end, say
/some-post/?utm_source=test, and click Delete Cache. Nothing is deleted, and the debug log shows the page served from cache again on the next request. It looks like the button is broken, and from the user's point of view it is.Why
Two things combine.
wp_cache_serve_cache_file()refuses to serve a supercache file for any URL carrying a query string, atwp-cache-phase2.php:198:So
/some-post/?utm_source=testwas never being served out ofsupercache/<host>/some-post/. What serves it is the legacy wp-cache file, keyed on the full request URI including the query.And
wpsc_delete_cache_directory()ininc/delete-cache-button.php:146only ever resolves a supercache directory and callswpsc_delete_files()on it. There is no code path in it that deletes a legacy wp-cache file. So the one thing actually serving that request is the one thing the button cannot clear.This is not specific to query strings. A plain permalink normally has both a supercache file and a legacy wp-cache file, and the button clears the first and leaves the second. It only becomes visible when the legacy file is the one being served, which is what a query string guarantees.
What it should do
Delete every cache file for that one URL, and nothing below it.
The supercache half already behaves correctly.
wpsc_delete_files()atwp-cache-phase2.php:1132iterates withis_file()and never recurses, and its trailing@rmdir()fails harmlessly when subdirectories exist, so children survive. That behaviour should be preserved, not replaced withprune_super_cache(), which does recurse.The missing half is the legacy files.
Suggested fix
The plugin already knows how to do this.
wp_cache_post_change()atwp-cache-phase2.php:3594opens$blog_cache_dir, decodes each meta file, and matches on the metadata before unlinking the pair:wpsc_delete_cache_directory()wants the same scan, matched on the URI rather than on a post ID.$wp_cache_meta['uri']is written atwp-cache-phase2.php:3059as the host followed by the full request URI:Because that includes the query string, one path can have many legacy entries: the bare URL,
?utm_source=a,?utm_source=b, and so on. All of them are the same page and all should go.So the match is on the path component only, compared for equality after the query is removed:
/some-post/matches/some-post/,/some-post/?utm_source=aand/some-post/?fbclid=b/some-post/does not match/some-post/child/, which is what keeps children out of itWorth checking while implementing:
$blog_cache_diris not$cache_pathon multisite, so use the global rather than rebuilding the path..html-suffixed legacy files in that directory belong to the same URL and should go with it.$_POST['admin'] == 1branch is the separate "delete the whole site cache" button and should be left alone.wp_cache_confirm_delete()and the check that the resolved path sits inside the supercache directory guard the directory half. The legacy half needs its own guard, since it is matching on metadata rather than resolving a path, and the value being matched comes from the request.Where this came from
Found while testing #1084, which fixed cache clearing for non-ASCII slugs. Not a regression from that PR: the behaviour is identical in 3.1.1 and on trunk, because the old
sanitize_text_field()call left?and=alone andrealpath()failed on the same directory that does not exist. It is a long-standing gap rather than anything new.