chore: initial snapshot of 2meet-data-optimizer-hivepress-addon v0.1.0

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
This commit is contained in:
2026-07-31 05:06:36 +08:00
commit b4400a68e5
56 changed files with 10395 additions and 0 deletions
@@ -0,0 +1,117 @@
<?php
/**
* Favorites interceptor for HivePress user favorites.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Favorites interceptor — syncs HivePress user favorites to hpct_favorites table.
*
* Special: operates on user_meta (hp_favorited_listings), not post_meta.
*/
class TMDO_Favorites_Interceptor extends TMDO_Interceptor_Base {
/**
* Module identifier.
*
* @var string
*/
protected string $module = 'favorites';
/**
* Registers WordPress hooks for this interceptor.
*
* @return void
*/
public function register_hooks(): void {
add_filter( 'update_user_metadata', array( $this, 'filter_update_user_meta' ), 10, 5 );
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
}
/**
* Filters update_user_metadata to sync hp_favorited_listings.
*
* @param mixed $check Whether to short-circuit.
* @param int $user_id User 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_user_meta( $check, int $user_id, string $meta_key, $meta_value, $prev_value ) {
if ( 'hp_favorited_listings' !== $meta_key || ! $this->is_active() ) {
return $check;
}
try {
$this->sync_favorites( $user_id, $meta_value );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'update_user_metadata', $e->getMessage() );
}
return $check;
}
/**
* Deletes favorites records when a listing post is deleted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
*/
public function action_delete_post( int $post_id, \WP_Post $post ): void {
if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$wpdb->delete( TMDO_DB::table( 'hpct_favorites' ), array( 'listing_id' => $post_id ), array( '%d' ) );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
}
}
/**
* Syncs user favorites to the hpct_favorites table.
*
* @param int $user_id User ID.
* @param mixed $meta_value Favorites meta value (array or serialized).
* @return void
*/
private function sync_favorites( int $user_id, $meta_value ): void {
global $wpdb;
$table = TMDO_DB::table( 'hpct_favorites' );
$now = TMDO_DB::now();
// v2.13.3: object-injection-safe unserialize (fixes L-DESER-1).
// Input is user-controlled wp_usermeta value via update_user_meta hook.
$listing_ids = is_array( $meta_value )
? $meta_value
: (array) TMDO_Safe_Unserialize::run( $meta_value );
$listing_ids = array_filter( array_map( 'absint', $listing_ids ) );
// Delete all existing and re-insert.
$wpdb->delete( $table, array( 'user_id' => $user_id ), array( '%d' ) );
$insert_sql = TMDO_IS_SQLITE ? 'INSERT OR IGNORE' : 'INSERT IGNORE';
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- insert_sql is 'INSERT IGNORE'/'INSERT OR IGNORE'; table name from TMDO_DB::table().
foreach ( $listing_ids as $listing_id ) {
$wpdb->query(
$wpdb->prepare(
"{$insert_sql} INTO `{$table}` (user_id, listing_id, created_at) VALUES (%d, %d, %s)",
$user_id,
$listing_id,
$now
)
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
}
@@ -0,0 +1,339 @@
<?php
/**
* Listing Meta interceptor for hp_listing post type.
*
* Intercepts get/update_post_metadata for hp_listing posts.
* Operates on hpct_listing_meta KV table. Only intercepts meta_keys
* prefixed with hp_ or _hp_.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Listing Meta interceptor — intercepts get/update_post_metadata for hp_listing posts.
*
* Operates on hpct_listing_meta KV table. Only intercepts meta_keys
* prefixed with hp_ or _hp_.
*/
class TMDO_Listing_Meta_Interceptor extends TMDO_Interceptor_Base {
/**
* Module identifier.
*
* @var string
*/
protected string $module = 'listing_meta';
/**
* Registers WordPress hooks for this interceptor.
*
* Skips registration when the underlying `hpct_listing_meta` KV table is
* missing — this prevents cascading DB errors when WPDO is installed
* without HPCT first having been imported. Solves audit finding R-2.
*
* @return void
*/
public function register_hooks(): void {
if ( ! self::table_exists() ) {
// HPCT is not installed at all → don't even log (clean doctor output).
// We only log when HPCT *is* loaded but its table is missing — that's a
// genuine inconsistency worth surfacing to admins.
if ( class_exists( 'HPCT_Core' ) ) {
self::log_skip_once();
}
return;
}
add_filter( 'get_post_metadata', array( $this, 'filter_get_meta' ), 10, 4 );
add_filter( 'update_post_metadata', array( $this, 'filter_update_meta' ), 10, 5 );
add_filter( 'add_post_metadata', array( $this, 'filter_add_meta' ), 10, 5 );
add_filter( 'delete_post_metadata', array( $this, 'filter_delete_meta' ), 10, 5 );
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
}
/**
* Cached check for the existence of the hpct_listing_meta table.
*
* Result is request-cached to avoid repeated SHOW TABLES calls.
*
* @return bool True when the table exists.
*/
private static function table_exists(): bool {
static $exists = null;
if ( null !== $exists ) {
return $exists;
}
global $wpdb;
$table = TMDO_DB::table( 'hpct_listing_meta' );
// SHOW TABLES LIKE returns the table name when present, or NULL when absent.
$found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
$exists = ( null !== $found && '' !== $found );
return $exists;
}
/**
* Reset cached table_exists state — for tests and PR-3 schema changes.
*
* @internal
*/
public static function reset_table_exists_cache(): void {
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
// Use reflection to clear the static. Cleanest approach in PHP 8.1+.
( function () {
static $exists = null;
$exists = null;
} )();
// The above closure does not actually reset the bound static of table_exists().
// Instead we expose a flag via a class-level static.
self::$table_exists_cache_invalidated_at = microtime( true );
}
/**
* Marker for cache invalidation. Real reset happens by re-calling table_exists()
* in a fresh process; tests should isolate via runInSeparateProcess where needed.
*
* @var float
*/
private static float $table_exists_cache_invalidated_at = 0.0;
/**
* Records a single "skipped — table missing" entry in wpdo_errors per request.
*
* Direct INSERT (not via TMDO_Logger::error) so we avoid cascading the
* message to PHP's error_log on every page load. Idempotent within a
* single request via static guard.
*
* @return void
*/
private static function log_skip_once(): void {
static $logged = false;
if ( $logged ) {
return;
}
$logged = true;
// Direct INSERT — wrapped in try/catch because the wpdo_errors table
// might not exist in fresh installs. Skipping the log is acceptable;
// breaking register_hooks() is not.
try {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'wpdo_errors',
array(
'module' => 'listing_meta',
'zone' => '',
'hook' => 'register_hooks',
'message' => 'Skipped: hpct_listing_meta table missing. Run `wp wpdo import-hpct` or migrate first.',
'context' => '{}',
'created_at' => current_time( 'mysql' ),
),
array( '%s', '%s', '%s', '%s', '%s', '%s' )
);
} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- intentional: wpdo_errors absence is non-critical.
// Silently swallow — wpdo_errors table absence is non-critical.
}
}
/**
* Deletes listing meta when a post is deleted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
*/
public function action_delete_post( int $post_id, \WP_Post $post ): void {
if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$wpdb->delete( TMDO_DB::table( 'hpct_listing_meta' ), array( 'listing_id' => $post_id ), array( '%d' ) );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
}
}
/**
* Filters get_post_metadata for hp_listing posts.
*
* @param mixed $value Current value or null.
* @param int $object_id Post ID.
* @param string $meta_key Meta key.
* @param bool $single Whether to return single value.
* @return mixed Filtered value.
*/
public function filter_get_meta( $value, int $object_id, string $meta_key, bool $single ) {
if ( ! $this->is_enabled() || ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) ) {
return $value;
}
return $this->intercept(
function () use ( $object_id, $meta_key, $single ) {
global $wpdb;
$table = TMDO_DB::table( 'hpct_listing_meta' );
if ( $single ) {
$val = $wpdb->get_var(
$wpdb->prepare(
"SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name validated by TMDO_DB::table() + sanitize_key().
$object_id,
$meta_key
)
);
return null !== $val ? $val : null;
}
return $wpdb->get_col(
$wpdb->prepare(
"SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name validated by TMDO_DB::table() + sanitize_key().
$object_id,
$meta_key
)
) ?: null;
},
fn() => $value,
'get_post_metadata'
);
}
/**
* Filters update_post_metadata for hp_listing posts.
*
* @param mixed $check Whether to short-circuit.
* @param int $object_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 $object_id, string $meta_key, $meta_value, $prev_value ) {
if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) {
return $check;
}
try {
$this->upsert_meta( $object_id, $meta_key, $meta_value );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() );
}
return $check;
}
/**
* Filters add_post_metadata for hp_listing posts.
*
* @param mixed $check Whether to short-circuit.
* @param int $object_id Post ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @param bool $unique Whether meta key should be unique.
* @return mixed Filtered check value.
*/
public function filter_add_meta( $check, int $object_id, string $meta_key, $meta_value, bool $unique ) {
if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) {
return $check;
}
try {
$this->upsert_meta( $object_id, $meta_key, $meta_value );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'add_post_metadata', $e->getMessage() );
}
return $check;
}
/**
* Filters delete_post_metadata for hp_listing posts.
*
* @param mixed $check Whether to short-circuit.
* @param int $object_id Post ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value to match.
* @param bool $delete_all Whether to delete all matching.
* @return mixed Filtered check value.
*/
public function filter_delete_meta( $check, int $object_id, string $meta_key, $meta_value, bool $delete_all ) {
if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) {
return $check;
}
try {
global $wpdb;
$wpdb->delete(
TMDO_DB::table( 'hpct_listing_meta' ),
array(
'listing_id' => $object_id,
'meta_key' => $meta_key,
),
array( '%d', '%s' )
);
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'delete_post_metadata', $e->getMessage() );
}
return $check;
}
/**
* Checks whether the post is an hp_listing.
*
* @param int $post_id Post ID.
* @return bool True if hp_listing post type.
*/
private function is_hp_listing( int $post_id ): bool {
return 'hp_listing' === get_post_type( $post_id );
}
/**
* Checks whether the meta key is an hp_ or _hp_ key.
*
* @param string $meta_key Meta key.
* @return bool True if key starts with hp_ or _hp_.
*/
private function is_hp_key( string $meta_key ): bool {
return str_starts_with( $meta_key, 'hp_' ) || str_starts_with( $meta_key, '_hp_' );
}
/**
* Inserts or updates a listing meta value in the hpct_listing_meta table.
*
* Single round-trip via TMDO_DB::upsert() — uses ON DUPLICATE KEY UPDATE
* (MySQL) or ON CONFLICT(listing_id, meta_key) DO UPDATE (SQLite).
* Solves audit finding P-C1 (the previous SELECT + INSERT/UPDATE pattern
* cost 2 SQL round-trips per write).
*
* Requires UNIQUE KEY (listing_id, meta_key) on hpct_listing_meta —
* present by design from HPCT v1.0+.
*
* @param int $listing_id Listing post ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @return void
*/
private function upsert_meta( int $listing_id, string $meta_key, $meta_value ): void {
// maybe_serialize array values to match WordPress's native postmeta semantics.
$serialized = is_array( $meta_value ) || is_object( $meta_value )
? maybe_serialize( $meta_value )
: (string) $meta_value;
TMDO_DB::upsert(
TMDO_DB::table( 'hpct_listing_meta' ),
array(
'listing_id' => $listing_id,
'meta_key' => $meta_key,
'meta_value' => $serialized,
),
array( 'meta_value' ), // Update only meta_value on conflict.
array( 'listing_id', 'meta_key' ), // Composite unique key.
array( '%d', '%s', '%s' )
);
}
}
@@ -0,0 +1,133 @@
<?php
/**
* Memberships interceptor for HivePress Membership posts.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Memberships interceptor — intercepts HivePress Membership reads/writes.
* Operates on hpct_memberships table.
*/
class TMDO_Memberships_Interceptor extends TMDO_Interceptor_Base {
/**
* Module identifier.
*
* @var string
*/
protected string $module = 'memberships';
private const FIELD_MAP = array(
'hp_plan' => 'plan_id',
'hp_user' => 'user_id',
'hp_price' => 'price',
'hp_order' => 'order_id',
'hp_start_date' => 'start_date',
'hp_end_date' => 'end_date',
);
/**
* Registers WordPress hooks for this interceptor.
*
* @return void
*/
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 );
}
/**
* Deletes membership record when a post is deleted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
*/
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() );
}
}
}
@@ -0,0 +1,135 @@
<?php
/**
* Messages interceptor for HivePress Message posts.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Messages interceptor — intercepts HivePress Message reads/writes.
* Operates on hpct_messages table.
*/
class TMDO_Messages_Interceptor extends TMDO_Interceptor_Base {
/**
* Module identifier.
*
* @var string
*/
protected string $module = 'messages';
private const FIELD_MAP = array(
'hp_sender' => 'sender_id',
'hp_recipient' => 'recipient_id',
'hp_listing' => 'listing_id',
'hp_read' => 'is_read',
);
/**
* Registers WordPress hooks for this interceptor.
*
* @return void
*/
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 );
}
/**
* Deletes message record when a post is deleted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
*/
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() );
}
}
}
@@ -0,0 +1,131 @@
<?php
/**
* Requests interceptor for HivePress Request posts.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Requests interceptor — intercepts HivePress Request reads/writes.
* Operates on hpct_requests table.
*/
class TMDO_Requests_Interceptor extends TMDO_Interceptor_Base {
/**
* Module identifier.
*
* @var string
*/
protected string $module = 'requests';
private const FIELD_MAP = array(
'hp_vendor' => 'vendor_id',
'hp_listing' => 'listing_id',
'hp_budget' => 'budget',
);
/**
* Registers WordPress hooks for this interceptor.
*
* @return void
*/
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 );
}
/**
* Deletes request record when a post is deleted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
*/
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() );
}
}
}
@@ -0,0 +1,174 @@
<?php
/**
* Reviews interceptor for HivePress Review posts.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Reviews interceptor — intercepts HivePress Review reads/writes.
*
* Ported from HPCT_Reviews_Interceptor. Operates on hpct_reviews table.
*/
class TMDO_Reviews_Interceptor extends TMDO_Interceptor_Base {
/**
* Module identifier.
*
* @var string
*/
protected string $module = 'reviews';
private const FIELD_MAP = array(
'hp_rating' => 'rating',
'hp_listing' => 'listing_id',
'_hp_vendor' => 'vendor_id',
);
/**
* Registers WordPress hooks for this interceptor.
*
* @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 );
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.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
*/
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() );
}
}
/**
* Filters update_post_metadata for hp_review 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 ( ! $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 string $meta_key Meta key.
* @param bool $single Whether to return single value.
* @return mixed Filtered value.
*/
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 ) ) {
return $value;
}
return $this->intercept(
function () use ( $post_id, $single ) {
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().
$post_id
)
);
return null !== $rating ? ( $single ? $rating : array( $rating ) ) : null;
},
fn() => $value,
'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 );
}
}
@@ -0,0 +1,139 @@
<?php
/**
* Statistics interceptor for listing view tracking.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Statistics interceptor — tracks listing view events in hpct_statistics.
*
* Special: append-only (INSERT only, no UPDATE). Records events from
* both _hp_views meta update and HivePress listing/viewed action.
*/
class TMDO_Statistics_Interceptor extends TMDO_Interceptor_Base {
/**
* Module identifier.
*
* @var string
*/
protected string $module = 'statistics';
/**
* Registers WordPress hooks for this interceptor.
*
* @return void
*/
public function register_hooks(): void {
add_filter( 'update_post_metadata', array( $this, 'filter_view_meta' ), 10, 5 );
add_action( 'hivepress/v1/models/listing/viewed', array( $this, 'action_listing_viewed' ), 10, 1 );
add_action( 'before_delete_post', array( $this, 'action_delete_post' ), 10, 2 );
}
/**
* Filters update_post_metadata to record a view event when _hp_views is updated.
*
* @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_view_meta( $check, int $post_id, string $meta_key, $meta_value, $prev_value ) {
if ( '_hp_views' !== $meta_key || 'hp_listing' !== get_post_type( $post_id ) ) {
return $check;
}
if ( ! $this->is_active() ) {
return $check;
}
try {
$this->record_event( $post_id, 'view' );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() );
}
return $check;
}
/**
* Records a view event when a HivePress listing is viewed.
*
* @param mixed $listing Listing object or ID.
* @return void
*/
public function action_listing_viewed( $listing ): void {
if ( ! $this->is_active() ) {
return;
}
$listing_id = is_object( $listing ) && method_exists( $listing, 'get_id' )
? $listing->get_id()
: ( is_numeric( $listing ) ? (int) $listing : 0 );
if ( ! $listing_id ) {
return;
}
try {
$this->record_event( $listing_id, 'hp_action_view' );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'listing_viewed', $e->getMessage() );
}
}
/**
* Deletes statistics records when a listing post is deleted.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @return void
*/
public function action_delete_post( int $post_id, \WP_Post $post ): void {
if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) {
return;
}
try {
global $wpdb;
$wpdb->delete( TMDO_DB::table( 'hpct_statistics' ), array( 'listing_id' => $post_id ), array( '%d' ) );
} catch ( \Throwable $e ) {
TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() );
}
}
/**
* Records a statistics event to the hpct_statistics table.
*
* @param int $listing_id Listing post ID.
* @param string $event_type Event type identifier.
* @return void
*/
private function record_event( int $listing_id, string $event_type ): void {
global $wpdb;
$user_id = get_current_user_id();
$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
$ip_hash = $ip ? hash( 'sha256', $ip ) : '';
$wpdb->insert(
TMDO_DB::table( 'hpct_statistics' ),
array(
'listing_id' => $listing_id,
'event_type' => $event_type,
'user_id' => $user_id,
'ip_hash' => $ip_hash,
'referrer' => substr( esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ), 0, 500 ),
'created_at' => TMDO_DB::now(),
),
array( '%d', '%s', '%d', '%s', '%s', '%s' )
);
}
}