Files
2meet-data-optimizer/includes/integrations/class-tmdo-term-comment-garbage-filter.php
T
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入
並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 06:13:33 +08:00

232 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
*/
declare(strict_types=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 );
}
}