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:
@@ -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' )
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user