fix(hivepress): detector active-plugin 偵測 + 4 個 interceptor 改繼承核心基底(F2/F3/F4)

F3 detector(A v3.4.5):HivePress addon 不暴露任何 per-addon class/const,
   它們是透過 add_filter('hivepress/v1/extensions') 註冊目錄,所以原本的
   class_exists / defined 偵測永遠只認得 core → 反 EAV 覆蓋率卡在 1/13。
   改以 get_option('active_plugins') 為第一級訊號(新增 active_plugin_files()),
   version_for() 對 addon 改讀其主檔 Version: header。覆蓋率回到 7/13。

F2 interceptor:reviews / messages / memberships / requests 改繼承核心的
   TMDO_Standard_Post_Interceptor,573 → 344 行。FIELD_MAP 由 private
   提升為 public(late static binding 從基底讀取)。
   bootstrap 的 require 迴圈加核心版本守衛:這 4 個檔案在舊核心下會於
   parse 階段就 fatal,class_exists 守衛來不及。

F4 listing-stats:view 計數改用 Zone_Warm::increment() 原子遞增,
   flush 改原子 UPDATE,消除 get()+set() 的 lost-update race。

F5:主檔補 TablePrefix header(打包終檢 §10 schema drift 主路徑)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
2026-07-31 06:07:39 +08:00
parent e3ede89197
commit b96b041445
8 changed files with 240 additions and 414 deletions
@@ -2,7 +2,7 @@
/**
* Memberships interceptor for HivePress Membership posts.
*
* @package WP_Data_Optimizer
* @package TMDO
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -11,9 +11,10 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Memberships interceptor — intercepts HivePress Membership reads/writes.
*
* Operates on hpct_memberships table.
*/
class TMDO_Memberships_Interceptor extends TMDO_Interceptor_Base {
class TMDO_Memberships_Interceptor extends TMDO_Standard_Post_Interceptor {
/**
* Module identifier.
@@ -22,7 +23,10 @@ class TMDO_Memberships_Interceptor extends TMDO_Interceptor_Base {
*/
protected string $module = 'memberships';
private const FIELD_MAP = array(
/**
* Meta key → flat column mapping.
*/
public const FIELD_MAP = array(
'hp_plan' => 'plan_id',
'hp_user' => 'user_id',
'hp_price' => 'price',
@@ -32,102 +36,41 @@ class TMDO_Memberships_Interceptor extends TMDO_Interceptor_Base {
);
/**
* Registers WordPress hooks for this interceptor.
* Returns the WordPress post_type slug handled by this interceptor.
*
* @return void
* @return string
*/
public function register_hooks(): void {
add_action( 'wp_insert_post', array( $this, 'action_insert_post' ), 10, 3 );
add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 );
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
protected function get_post_type(): string {
return 'hp_membership';
}
/**
* Deletes membership record when a post is deleted.
* Returns the table key passed to TMDO_DB::table().
*
* @return string
*/
protected function get_table_key(): string {
return 'hpct_memberships';
}
/**
* Builds the INSERT row for a newly created membership post.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
* @param string $now Formatted datetime string.
* @return array{values: array, formats: array}
*/
public function action_delete_post( int $post_id, \WP_Post $post ): void {
if ( 'hp_membership' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$wpdb->delete( TMDO_DB::table( 'hpct_memberships' ), array( 'post_id' => $post_id ), array( '%d' ) );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
}
}
/**
* Filters update_post_metadata for hp_membership posts.
*
* @param mixed $check Whether to short-circuit.
* @param int $post_id Post ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @param mixed $prev_value Previous meta value.
* @return mixed Filtered check value.
*/
public function filter_update_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) {
if ( 'hp_membership' !== get_post_type( $post_id ) || ! isset( self::FIELD_MAP[ $meta_key ] ) ) {
return $check;
}
if ( ! $this->is_active() ) {
return $check;
}
try {
global $wpdb;
$col = self::FIELD_MAP[ $meta_key ];
$wpdb->query(
$wpdb->prepare(
'UPDATE ' . TMDO_DB::table( 'hpct_memberships' ) . " SET `{$col}` = %s, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(); column validated by FIELD_MAP constant.
$meta_value,
TMDO_DB::now(),
$post_id
)
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() );
}
return $check;
}
/**
* Inserts a new membership record when a post is inserted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @param bool $update Whether this is an update.
* @return void
*/
public function action_insert_post( int $post_id, \WP_Post $post, bool $update ): void {
if ( $update || 'hp_membership' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$now = TMDO_DB::now();
$wpdb->insert(
TMDO_DB::table( 'hpct_memberships' ),
array(
'post_id' => $post_id,
'user_id' => (int) $post->post_author,
'status' => $post->post_status,
'created_at' => $now,
'updated_at' => $now,
),
array( '%d', '%d', '%s', '%s', '%s' )
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'wp_insert_post', $e->getMessage() );
}
protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array {
return array(
'values' => array(
'post_id' => $post_id,
'user_id' => (int) $post->post_author,
'status' => $post->post_status,
'created_at' => $now,
'updated_at' => $now,
),
'formats' => array( '%d', '%d', '%s', '%s', '%s' ),
);
}
}
@@ -2,7 +2,7 @@
/**
* Messages interceptor for HivePress Message posts.
*
* @package WP_Data_Optimizer
* @package TMDO
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -11,9 +11,10 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Messages interceptor — intercepts HivePress Message reads/writes.
*
* Operates on hpct_messages table.
*/
class TMDO_Messages_Interceptor extends TMDO_Interceptor_Base {
class TMDO_Messages_Interceptor extends TMDO_Standard_Post_Interceptor {
/**
* Module identifier.
@@ -22,7 +23,10 @@ class TMDO_Messages_Interceptor extends TMDO_Interceptor_Base {
*/
protected string $module = 'messages';
private const FIELD_MAP = array(
/**
* Meta key → flat column mapping.
*/
public const FIELD_MAP = array(
'hp_sender' => 'sender_id',
'hp_recipient' => 'recipient_id',
'hp_listing' => 'listing_id',
@@ -30,106 +34,45 @@ class TMDO_Messages_Interceptor extends TMDO_Interceptor_Base {
);
/**
* Registers WordPress hooks for this interceptor.
* Returns the WordPress post_type slug handled by this interceptor.
*
* @return void
* @return string
*/
public function register_hooks(): void {
add_action( 'wp_insert_post', array( $this, 'action_insert_post' ), 10, 3 );
add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 );
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
protected function get_post_type(): string {
return 'hp_message';
}
/**
* Deletes message record when a post is deleted.
* Returns the table key passed to TMDO_DB::table().
*
* @return string
*/
protected function get_table_key(): string {
return 'hpct_messages';
}
/**
* Builds the INSERT row for a newly created message post.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
* @param string $now Formatted datetime string.
* @return array{values: array, formats: array}
*/
public function action_delete_post( int $post_id, \WP_Post $post ): void {
if ( 'hp_message' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$wpdb->delete( TMDO_DB::table( 'hpct_messages' ), array( 'post_id' => $post_id ), array( '%d' ) );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
}
}
/**
* Filters update_post_metadata for hp_message posts.
*
* @param mixed $check Whether to short-circuit.
* @param int $post_id Post ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @param mixed $prev_value Previous meta value.
* @return mixed Filtered check value.
*/
public function filter_update_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) {
if ( 'hp_message' !== get_post_type( $post_id ) || ! isset( self::FIELD_MAP[ $meta_key ] ) ) {
return $check;
}
if ( ! $this->is_active() ) {
return $check;
}
try {
global $wpdb;
$col = self::FIELD_MAP[ $meta_key ];
$wpdb->query(
$wpdb->prepare(
'UPDATE ' . TMDO_DB::table( 'hpct_messages' ) . " SET `{$col}` = %s, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(); column validated by FIELD_MAP constant.
$meta_value,
TMDO_DB::now(),
$post_id
)
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() );
}
return $check;
}
/**
* Inserts a new message record when a post is inserted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @param bool $update Whether this is an update.
* @return void
*/
public function action_insert_post( int $post_id, \WP_Post $post, bool $update ): void {
if ( $update || 'hp_message' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$now = TMDO_DB::now();
$wpdb->insert(
TMDO_DB::table( 'hpct_messages' ),
array(
'post_id' => $post_id,
'thread_id' => (int) $post->post_parent,
'sender_id' => (int) $post->post_author,
'subject' => sanitize_text_field( $post->post_title ),
'body' => $post->post_content,
'status' => $post->post_status,
'sent_at' => $now,
'created_at' => $now,
'updated_at' => $now,
),
array( '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s' )
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'wp_insert_post', $e->getMessage() );
}
protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array {
return array(
'values' => array(
'post_id' => $post_id,
'thread_id' => (int) $post->post_parent,
'sender_id' => (int) $post->post_author,
'subject' => sanitize_text_field( $post->post_title ),
'body' => $post->post_content,
'status' => $post->post_status,
'sent_at' => $now,
'created_at' => $now,
'updated_at' => $now,
),
'formats' => array( '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s' ),
);
}
}
@@ -2,7 +2,7 @@
/**
* Requests interceptor for HivePress Request posts.
*
* @package WP_Data_Optimizer
* @package TMDO
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -11,9 +11,10 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Requests interceptor — intercepts HivePress Request reads/writes.
*
* Operates on hpct_requests table.
*/
class TMDO_Requests_Interceptor extends TMDO_Interceptor_Base {
class TMDO_Requests_Interceptor extends TMDO_Standard_Post_Interceptor {
/**
* Module identifier.
@@ -22,110 +23,52 @@ class TMDO_Requests_Interceptor extends TMDO_Interceptor_Base {
*/
protected string $module = 'requests';
private const FIELD_MAP = array(
/**
* Meta key → flat column mapping.
*/
public const FIELD_MAP = array(
'hp_vendor' => 'vendor_id',
'hp_listing' => 'listing_id',
'hp_budget' => 'budget',
);
/**
* Registers WordPress hooks for this interceptor.
* Returns the WordPress post_type slug handled by this interceptor.
*
* @return void
* @return string
*/
public function register_hooks(): void {
add_action( 'wp_insert_post', array( $this, 'action_insert_post' ), 10, 3 );
add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 );
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
protected function get_post_type(): string {
return 'hp_request';
}
/**
* Deletes request record when a post is deleted.
* Returns the table key passed to TMDO_DB::table().
*
* @return string
*/
protected function get_table_key(): string {
return 'hpct_requests';
}
/**
* Builds the INSERT row for a newly created request post.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
* @param string $now Formatted datetime string.
* @return array{values: array, formats: array}
*/
public function action_delete_post( int $post_id, \WP_Post $post ): void {
if ( 'hp_request' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$wpdb->delete( TMDO_DB::table( 'hpct_requests' ), array( 'post_id' => $post_id ), array( '%d' ) );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
}
}
/**
* Filters update_post_metadata for hp_request posts.
*
* @param mixed $check Whether to short-circuit.
* @param int $post_id Post ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @param mixed $prev_value Previous meta value.
* @return mixed Filtered check value.
*/
public function filter_update_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) {
if ( 'hp_request' !== get_post_type( $post_id ) || ! isset( self::FIELD_MAP[ $meta_key ] ) ) {
return $check;
}
if ( ! $this->is_active() ) {
return $check;
}
try {
global $wpdb;
$col = self::FIELD_MAP[ $meta_key ];
$wpdb->query(
$wpdb->prepare(
'UPDATE ' . TMDO_DB::table( 'hpct_requests' ) . " SET `{$col}` = %s, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(); column validated by FIELD_MAP constant.
$meta_value,
TMDO_DB::now(),
$post_id
)
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() );
}
return $check;
}
/**
* Inserts a new request record when a post is inserted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @param bool $update Whether this is an update.
* @return void
*/
public function action_insert_post( int $post_id, \WP_Post $post, bool $update ): void {
if ( $update || 'hp_request' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$now = TMDO_DB::now();
$wpdb->insert(
TMDO_DB::table( 'hpct_requests' ),
array(
'post_id' => $post_id,
'user_id' => (int) $post->post_author,
'status' => $post->post_status,
'message' => $post->post_content,
'created_at' => $now,
'updated_at' => $now,
),
array( '%d', '%d', '%s', '%s', '%s', '%s' )
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'wp_insert_post', $e->getMessage() );
}
protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array {
return array(
'values' => array(
'post_id' => $post_id,
'user_id' => (int) $post->post_author,
'status' => $post->post_status,
'message' => $post->post_content,
'created_at' => $now,
'updated_at' => $now,
),
'formats' => array( '%d', '%d', '%s', '%s', '%s', '%s' ),
);
}
}
@@ -2,9 +2,10 @@
/**
* Reviews interceptor for HivePress Review posts.
*
* @package WP_Data_Optimizer
* @package TMDO
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -12,9 +13,9 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Reviews interceptor — intercepts HivePress Review reads/writes.
*
* Ported from HPCT_Reviews_Interceptor. Operates on hpct_reviews table.
* Operates on hpct_reviews table.
*/
class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base {
class TMDO_Reviews_Interceptor extends TMDO_Standard_Post_Interceptor {
/**
* Module identifier.
@@ -23,126 +24,77 @@ class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base {
*/
protected string $module = 'reviews';
private const FIELD_MAP = array(
/**
* Meta key → flat column mapping.
*/
public const FIELD_MAP = array(
'hp_rating' => 'rating',
'hp_listing' => 'listing_id',
'_hp_vendor' => 'vendor_id',
);
/**
* Registers WordPress hooks for this interceptor.
* Returns the WordPress post_type slug handled by this interceptor.
*
* @return string
*/
protected function get_post_type(): string {
return 'hp_review';
}
/**
* Returns the table key passed to TMDO_DB::table().
*
* @return string
*/
protected function get_table_key(): string {
return 'hpct_reviews';
}
/**
* Registers WordPress hooks, including the extra get_post_metadata hook.
*
* @return void
*/
public function register_hooks(): void {
add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 );
add_action( 'wp_insert_post', array( $this, 'action_insert_post' ), 10, 3 );
parent::register_hooks();
add_filter( 'get_post_metadata', array( $this, 'filter_get_meta' ), 10, 4 );
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
}
/**
* Deletes review record when a post is deleted.
* Builds the INSERT row for a newly created review post.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
* @param string $now Formatted datetime string.
* @return array{values: array, formats: array}
*/
public function action_delete_post( int $post_id, \WP_Post $post ): void {
if ( 'hp_review' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$wpdb->delete( TMDO_DB::table( 'hpct_reviews' ), array( 'post_id' => $post_id ), array( '%d' ) );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
}
protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array {
return array(
'values' => array(
'post_id' => $post_id,
'user_id' => (int) $post->post_author,
'status' => $post->post_status,
'title' => sanitize_text_field( $post->post_title ),
'content' => $post->post_content,
'created_at' => $now,
'updated_at' => $now,
),
'formats' => array( '%d', '%d', '%s', '%s', '%s', '%s', '%s' ),
);
}
/**
* Filters update_post_metadata for hp_review posts.
* Reads hp_rating directly from the reviews flat table.
*
* @param mixed $check Whether to short-circuit.
* @param int $post_id Post ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @param mixed $prev_value Previous meta value.
* @return mixed Filtered check value.
*/
public function filter_update_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) {
if ( ! $this->is_hp_review( $post_id ) || ! isset( self::FIELD_MAP[ $meta_key ] ) ) {
return $check;
}
if ( ! $this->is_active() ) {
return $check;
}
try {
global $wpdb;
$col = self::FIELD_MAP[ $meta_key ];
$wpdb->query(
$wpdb->prepare(
'UPDATE ' . TMDO_DB::table( 'hpct_reviews' ) . " SET `{$col}` = %s, updated_at = %s WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table(); column validated by FIELD_MAP constant.
$meta_value,
TMDO_DB::now(),
$post_id
)
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() );
}
return $check;
}
/**
* Inserts a new review record when a post is inserted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @param bool $update Whether this is an update.
* @return void
*/
public function action_insert_post( int $post_id, \WP_Post $post, bool $update ): void {
if ( $update || 'hp_review' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$now = TMDO_DB::now();
$wpdb->insert(
TMDO_DB::table( 'hpct_reviews' ),
array(
'post_id' => $post_id,
'user_id' => (int) $post->post_author,
'status' => $post->post_status,
'title' => sanitize_text_field( $post->post_title ),
'content' => $post->post_content,
'created_at' => $now,
'updated_at' => $now,
),
array( '%d', '%d', '%s', '%s', '%s', '%s', '%s' )
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'wp_insert_post', $e->getMessage() );
}
}
/**
* Filters get_post_metadata for hp_rating on hp_review posts.
*
* @param mixed $value Current value or null.
* @param int $post_id Post ID.
* @param mixed $value Short-circuit value.
* @param int $post_id Post ID.
* @param string $meta_key Meta key.
* @param bool $single Whether to return single value.
* @return mixed Filtered value.
* @param bool $single Whether to return a single value.
* @return mixed
*/
public function filter_get_meta( $value, int $post_id, string $meta_key, bool $single ) {
if ( ! $this->is_enabled() || 'hp_rating' !== $meta_key || ! $this->is_hp_review( $post_id ) ) {
if ( ! $this->is_enabled() || 'hp_rating' !== $meta_key || 'hp_review' !== get_post_type( $post_id ) ) {
return $value;
}
@@ -151,7 +103,7 @@ class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base {
global $wpdb;
$rating = $wpdb->get_var(
$wpdb->prepare(
'SELECT rating FROM ' . TMDO_DB::table( 'hpct_reviews' ) . ' WHERE post_id = %d LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name from TMDO_DB::table().
'SELECT rating FROM ' . TMDO_DB::table( 'hpct_reviews' ) . ' WHERE post_id = %d LIMIT 1',
$post_id
)
);
@@ -161,14 +113,4 @@ class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base {
'get_post_metadata'
);
}
/**
* Checks whether the post is an hp_review.
*
* @param int $post_id Post ID.
* @return bool True if hp_review post type.
*/
private function is_hp_review( int $post_id ): bool {
return 'hp_review' === get_post_type( $post_id );
}
}