hardening 2.1: 2meet-data-optimizer 1.1.0 (search-index mode, staged rollout) #4
@@ -104,7 +104,10 @@ class TMDO_Zone_Router {
|
|||||||
*
|
*
|
||||||
* Callers only need post_id and post_type; zone enumeration and per-zone
|
* Callers only need post_id and post_type; zone enumeration and per-zone
|
||||||
* error isolation are handled here. Hot and Cold are skipped when the
|
* error isolation are handled here. Hot and Cold are skipped when the
|
||||||
* post_type has no registered fields in that zone (table may not exist).
|
* post_type has no registered fields in that zone, or when the zone table
|
||||||
|
* was never created (fields are registered long before a module runs).
|
||||||
|
* Module state is not checked: a table left by a rolled-back module still
|
||||||
|
* gets its rows for deleted posts removed.
|
||||||
*
|
*
|
||||||
* @param int $post_id Post ID being deleted.
|
* @param int $post_id Post ID being deleted.
|
||||||
* @param string $post_type Post type slug.
|
* @param string $post_type Post type slug.
|
||||||
@@ -113,7 +116,7 @@ class TMDO_Zone_Router {
|
|||||||
public static function delete_post( int $post_id, string $post_type ): void {
|
public static function delete_post( int $post_id, string $post_type ): void {
|
||||||
$registry = TMDO_Schema_Registry::instance();
|
$registry = TMDO_Schema_Registry::instance();
|
||||||
|
|
||||||
if ( ! empty( $registry->get_hot_columns( $post_type ) ) ) {
|
if ( ! empty( $registry->get_hot_columns( $post_type ) ) && TMDO_Schema_Manager::table_exists( TMDO_Zone_Hot::table( $post_type ) ) ) {
|
||||||
try {
|
try {
|
||||||
TMDO_Zone_Hot::delete( $post_id, $post_type );
|
TMDO_Zone_Hot::delete( $post_id, $post_type );
|
||||||
} catch ( \Throwable $e ) {
|
} catch ( \Throwable $e ) {
|
||||||
@@ -121,7 +124,7 @@ class TMDO_Zone_Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! empty( $registry->get_cold_meta_keys( $post_type ) ) ) {
|
if ( ! empty( $registry->get_cold_meta_keys( $post_type ) ) && TMDO_Schema_Manager::table_exists( TMDO_Zone_Cold::table( $post_type ) ) ) {
|
||||||
try {
|
try {
|
||||||
TMDO_Zone_Cold::delete( $post_id, $post_type );
|
TMDO_Zone_Cold::delete( $post_id, $post_type );
|
||||||
} catch ( \Throwable $e ) {
|
} catch ( \Throwable $e ) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Exit code 0 when every test passed, 1 otherwise. Output per test:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `shadow_write` | writes in shadow_read reach native meta (user, term, HivePress listing) |
|
| `shadow_write` | writes in shadow_read reach native meta (user, term, HivePress listing) |
|
||||||
| `serialized` | array meta values are stored serialized in flat tables, never "Array" |
|
| `serialized` | array meta values are stored serialized in flat tables, never "Array" |
|
||||||
|
| `zero_side_effect` | zone tables that were never created are never queried on save / delete |
|
||||||
|
|
||||||
## Tools (`tools/`)
|
## Tools (`tools/`)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Suite: zone tables that were never created are never queried.
|
||||||
|
*
|
||||||
|
* Bug (dev36, 2026-09-26): the HivePress AddOn registers cold fields, so every
|
||||||
|
* save_post / loop_start (PR-054) and every post delete (PR-054b) queried
|
||||||
|
* wp_wpdo_cold_{post_type} although the cold module was idle and the table
|
||||||
|
* did not exist ("table doesn't exist" DB errors on each write).
|
||||||
|
*
|
||||||
|
* @package WP_Data_Optimizer
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
TMDO_Live::test(
|
||||||
|
'saving and deleting an hp_listing does not touch missing zone tables',
|
||||||
|
static function () {
|
||||||
|
global $wpdb;
|
||||||
|
$tables = array( TMDO_Zone_Cold::table( 'hp_listing' ), TMDO_Zone_Hot::table( 'hp_listing' ) );
|
||||||
|
foreach ( $tables as $table ) {
|
||||||
|
if ( TMDO_Schema_Manager::table_exists( $table ) ) {
|
||||||
|
throw new TMDO_Live_Skip( "{$table} exists on this site" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$touched = array();
|
||||||
|
$spy = static function ( $sql ) use ( $tables, &$touched ) {
|
||||||
|
foreach ( $tables as $table ) {
|
||||||
|
if ( false !== strpos( $sql, $table ) && 0 !== stripos( ltrim( $sql ), 'SHOW TABLES' ) ) {
|
||||||
|
$touched[] = $sql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $sql;
|
||||||
|
};
|
||||||
|
add_filter( 'query', $spy );
|
||||||
|
try {
|
||||||
|
$id = TMDO_Live_Fixtures::post( 'hp_listing' );
|
||||||
|
wp_update_post( array( 'ID' => $id, 'post_title' => 'tmdo-live-updated' ) );
|
||||||
|
wp_delete_post( $id, true );
|
||||||
|
} finally {
|
||||||
|
remove_filter( 'query', $spy );
|
||||||
|
}
|
||||||
|
|
||||||
|
tmdo_live_assert_same( array(), $touched, 'queries on missing zone tables' );
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -21,9 +21,69 @@ class ZoneRouterTest extends TestCase {
|
|||||||
self::$last_query = '';
|
self::$last_query = '';
|
||||||
self::$last_delete = [];
|
self::$last_delete = [];
|
||||||
self::$get_var_return = null;
|
self::$get_var_return = null;
|
||||||
|
WPDO_Schema_Manager::flush_table_exists_cache();
|
||||||
$this->setup_wpdb_mock();
|
$this->setup_wpdb_mock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $wpdb mock recording delete() tables; SHOW TABLES reports $existing.
|
||||||
|
*
|
||||||
|
* @param string[] $existing Tables that exist.
|
||||||
|
*/
|
||||||
|
private function delete_logger( array &$captured, array $existing ): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb = new class( $captured, $existing ) {
|
||||||
|
public string $prefix = 'wp_';
|
||||||
|
public array $log;
|
||||||
|
public function __construct( array &$ref, private array $existing ) { $this->log = &$ref; }
|
||||||
|
public function prepare( string $s, mixed ...$a ): string { return str_replace( '%s', (string) ( $a[0] ?? '' ), $s ); }
|
||||||
|
public function query( string $s ): int { return 1; }
|
||||||
|
public function get_var( string $s ): mixed {
|
||||||
|
foreach ( $this->existing as $t ) {
|
||||||
|
if ( "SHOW TABLES LIKE {$t}" === $s ) {
|
||||||
|
return $t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public function insert( string $t, array $d, mixed $f = null ): int { return 1; }
|
||||||
|
public function delete( string $table, array $where, array $formats ): void {
|
||||||
|
$this->log[] = $table;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function register_hot_and_cold(): void {
|
||||||
|
$sr = new ReflectionClass( WPDO_Schema_Registry::class );
|
||||||
|
$sr->getProperty( 'instance' )->setValue( null, null );
|
||||||
|
WPDO_Schema_Registry::instance()->register( 'test', array( 'post_type' => 'hp_listing', 'meta_key' => 'hp_price', 'zone' => 'hot', 'column' => 'hp_price', 'type' => 'decimal' ) );
|
||||||
|
WPDO_Schema_Registry::instance()->register( 'test', array( 'post_type' => 'hp_listing', 'meta_key' => 'hp_images', 'zone' => 'cold' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registered fields do not mean the zone tables exist (zero side effect). */
|
||||||
|
public function test_delete_post_skips_hot_and_cold_tables_that_do_not_exist(): void {
|
||||||
|
$this->register_hot_and_cold();
|
||||||
|
$captured = array();
|
||||||
|
$this->delete_logger( $captured, array() );
|
||||||
|
|
||||||
|
WPDO_Zone_Router::delete_post( 3, 'hp_listing' );
|
||||||
|
|
||||||
|
$this->assertNotContains( 'wp_wpdo_hot_hp_listing', $captured );
|
||||||
|
$this->assertNotContains( 'wp_wpdo_cold_hp_listing', $captured );
|
||||||
|
$this->assertContains( 'wp_wpdo_warm', $captured );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_post_deletes_cold_rows_when_the_table_exists(): void {
|
||||||
|
$this->register_hot_and_cold();
|
||||||
|
$captured = array();
|
||||||
|
$this->delete_logger( $captured, array( 'wp_wpdo_cold_hp_listing' ) );
|
||||||
|
|
||||||
|
WPDO_Zone_Router::delete_post( 4, 'hp_listing' );
|
||||||
|
|
||||||
|
$this->assertContains( 'wp_wpdo_cold_hp_listing', $captured );
|
||||||
|
$this->assertNotContains( 'wp_wpdo_hot_hp_listing', $captured );
|
||||||
|
}
|
||||||
|
|
||||||
private function setup_wpdb_mock(): void {
|
private function setup_wpdb_mock(): void {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb = new class {
|
$wpdb = new class {
|
||||||
@@ -204,19 +264,7 @@ class ZoneRouterTest extends TestCase {
|
|||||||
) );
|
) );
|
||||||
|
|
||||||
$captured = array();
|
$captured = array();
|
||||||
global $wpdb;
|
$this->delete_logger( $captured, array( 'wp_wpdo_hot_hp_listing' ) );
|
||||||
$wpdb = new class( $captured ) {
|
|
||||||
public string $prefix = 'wp_';
|
|
||||||
public array $log;
|
|
||||||
public function __construct( array &$ref ) { $this->log = &$ref; }
|
|
||||||
public function prepare( string $s, mixed ...$a ): string { return $s; }
|
|
||||||
public function query( string $s ): int { return 1; }
|
|
||||||
public function get_var( string $s ): mixed { return null; }
|
|
||||||
public function insert( string $t, array $d, mixed $f = null ): int { return 1; }
|
|
||||||
public function delete( string $table, array $where, array $formats ): void {
|
|
||||||
$this->log[] = $table;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
WPDO_Zone_Router::delete_post( 10, 'hp_listing' );
|
WPDO_Zone_Router::delete_post( 10, 'hp_listing' );
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user