d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
230 lines
7.3 KiB
PHP
230 lines
7.3 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_Term_Comment_Garbage_Filter — Block known-garbage writes to
|
|
* wp_termmeta / wp_commentmeta at the metadata filter layer (v2.12.1 Phase 1).
|
|
*
|
|
* Phase 0 (v2.12.0) provided cleanup CLI to delete historical garbage. This
|
|
* filter prevents the same garbage from accumulating again by intercepting
|
|
* writes via WordPress metadata filters (`add_term_metadata`, etc.) and
|
|
* silently dropping them — short-circuiting the database INSERT entirely.
|
|
*
|
|
* Targets (must align with TMDO_Termmeta_Cleaner / TMDO_Commentmeta_Cleaner):
|
|
*
|
|
* wp_termmeta + wp_commentmeta:
|
|
* - meta_key matching `_wxr_import_*` (WordPress importer residue —
|
|
* written once during WXR import, never read afterward)
|
|
* - meta_key matching `_2meet_demo_*` (project-specific demo markers,
|
|
* safe to drop and re-seed)
|
|
*
|
|
* wp_commentmeta only (orphan post-meta keys):
|
|
* - 8 hardcoded keys from TMDO_Commentmeta_Cleaner::ORPHAN_POST_META_KEYS
|
|
* — these are bugs / typos writing post-domain meta to comment table
|
|
*
|
|
* Read paths are NOT filtered. Existing rows in wp_*meta still resolve normally
|
|
* via standard WP metadata API; once cleanup CLI runs, reads naturally return
|
|
* empty. This minimizes risk of breaking any reader code that still expects
|
|
* the keys (none should, but defense in depth).
|
|
*
|
|
* Init: hooks registered on `init` priority 5 from TMDO_Core (after
|
|
* HivePress's plugins_loaded:5 boot, before main entity bridge filters at 10).
|
|
*
|
|
* Lessons applied from v2.11.7 — this class is added to
|
|
* TMDO_Hook_Bus_Bridge::COEXIST_WHITELIST so `wp wpdo conflict-scan` does not
|
|
* report a false positive when both Hook Bus and this filter run on the same
|
|
* `add_term_metadata` / `add_comment_metadata` hook.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.12.1
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Block known-garbage writes to wp_termmeta / wp_commentmeta.
|
|
*/
|
|
final class TMDO_Term_Comment_Garbage_Filter {
|
|
|
|
/** Option key for admin toggle. */
|
|
public const OPT_ENABLED = 'wpdo_term_comment_garbage_filter_enabled';
|
|
|
|
/** Telemetry option: count of garbage writes dropped (rolling 24h cumulative). */
|
|
public const OPT_DROPPED_COUNT = 'wpdo_term_comment_garbage_drops_24h';
|
|
|
|
/** Telemetry option: timestamp of the last reset of the 24h counter. */
|
|
public const OPT_DROPPED_RESET_AT = 'wpdo_term_comment_garbage_drops_reset_at';
|
|
|
|
/**
|
|
* Pattern prefixes that trigger a drop on writes to BOTH wp_termmeta and
|
|
* wp_commentmeta. Aligned with cleanup CLI `--target` buckets.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private const SHARED_DROP_PREFIXES = array(
|
|
'_wxr_import_',
|
|
'_2meet_demo_',
|
|
);
|
|
|
|
/**
|
|
* Exact meta_keys that are dropped only for wp_commentmeta writes
|
|
* (post-domain keys mistakenly written to comment table — always a bug).
|
|
*
|
|
* Must stay in sync with TMDO_Commentmeta_Cleaner::ORPHAN_POST_META_KEYS.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private const COMMENT_ONLY_ORPHAN_KEYS = array(
|
|
'_hp_price',
|
|
'_hp_status',
|
|
'_hp_featured',
|
|
'_hp_verified',
|
|
'_hp_view_count',
|
|
'_thumbnail_id',
|
|
'_edit_lock',
|
|
'_edit_last',
|
|
);
|
|
|
|
/**
|
|
* Register filters. Called from TMDO_Core::run() on init:5.
|
|
*
|
|
* Idempotent — safe to call multiple times.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function init(): void {
|
|
if ( ! self::is_enabled() ) {
|
|
return;
|
|
}
|
|
|
|
// Term meta writes — priority 9 (before Hook Bus at 10).
|
|
add_filter( 'add_term_metadata', array( self::class, 'on_term_write' ), 9, 5 );
|
|
add_filter( 'update_term_metadata', array( self::class, 'on_term_write' ), 9, 5 );
|
|
|
|
// Comment meta writes.
|
|
add_filter( 'add_comment_metadata', array( self::class, 'on_comment_write' ), 9, 5 );
|
|
add_filter( 'update_comment_metadata', array( self::class, 'on_comment_write' ), 9, 5 );
|
|
}
|
|
|
|
/**
|
|
* Check the admin toggle. Defaults to enabled.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function is_enabled(): bool {
|
|
return (bool) get_option( self::OPT_ENABLED, '1' );
|
|
}
|
|
|
|
/**
|
|
* Test whether a meta_key triggers the shared drop rules
|
|
* (applicable to both term and comment meta).
|
|
*
|
|
* @param mixed $meta_key Candidate meta_key.
|
|
* @return bool
|
|
*/
|
|
public static function is_shared_garbage_key( $meta_key ): bool {
|
|
if ( ! is_string( $meta_key ) ) {
|
|
return false;
|
|
}
|
|
foreach ( self::SHARED_DROP_PREFIXES as $prefix ) {
|
|
if ( str_starts_with( $meta_key, $prefix ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Test whether a meta_key triggers the comment-only orphan post-meta drop.
|
|
*
|
|
* @param mixed $meta_key Candidate meta_key.
|
|
* @return bool
|
|
*/
|
|
public static function is_comment_orphan_key( $meta_key ): bool {
|
|
if ( ! is_string( $meta_key ) ) {
|
|
return false;
|
|
}
|
|
return in_array( $meta_key, self::COMMENT_ONLY_ORPHAN_KEYS, true );
|
|
}
|
|
|
|
/**
|
|
* Filter callback: add_term_metadata / update_term_metadata.
|
|
*
|
|
* Returns null → continue normal flow (write to wp_termmeta).
|
|
* Returns true → short-circuit; WP treats as success without DB write.
|
|
*
|
|
* @param mixed $check Filter accumulator (null at our priority).
|
|
* @param int $object_id Term ID.
|
|
* @param string $meta_key Meta key being written.
|
|
* @param mixed $meta_value Value being written (unused).
|
|
* @param mixed $extra Either $unique (add) or $prev_value (update). Unused.
|
|
* @return mixed
|
|
*/
|
|
public static function on_term_write( $check, $object_id, $meta_key, $meta_value, $extra ) {
|
|
unset( $object_id, $meta_value, $extra );
|
|
if ( self::is_shared_garbage_key( $meta_key ) ) {
|
|
self::increment_drop_counter();
|
|
return true;
|
|
}
|
|
return $check;
|
|
}
|
|
|
|
/**
|
|
* Filter callback: add_comment_metadata / update_comment_metadata.
|
|
*
|
|
* Returns null → continue normal flow.
|
|
* Returns true → short-circuit (silent drop).
|
|
*
|
|
* @param mixed $check Filter accumulator.
|
|
* @param int $object_id Comment ID.
|
|
* @param string $meta_key Meta key being written.
|
|
* @param mixed $meta_value Value being written (unused).
|
|
* @param mixed $extra Either $unique (add) or $prev_value (update). Unused.
|
|
* @return mixed
|
|
*/
|
|
public static function on_comment_write( $check, $object_id, $meta_key, $meta_value, $extra ) {
|
|
unset( $object_id, $meta_value, $extra );
|
|
if ( self::is_shared_garbage_key( $meta_key ) || self::is_comment_orphan_key( $meta_key ) ) {
|
|
self::increment_drop_counter();
|
|
return true;
|
|
}
|
|
return $check;
|
|
}
|
|
|
|
/**
|
|
* Increment the rolling 24h drop counter.
|
|
*
|
|
* Auto-resets every 24h based on a stored timestamp; this avoids
|
|
* unbounded growth and gives the admin status panel a meaningful
|
|
* "drops in last day" indicator.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function increment_drop_counter(): void {
|
|
$now = time();
|
|
$reset_at = (int) get_option( self::OPT_DROPPED_RESET_AT, 0 );
|
|
|
|
if ( 0 === $reset_at || ( $now - $reset_at ) >= DAY_IN_SECONDS ) {
|
|
update_option( self::OPT_DROPPED_COUNT, 1, false );
|
|
update_option( self::OPT_DROPPED_RESET_AT, $now, false );
|
|
return;
|
|
}
|
|
|
|
$count = (int) get_option( self::OPT_DROPPED_COUNT, 0 );
|
|
update_option( self::OPT_DROPPED_COUNT, $count + 1, false );
|
|
}
|
|
|
|
/**
|
|
* Get the current 24h rolling drop counter (for admin status panel).
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function get_drop_count_24h(): int {
|
|
$reset_at = (int) get_option( self::OPT_DROPPED_RESET_AT, 0 );
|
|
if ( 0 === $reset_at || ( time() - $reset_at ) >= DAY_IN_SECONDS ) {
|
|
return 0;
|
|
}
|
|
return (int) get_option( self::OPT_DROPPED_COUNT, 0 );
|
|
}
|
|
}
|