hardening 2.3: 2meet-data-optimizer 1.3.0 (flat-table DDL outside web requests, routed count cache, audit fixes) #7

Merged
wpdev merged 20 commits from hardening/2.3 into main 2026-09-27 18:19:18 +08:00
2 changed files with 48 additions and 2 deletions
Showing only changes of commit 7851f150fa - Show all commits
@@ -433,8 +433,11 @@ class TMDO_Post_Query_Router {
global $wpdb;
list( $count_sql, $excluded_sql ) = array_pad( (array) $count_sql, 2, '' );
$key = md5( $count_sql ) . ':' . wp_cache_get_last_changed( 'posts' );
$total = wp_cache_get( $key, self::COUNT_CACHE_GROUP );
// Keyword searches carry wpdb's placeholder escape for "%", which is new
// in every request: without it the key would never match again.
$stable = method_exists( $wpdb, 'remove_placeholder_escape' ) ? $wpdb->remove_placeholder_escape( $count_sql ) : $count_sql;
$key = md5( $stable ) . ':' . wp_cache_get_last_changed( 'posts' );
$total = wp_cache_get( $key, self::COUNT_CACHE_GROUP );
if ( false === $total ) {
$total = (int) $wpdb->get_var( $count_sql );
wp_cache_set( $key, $total, self::COUNT_CACHE_GROUP, HOUR_IN_SECONDS );
@@ -157,6 +157,49 @@ class PostQueryRouterFoundRowsTest extends TestCase {
$this->assertSame( '0', $count( '2' ), 'recounted' );
}
/**
* Keyword searches carry wpdb's placeholder escape for "%", which changes
* with every request: the cache key uses the SQL without it, or no search
* with a keyword ever hits the cached count.
*/
public function test_the_cached_count_survives_a_new_placeholder_escape(): void {
global $wpdb;
$real = $wpdb;
$wpdb = new class( $real ) {
public int $counts = 0;
public function __construct( private object $db ) {}
public function remove_placeholder_escape( string $sql ): string {
return (string) preg_replace( '/\{[0-9a-f]{8}\}/', '%', $sql );
}
public function __call( string $name, array $args ) {
if ( 'get_var' === $name && str_contains( (string) $args[0], 'COUNT(*)' ) ) {
++$this->counts;
}
return $this->db->$name( ...$args );
}
public function __get( string $name ) {
return $this->db->$name;
}
};
try {
foreach ( array( '{1a2b3c4d}', '{9f8e7d6c}' ) as $escape ) { // Two requests.
list( $router, $query, $request ) = $this->routed();
$request = str_replace( ' GROUP BY', " AND '{$escape}' = '{$escape}' GROUP BY", $request );
$router->posts_request( $request, $query );
$this->assertSame( '2', $real->get_var( $router->found_posts_query( 'SELECT FOUND_ROWS()', $query ) ) );
}
$counts = $wpdb->counts;
} finally {
$wpdb = $real;
}
$this->assertSame( 1, $counts, 'counted once' );
}
public function test_flat_writes_and_deletes_move_posts_last_changed(): void {
$GLOBALS['_wp_current_filter'] = 'update_post_metadata';
$before = wp_cache_get_last_changed( 'posts' );