diff --git a/includes/back-compat-aliases.php b/includes/back-compat-aliases.php index 78f487f..27c5adf 100644 --- a/includes/back-compat-aliases.php +++ b/includes/back-compat-aliases.php @@ -20,7 +20,6 @@ add_action( $aliases = array( 'TMDO_Admin_HivePress' => 'WPDO_Admin_HivePress', 'TMDO_CLI_HivePress' => 'WPDO_CLI_HivePress', - 'TMDO_Favorites_Interceptor' => 'WPDO_Favorites_Interceptor', 'TMDO_HPCT_Import' => 'WPDO_HPCT_Import', 'TMDO_HP_Bootstrap' => 'WPDO_HP_Bootstrap', 'TMDO_HivePress' => 'WPDO_HivePress', @@ -47,18 +46,7 @@ add_action( 'TMDO_HivePress_Tags_Adapter' => 'WPDO_HivePress_Tags_Adapter', 'TMDO_Hivepress_Term_Comment_Fields' => 'WPDO_Hivepress_Term_Comment_Fields', 'TMDO_Hivepress_Transient_Filter' => 'WPDO_Hivepress_Transient_Filter', - 'TMDO_Listing_Meta_Interceptor' => 'WPDO_Listing_Meta_Interceptor', - 'TMDO_Listing_Meta_Query' => 'WPDO_Listing_Meta_Query', 'TMDO_Listing_Stats' => 'WPDO_Listing_Stats', - 'TMDO_Memberships_Interceptor' => 'WPDO_Memberships_Interceptor', - 'TMDO_Memberships_Query' => 'WPDO_Memberships_Query', - 'TMDO_Messages_Interceptor' => 'WPDO_Messages_Interceptor', - 'TMDO_Messages_Query' => 'WPDO_Messages_Query', - 'TMDO_Requests_Interceptor' => 'WPDO_Requests_Interceptor', - 'TMDO_Requests_Query' => 'WPDO_Requests_Query', - 'TMDO_Reviews_Interceptor' => 'WPDO_Reviews_Interceptor', - 'TMDO_Reviews_Query' => 'WPDO_Reviews_Query', - 'TMDO_Statistics_Interceptor' => 'WPDO_Statistics_Interceptor', ); foreach ( $aliases as $tmdo_class => $wpdo_alias ) { if ( class_exists( $tmdo_class, false ) && ! class_exists( $wpdo_alias, false ) ) { diff --git a/includes/class-tmdo-hivepress-bootstrap.php b/includes/class-tmdo-hivepress-bootstrap.php index c5f9c5c..831d33d 100644 --- a/includes/class-tmdo-hivepress-bootstrap.php +++ b/includes/class-tmdo-hivepress-bootstrap.php @@ -2,8 +2,7 @@ /** * Bootstrap for 2meet Data Optimizer HivePress AddOn. * - * 載入 13 adapter + 7 interceptor + 5 query interceptor + 補助組件 + - * admin tab + CLI namespace。 + * 載入 13 adapter + 補助組件 + admin tab + CLI namespace。 * * 注意:HP 的 V3.0.0 family bootstrap 內部也有一個 class TMDO_HivePress_Bootstrap * (在 includes/hivepress/class-tmdo-hivepress-bootstrap.php)。為避免衝突,本 @@ -64,35 +63,6 @@ final class TMDO_HP_Bootstrap { require_once $base . 'includes/hivepress/class-tmdo-hivepress-benchmark.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-rest.php'; - // 7 HPCT interceptors. - // reviews / messages / memberships / requests extend the core's - // TMDO_Standard_Post_Interceptor — requiring them against a core older than - // v0.2.0 would fatal at parse time, so gate those four on the base class. - $has_standard_base = class_exists( 'TMDO_Standard_Post_Interceptor' ); - foreach ( array( - 'reviews', 'messages', 'favorites', 'memberships', - 'statistics', 'requests', 'listing-meta', - ) as $intslug ) { - $needs_base = in_array( $intslug, array( 'reviews', 'messages', 'memberships', 'requests' ), true ); - if ( $needs_base && ! $has_standard_base ) { - continue; - } - $file = $base . "includes/interceptors/class-tmdo-{$intslug}-interceptor.php"; - if ( file_exists( $file ) ) { - require_once $file; - } - } - - // 5 query interceptors - foreach ( array( - 'reviews', 'messages', 'memberships', 'requests', 'listing-meta', - ) as $qslug ) { - $file = $base . "includes/query/class-tmdo-{$qslug}-query.php"; - if ( file_exists( $file ) ) { - require_once $file; - } - } - // Misc HP integration files require_once $base . 'includes/class-tmdo-hivepress.php'; require_once $base . 'includes/class-tmdo-hivepress-transient-filter.php'; @@ -110,34 +80,6 @@ final class TMDO_HP_Bootstrap { TMDO_HivePress_Bootstrap::boot(); } - // 7 HPCT interceptors - foreach ( array( - 'TMDO_Reviews_Interceptor', - 'TMDO_Messages_Interceptor', - 'TMDO_Favorites_Interceptor', - 'TMDO_Memberships_Interceptor', - 'TMDO_Statistics_Interceptor', - 'TMDO_Requests_Interceptor', - 'TMDO_Listing_Meta_Interceptor', - ) as $cls ) { - if ( class_exists( $cls ) ) { - ( new $cls() )->register_hooks(); - } - } - - // 5 query interceptors - foreach ( array( - 'TMDO_Reviews_Query', - 'TMDO_Messages_Query', - 'TMDO_Memberships_Query', - 'TMDO_Requests_Query', - 'TMDO_Listing_Meta_Query', - ) as $cls ) { - if ( class_exists( $cls ) ) { - ( new $cls() )->register_hooks(); - } - } - // HP transient filter (init:5) if ( class_exists( 'TMDO_Hivepress_Transient_Filter' ) ) { add_action( 'init', array( 'TMDO_Hivepress_Transient_Filter', 'init' ), 5 ); diff --git a/includes/interceptors/class-tmdo-favorites-interceptor.php b/includes/interceptors/class-tmdo-favorites-interceptor.php deleted file mode 100644 index c24abad..0000000 --- a/includes/interceptors/class-tmdo-favorites-interceptor.php +++ /dev/null @@ -1,117 +0,0 @@ -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 - } -} diff --git a/includes/interceptors/class-tmdo-listing-meta-interceptor.php b/includes/interceptors/class-tmdo-listing-meta-interceptor.php deleted file mode 100644 index bd4e9d6..0000000 --- a/includes/interceptors/class-tmdo-listing-meta-interceptor.php +++ /dev/null @@ -1,339 +0,0 @@ -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' ) - ); - } -} diff --git a/includes/interceptors/class-tmdo-memberships-interceptor.php b/includes/interceptors/class-tmdo-memberships-interceptor.php deleted file mode 100644 index fad2903..0000000 --- a/includes/interceptors/class-tmdo-memberships-interceptor.php +++ /dev/null @@ -1,76 +0,0 @@ - 'plan_id', - 'hp_user' => 'user_id', - 'hp_price' => 'price', - 'hp_order' => 'order_id', - 'hp_start_date' => 'start_date', - 'hp_end_date' => 'end_date', - ); - - /** - * Returns the WordPress post_type slug handled by this interceptor. - * - * @return string - */ - protected function get_post_type(): string { - return 'hp_membership'; - } - - /** - * 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. - * @param string $now Formatted datetime string. - * @return array{values: array, formats: array} - */ - 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' ), - ); - } -} diff --git a/includes/interceptors/class-tmdo-messages-interceptor.php b/includes/interceptors/class-tmdo-messages-interceptor.php deleted file mode 100644 index 777545c..0000000 --- a/includes/interceptors/class-tmdo-messages-interceptor.php +++ /dev/null @@ -1,78 +0,0 @@ - 'sender_id', - 'hp_recipient' => 'recipient_id', - 'hp_listing' => 'listing_id', - 'hp_read' => 'is_read', - ); - - /** - * Returns the WordPress post_type slug handled by this interceptor. - * - * @return string - */ - protected function get_post_type(): string { - return 'hp_message'; - } - - /** - * 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. - * @param string $now Formatted datetime string. - * @return array{values: array, formats: array} - */ - 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' ), - ); - } -} diff --git a/includes/interceptors/class-tmdo-requests-interceptor.php b/includes/interceptors/class-tmdo-requests-interceptor.php deleted file mode 100644 index 87cef31..0000000 --- a/includes/interceptors/class-tmdo-requests-interceptor.php +++ /dev/null @@ -1,74 +0,0 @@ - 'vendor_id', - 'hp_listing' => 'listing_id', - 'hp_budget' => 'budget', - ); - - /** - * Returns the WordPress post_type slug handled by this interceptor. - * - * @return string - */ - protected function get_post_type(): string { - return 'hp_request'; - } - - /** - * 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. - * @param string $now Formatted datetime string. - * @return array{values: array, formats: array} - */ - 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' ), - ); - } -} diff --git a/includes/interceptors/class-tmdo-reviews-interceptor.php b/includes/interceptors/class-tmdo-reviews-interceptor.php deleted file mode 100644 index fa66e6e..0000000 --- a/includes/interceptors/class-tmdo-reviews-interceptor.php +++ /dev/null @@ -1,116 +0,0 @@ - 'rating', - 'hp_listing' => 'listing_id', - '_hp_vendor' => 'vendor_id', - ); - - /** - * 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 { - parent::register_hooks(); - add_filter( 'get_post_metadata', array( $this, 'filter_get_meta' ), 10, 4 ); - } - - /** - * Builds the INSERT row for a newly created review post. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param string $now Formatted datetime string. - * @return array{values: array, formats: array} - */ - 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' ), - ); - } - - /** - * Reads hp_rating directly from the reviews flat table. - * - * @param mixed $value Short-circuit value. - * @param int $post_id Post ID. - * @param string $meta_key Meta key. - * @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 || 'hp_review' !== get_post_type( $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', - $post_id - ) - ); - return null !== $rating ? ( $single ? $rating : array( $rating ) ) : null; - }, - fn() => $value, - 'get_post_metadata' - ); - } -} diff --git a/includes/interceptors/class-tmdo-statistics-interceptor.php b/includes/interceptors/class-tmdo-statistics-interceptor.php deleted file mode 100644 index 4f160eb..0000000 --- a/includes/interceptors/class-tmdo-statistics-interceptor.php +++ /dev/null @@ -1,139 +0,0 @@ -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' ) - ); - } -} diff --git a/includes/query/class-tmdo-listing-meta-query.php b/includes/query/class-tmdo-listing-meta-query.php deleted file mode 100644 index 5d634f1..0000000 --- a/includes/query/class-tmdo-listing-meta-query.php +++ /dev/null @@ -1,236 +0,0 @@ -= 100 - * - * All hp_* and _hp_* meta keys are intercepted. Any other meta keys are - * left in the meta_query to be handled by wp_postmeta as usual. - */ -class TMDO_Listing_Meta_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'listing_meta'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_listing' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_listing_meta'; - } - - /** - * Returns the join column for this KV table. - * - * @return string Join column name. - */ - protected function get_join_column(): string { - return 'listing_id'; - } - - /** - * Not used — this class overrides pre_get_posts directly. - * - * @return array Empty array. - */ - protected function get_meta_key_map(): array { - return array(); - } - - // ── Override pre_get_posts for KV-table pattern ─────────────────────── - - /** - * Intercepts pre_get_posts to handle KV-table meta_query conditions. - * - * @param \WP_Query $query The WP_Query object. - * @return void - */ - public function pre_get_posts( \WP_Query $query ): void { - if ( ! $this->should_intercept( $query ) ) { - return; - } - - $raw_meta_query = (array) $query->get( 'meta_query' ); - if ( empty( $raw_meta_query ) ) { - return; - } - - $our_clauses = array(); - $remaining = array(); - - foreach ( $raw_meta_query as $k => $clause ) { - if ( 'relation' === $k || ! is_array( $clause ) || ! isset( $clause['key'] ) ) { - $remaining[ $k ] = $clause; - continue; - } - - if ( $this->is_hp_key( $clause['key'] ) ) { - $our_clauses[] = array( - 'meta_key' => $clause['key'], - 'value' => $clause['value'] ?? '', - 'compare' => strtoupper( trim( $clause['compare'] ?? '=' ) ), - 'type' => strtoupper( trim( $clause['type'] ?? 'CHAR' ) ), - ); - } else { - $remaining[ $k ] = $clause; - } - } - - if ( empty( $our_clauses ) ) { - return; - } - - if ( isset( $raw_meta_query['relation'] ) && ! isset( $remaining['relation'] ) ) { - $remaining['relation'] = $raw_meta_query['relation']; - } - - $query->set( 'meta_query', $remaining ); - $query->set( $this->query_var(), $our_clauses ); - } - - // ── Override posts_join for KV-table: one JOIN alias per condition ───── - - /** - * Appends LEFT JOINs for each KV-table meta_query condition. - * - * @param string $join Current JOIN SQL. - * @param \WP_Query $query The WP_Query object. - * @return string Modified JOIN SQL. - */ - public function posts_join( ?string $join, \WP_Query $query ): string { - $join = (string) ( $join ?? '' ); - $clauses = $this->get_clauses( $query ); - if ( empty( $clauses ) ) { - return $join; - } - - global $wpdb; - $table = TMDO_DB::table( $this->get_table() ); - $join_col = esc_sql( $this->get_join_column() ); - - foreach ( $clauses as $i => $clause ) { - $alias = $this->alias_for( $i ); - $meta_key = $clause['meta_key']; - - if ( false === strpos( $join, "`{$alias}`" ) ) { - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table from TMDO_DB::table(); $alias and $join_col are sanitize_key()-validated. - $join .= $wpdb->prepare( - " LEFT JOIN `{$table}` AS `{$alias}`" - . " ON (`{$wpdb->posts}`.`ID` = `{$alias}`.`{$join_col}`" - . " AND `{$alias}`.`meta_key` = %s)", - $meta_key - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } - } - - return $join; - } - - // ── Override posts_where for KV-table ───────────────────────────────── - - /** - * Appends WHERE conditions for KV-table meta_value comparisons. - * - * @param string $where Current WHERE SQL. - * @param \WP_Query $query The WP_Query object. - * @return string Modified WHERE SQL. - */ - public function posts_where( ?string $where, \WP_Query $query ): string { - $where = (string) ( $where ?? '' ); - $clauses = $this->get_clauses( $query ); - if ( empty( $clauses ) ) { - return $where; - } - - foreach ( $clauses as $i => $clause ) { - $alias = $this->alias_for( $i ); - $col = "`{$alias}`.`meta_value`"; - $compare = $this->sanitize_compare( $clause['compare'] ); - $type = $clause['type']; - $value = $clause['value']; - - $where .= $this->build_condition( $col, $compare, $type, $value ); - } - - return $where; - } - - // ── Override posts_groupby ───────────────────────────────────────────── - - /** - * Ensures GROUP BY is set when KV-table clauses are active. - * - * @param string $groupby Current GROUP BY SQL. - * @param \WP_Query $query The WP_Query object. - * @return string Modified GROUP BY SQL. - */ - public function posts_groupby( ?string $groupby, \WP_Query $query ): string { - $groupby = (string) ( $groupby ?? '' ); - $clauses = $this->get_clauses( $query ); - if ( empty( $clauses ) ) { - return $groupby; - } - - global $wpdb; - if ( '' === trim( $groupby ) ) { - $groupby = "`{$wpdb->posts}`.`ID`"; - } - - return $groupby; - } - - // ── Helpers ─────────────────────────────────────────────────────────── - - /** - * Generates a unique SQL alias for a KV-table JOIN at a given index. - * - * @param int $index Zero-based clause index. - * @return string SQL alias string. - */ - private function alias_for( int $index ): string { - return "wpdo_lm_{$index}"; - } - - /** - * Checks whether a meta key belongs to HivePress (hp_ or _hp_ prefix). - * - * @param string $meta_key Meta key to check. - * @return bool True if the key is an HP key. - */ - private function is_hp_key( string $meta_key ): bool { - return str_starts_with( $meta_key, 'hp_' ) || str_starts_with( $meta_key, '_hp_' ); - } -} diff --git a/includes/query/class-tmdo-memberships-query.php b/includes/query/class-tmdo-memberships-query.php deleted file mode 100644 index 083c8bf..0000000 --- a/includes/query/class-tmdo-memberships-query.php +++ /dev/null @@ -1,67 +0,0 @@ - plan_id (BIGINT) - * hp_user => user_id (BIGINT) - * hp_price => price (VARCHAR) - * hp_order => order_id (BIGINT) - * hp_start_date => start_date (DATETIME) - * hp_end_date => end_date (DATETIME) - */ -class TMDO_Memberships_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'memberships'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_membership' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_memberships'; - } - - /** - * Returns the meta_key to column mapping for hpct_memberships. - * - * @return array Meta key to column name map. - */ - protected function get_meta_key_map(): array { - return 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', - ); - } -} diff --git a/includes/query/class-tmdo-messages-query.php b/includes/query/class-tmdo-messages-query.php deleted file mode 100644 index 604806f..0000000 --- a/includes/query/class-tmdo-messages-query.php +++ /dev/null @@ -1,63 +0,0 @@ - sender_id (BIGINT) - * hp_recipient => recipient_id (BIGINT) - * hp_listing => listing_id (BIGINT) - * hp_read => is_read (TINYINT) - */ -class TMDO_Messages_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'messages'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_message' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_messages'; - } - - /** - * Returns the meta_key to column mapping for hpct_messages. - * - * @return array Meta key to column name map. - */ - protected function get_meta_key_map(): array { - return array( - 'hp_sender' => 'sender_id', - 'hp_recipient' => 'recipient_id', - 'hp_listing' => 'listing_id', - 'hp_read' => 'is_read', - ); - } -} diff --git a/includes/query/class-tmdo-requests-query.php b/includes/query/class-tmdo-requests-query.php deleted file mode 100644 index bdf5e58..0000000 --- a/includes/query/class-tmdo-requests-query.php +++ /dev/null @@ -1,61 +0,0 @@ - vendor_id (BIGINT) - * hp_listing => listing_id (BIGINT) - * hp_budget => budget (VARCHAR) - */ -class TMDO_Requests_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'requests'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_request' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_requests'; - } - - /** - * Returns the meta_key to column mapping for hpct_requests. - * - * @return array Meta key to column name map. - */ - protected function get_meta_key_map(): array { - return array( - 'hp_vendor' => 'vendor_id', - 'hp_listing' => 'listing_id', - 'hp_budget' => 'budget', - ); - } -} diff --git a/includes/query/class-tmdo-reviews-query.php b/includes/query/class-tmdo-reviews-query.php deleted file mode 100644 index 55804c6..0000000 --- a/includes/query/class-tmdo-reviews-query.php +++ /dev/null @@ -1,64 +0,0 @@ - listing_id (BIGINT) - * _hp_vendor => vendor_id (BIGINT) - * hp_rating => rating (TINYINT) - */ -class TMDO_Reviews_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'reviews'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_review' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_reviews'; - } - - /** - * Returns the meta_key to column mapping for hpct_reviews. - * - * @return array Meta key to column name map. - */ - protected function get_meta_key_map(): array { - return array( - 'hp_listing' => 'listing_id', - '_hp_vendor' => 'vendor_id', - 'hp_rating' => 'rating', - ); - } -} diff --git a/tests/integration/ListingMetaInterceptorTest.php b/tests/integration/ListingMetaInterceptorTest.php deleted file mode 100644 index 6f0a04a..0000000 --- a/tests/integration/ListingMetaInterceptorTest.php +++ /dev/null @@ -1,160 +0,0 @@ -query( "DROP TABLE IF EXISTS `{$wpdb->prefix}hpct_listing_meta`" ); - - // Simulate HPCT_Core being present — register_hooks() only logs the skip - // when HPCT is loaded but its table is missing (genuine inconsistency). - if ( ! class_exists( 'HPCT_Core' ) ) { - // phpcs:ignore Generic.Commenting.InlineComment.InvalidEndChar -- runtime stub - eval( 'class HPCT_Core {}' ); // @phpcs:ignore - } - - // Ensure wpdo_errors exists for the skip log; harmless if pre-created. - $wpdb->query( - "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}wpdo_errors` ( - id bigint(20) unsigned NOT NULL AUTO_INCREMENT, - module varchar(50) NOT NULL DEFAULT '', - zone varchar(10) NOT NULL DEFAULT '', - hook varchar(255) NOT NULL DEFAULT '', - message longtext NOT NULL, - context longtext, - created_at datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (id), - KEY idx_module (module) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" - ); - - // Snapshot pre-call count of skip messages. - $pre = (int) $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_errors` WHERE module = %s AND hook = %s", - 'listing_meta', - 'register_hooks' - ) - ); - - $interceptor = new WPDO_Listing_Meta_Interceptor(); - $interceptor->register_hooks(); - - $post = (int) $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_errors` WHERE module = %s AND hook = %s", - 'listing_meta', - 'register_hooks' - ) - ); - $this->assertSame( $pre + 1, $post, 'Expected exactly one new skip message after register_hooks()' ); - } - - /** - * P-C1: WPDO_DB::upsert() must produce an INSERT ... ON DUPLICATE KEY UPDATE - * statement (single round-trip), and writes must succeed via the composite - * unique key (listing_id, meta_key). - * - * @runInSeparateProcess - */ - public function test_upsert_uses_composite_unique_key(): void { - global $wpdb; - - $table = $wpdb->prefix . 'hpct_listing_meta'; - - // Clean slate. - $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" ); - $wpdb->query( - "CREATE TABLE `{$table}` ( - id bigint(20) unsigned NOT NULL AUTO_INCREMENT, - listing_id bigint(20) unsigned NOT NULL DEFAULT 0, - meta_key varchar(255) NOT NULL DEFAULT '', - meta_value longtext, - PRIMARY KEY (id), - UNIQUE KEY ui_listing_meta (listing_id, meta_key(191)) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" - ); - - // First upsert: INSERT. - $result1 = WPDO_DB::upsert( - $table, - array( - 'listing_id' => 4242, - 'meta_key' => 'hp_price', - 'meta_value' => '100.00', - ), - array( 'meta_value' ), - array( 'listing_id', 'meta_key' ), - array( '%d', '%s', '%s' ) - ); - $this->assertNotFalse( $result1 ); - - // Second upsert: UPDATE via the composite key. - $result2 = WPDO_DB::upsert( - $table, - array( - 'listing_id' => 4242, - 'meta_key' => 'hp_price', - 'meta_value' => '250.50', - ), - array( 'meta_value' ), - array( 'listing_id', 'meta_key' ), - array( '%d', '%s', '%s' ) - ); - $this->assertNotFalse( $result2 ); - - // Verify only one row exists for (4242, 'hp_price') with the latest value. - $rows = $wpdb->get_results( - $wpdb->prepare( - "SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s", - 4242, - 'hp_price' - ), - ARRAY_A - ); - $this->assertCount( 1, $rows, 'Composite UNIQUE must collapse two upserts into a single row' ); - $this->assertSame( '250.50', $rows[0]['meta_value'] ); - - // Cleanup. - $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" ); - } - - /** - * Verify table_exists() returns false when table missing, true after CREATE. - * - * @runInSeparateProcess - */ - public function test_table_exists_detection(): void { - global $wpdb; - - $wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}hpct_listing_meta`" ); - - // Use reflection to invoke private static. - $ref = new ReflectionClass( WPDO_Listing_Meta_Interceptor::class ); - $method = $ref->getMethod( 'table_exists' ); - $method->setAccessible( true ); - - $this->assertFalse( $method->invoke( null ), 'table_exists must return false when table missing' ); - } -} diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 72f5713..ec73697 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -29,8 +29,6 @@ define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivep define( 'TMDO_HIVEPRESS_VERSION', '0.1.0' ); // ── AddOn classes ────────────────────────────────────────────────────────── -// Superset of the unit bootstrap's list: the integration suite also exercises -// the interceptors, which the unit suite does not load. $tmdo_hp_files = array( 'includes/class-tmdo-hivepress.php', @@ -54,12 +52,6 @@ $tmdo_hp_files = array( foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/hivepress/adapters/*.php' ) ?: array() as $tmdo_hp_glob ) { $tmdo_hp_files[] = 'includes/hivepress/adapters/' . basename( $tmdo_hp_glob ); } -foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/interceptors/*.php' ) ?: array() as $tmdo_hp_glob ) { - $tmdo_hp_files[] = 'includes/interceptors/' . basename( $tmdo_hp_glob ); -} -foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/query/*.php' ) ?: array() as $tmdo_hp_glob ) { - $tmdo_hp_files[] = 'includes/query/' . basename( $tmdo_hp_glob ); -} foreach ( $tmdo_hp_files as $tmdo_hp_file ) { $tmdo_hp_path = TMDO_HIVEPRESS_PATH . $tmdo_hp_file;