cf47f141c5
Tests / PHP Lint (pull_request) Successful in 12s
TMDO_Infocards::register() 舊版在 bootstrap 當下以 class_exists('TMEETIC_*')
偵測 partner,失敗即 early return。但本 AddOn bootstrap 掛在 plugins_loaded:6,
而 2meet-infocards 要到 :20(tmeetic_run)才 require 自身類別——在 :6 當下
TMEETIC_* 一律不存在,偵測必定失敗,兩個註冊 hook 從未被掛上,整個 AddOn
等同未啟用。
三處修法:
1. 偵測從 bootstrap 移進 hook callback 內執行,hook 一律先掛上
2. 偵測改以 TMEETIC_VERSION / TMEETIC_PLUGIN_DIR 常數優先(檔案解析當下即
define,早於 plugins_loaded),class_exists() 退為後備
3. bootstrap priority 6 → 3;wpdo_register_custom_tables callback prio 設 5,
讓本 AddOn 較豐富的定義勝過主外掛無 expected_columns 的版本
同時修正 IG token 兩個欄位的 meta_key 錯配(tmeetic_ig_access_token_enc /
tmeetic_ig_token_expiry),以及 registry 索引 config key expected_indexes →
indexes。
實測驗證:
- wpdo_register_fields 掛載 prio 10、wpdo_register_custom_tables prio 5
- 2meet_vendor_profiles 由本 AddOn 註冊(16 欄位 / 1 索引 / doctor_callback)
- 缺 expected_columns 由 26/29 降為 0/46
- wp wpdo doctor 60 OK / 0 FAIL;anti-EAV lint 通過;站台 200/200
308 lines
11 KiB
PHP
308 lines
11 KiB
PHP
<?php
|
||
/**
|
||
* TMDO_Infocards — integration with 2meet-infocards plugin (PR-8 / Wave 1).
|
||
*
|
||
* Solves audit finding R-1 (infocards integration layer missing).
|
||
*
|
||
* Responsibilities:
|
||
* - Register tmeetic_profile_* (vendor primary fields) into Hot zone
|
||
* - Register tmeetic_ig_* (Instagram tokens etc.) into Cold zone
|
||
* - Register wp_2meet_vendor_profiles custom table to Custom_Table_Registry
|
||
* - Provide cache loader for inject_profile_styles() so it doesn't query DB
|
||
* on every wp_head call
|
||
*
|
||
* NOTE: 2meet-infocards 主檔(2meet-infocards.php)自 v6.9.24 起也直接掛
|
||
* wpdo_register_fields / wpdo_register_custom_tables(belt-and-suspenders,
|
||
* 繞過舊版 wp-data-optimizer 曾有的時序偵測 bug)。兩邊重複註冊同一批欄位是
|
||
* 刻意的防禦性冗餘,Schema Registry 對同一 meta_key 重複 register 是 idempotent
|
||
* 操作,不會造成資料損壞;但若未來要精簡,應二擇一保留,勿只改其中一邊的 key 名稱。
|
||
*
|
||
* @package TMDO
|
||
* @since 2.0.0
|
||
*/
|
||
|
||
if ( ! defined( 'ABSPATH' ) ) {
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Integration with 2meet-infocards. No-op when the partner plugin isn't loaded.
|
||
*/
|
||
final class TMDO_Infocards {
|
||
|
||
/**
|
||
* Static registration entry. Called from the AddOn bootstrap on plugins_loaded:6.
|
||
*
|
||
* 時序修正:偵測**不可**在此處做。本 AddOn bootstrap 於 plugins_loaded:6 執行,
|
||
* 但 2meet-infocards 要到 plugins_loaded:20(`tmeetic_run`)才 require 自身類別,
|
||
* 因此在 :6 當下 TMEETIC_* 一律不存在 → 舊版在此早退,導致整個 AddOn 從未掛上
|
||
* 任何 hook(實測:TMEETIC_Frontend 在 prio 6 = MISSING、prio 21 = EXISTS)。
|
||
*
|
||
* 核心於 plugins_loaded:30 有 late-bind safety net 會重新 fire 註冊 hook,
|
||
* 所以改為「hook 一律掛上、偵測延後到 callback 執行當下」才會正確生效。
|
||
*
|
||
* priority 5(而非預設 10)的理由:Custom_Table_Registry 對同一
|
||
* `provider:table_name` 是「先註冊者勝、後者回傳 false」。主外掛
|
||
* `2meet-infocards.php` 的 closure 在**檔案解析當下**就掛上 prio 10,早於本
|
||
* AddOn 在 plugins_loaded:6 掛載,同 prio 內先掛先跑 → 主外掛會先以「無
|
||
* expected_columns」的版本佔走 key。本 AddOn 的定義較豐富(含 expected_columns
|
||
* / indexes / doctor_callback),故提前到 prio 5 讓它勝出;主外掛其餘 26 張表
|
||
* 不受影響,仍由它自己註冊。
|
||
*/
|
||
public static function register(): void {
|
||
add_action( 'wpdo_register_fields', array( __CLASS__, 'register_post_fields' ) );
|
||
add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ), 5 );
|
||
}
|
||
|
||
/**
|
||
* Detect the partner plugin.
|
||
*
|
||
* 以常數優先、類別為輔:`TMEETIC_VERSION` / `TMEETIC_PLUGIN_DIR` 在
|
||
* `2meet-infocards.php` 的**檔案解析當下**(第 25–33 行,不在任何 function 內)
|
||
* 就已 define,早於 `plugins_loaded`;而 TMEETIC_* 類別要到 plugins_loaded:20
|
||
* 才 require。核心的 `fire_registration()` 最早在 plugins_loaded:4 觸發,
|
||
* 該時點只有常數可用——只靠 class_exists() 會在最關鍵的第一次註冊時誤判為
|
||
* 「partner 不存在」。
|
||
*/
|
||
private static function partner_detected(): bool {
|
||
return defined( 'TMEETIC_VERSION' )
|
||
|| defined( 'TMEETIC_PLUGIN_DIR' )
|
||
|| class_exists( 'TMEETIC_Plugin' )
|
||
|| class_exists( 'TMEETIC_Frontend' )
|
||
|| class_exists( '\\TwoMeet_InfoCards\\Plugin' );
|
||
}
|
||
|
||
/**
|
||
* Register hp_vendor meta keys owned by 2meet-infocards.
|
||
*
|
||
* @param TMDO_Schema_Registry $registry Singleton instance.
|
||
* @return void
|
||
*/
|
||
public static function register_post_fields( TMDO_Schema_Registry $registry ): void {
|
||
if ( ! self::partner_detected() ) {
|
||
return;
|
||
}
|
||
$registry->register_many(
|
||
'2meet-infocards',
|
||
array(
|
||
// Vendor profile colour fields → Hot zone (varchar columns).
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_profile_primary',
|
||
'zone' => 'hot',
|
||
'data_type' => "varchar(20) NOT NULL DEFAULT ''",
|
||
'column' => 'tmeetic_profile_primary',
|
||
),
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_profile_accent',
|
||
'zone' => 'hot',
|
||
'data_type' => "varchar(20) NOT NULL DEFAULT ''",
|
||
'column' => 'tmeetic_profile_accent',
|
||
),
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_profile_font',
|
||
'zone' => 'hot',
|
||
'data_type' => "varchar(50) NOT NULL DEFAULT ''",
|
||
'column' => 'tmeetic_profile_font',
|
||
),
|
||
// Hero image is descriptive → Cold zone.
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_profile_hero_image',
|
||
'zone' => 'cold',
|
||
'cache_group' => 'wpdo_cold_hp_vendor',
|
||
'cache_ttl' => HOUR_IN_SECONDS,
|
||
),
|
||
// IG tokens — sensitive + display-only → Cold zone.
|
||
// 註:app id / secret 不在此列——那是站台層級的 wp_options
|
||
// (`2meetic_ig_app_id` / `2meetic_ig_app_secret_enc`),非 hp_vendor
|
||
// postmeta。舊版誤註冊的兩個 postmeta 欄位全外掛零讀寫,已移除。
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_ig_access_token_enc',
|
||
'zone' => 'cold',
|
||
'cache_group' => 'wpdo_cold_hp_vendor',
|
||
'cache_ttl' => HOUR_IN_SECONDS,
|
||
),
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_ig_token_expiry',
|
||
'zone' => 'cold',
|
||
'cache_group' => 'wpdo_cold_hp_vendor',
|
||
'cache_ttl' => HOUR_IN_SECONDS,
|
||
),
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_ig_user_id',
|
||
'zone' => 'cold',
|
||
'cache_group' => 'wpdo_cold_hp_vendor',
|
||
'cache_ttl' => HOUR_IN_SECONDS,
|
||
),
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_ig_username',
|
||
'zone' => 'cold',
|
||
'cache_group' => 'wpdo_cold_hp_vendor',
|
||
'cache_ttl' => HOUR_IN_SECONDS,
|
||
),
|
||
array(
|
||
'post_type' => 'hp_vendor',
|
||
'meta_key' => 'tmeetic_ig_feed_show',
|
||
'zone' => 'cold',
|
||
'cache_group' => 'wpdo_cold_hp_vendor',
|
||
'cache_ttl' => HOUR_IN_SECONDS,
|
||
),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Register infocards custom tables so they appear in `wp wpdo doctor`,
|
||
* `benchmark`, and `cleanup` tooling.
|
||
*
|
||
* @param TMDO_Custom_Table_Registry $registry Custom Table Registry singleton.
|
||
* @return void
|
||
*/
|
||
public static function register_custom_tables( TMDO_Custom_Table_Registry $registry ): void {
|
||
if ( ! self::partner_detected() ) {
|
||
return;
|
||
}
|
||
// CSS variable source — checked by WPDO doctor for index health.
|
||
$registry->register(
|
||
'2meet-infocards',
|
||
array(
|
||
'table_name' => '2meet_vendor_profiles',
|
||
'primary_key' => 'vendor_id',
|
||
'post_type_link' => 'hp_vendor',
|
||
'doctor_callback' => array( __CLASS__, 'doctor_vendor_profiles' ),
|
||
'expected_columns' => array(
|
||
'id' => 'BIGINT',
|
||
'vendor_id' => 'BIGINT',
|
||
'primary_color' => 'VARCHAR',
|
||
'accent_color' => 'VARCHAR',
|
||
'bg_color' => 'VARCHAR',
|
||
'text_color' => 'VARCHAR',
|
||
'muted_color' => 'VARCHAR',
|
||
'font' => 'VARCHAR',
|
||
'radius_card' => 'INT',
|
||
'radius_btn' => 'INT',
|
||
'shadow_style' => 'VARCHAR',
|
||
'hero_image_id' => 'BIGINT',
|
||
'hero_fullwidth' => 'TINYINT',
|
||
'ig_feed_show' => 'TINYINT',
|
||
'theme_style_id' => 'BIGINT',
|
||
'updated_at' => 'DATETIME',
|
||
),
|
||
'indexes' => array(
|
||
'vendor_id' => array( 'vendor_id' ),
|
||
),
|
||
)
|
||
);
|
||
$registry->register(
|
||
'2meet-infocards',
|
||
array(
|
||
'table_name' => '2meet_cards',
|
||
'primary_key' => 'id',
|
||
'post_type_link' => 'hp_vendor',
|
||
'expected_columns' => array(
|
||
'id' => 'BIGINT',
|
||
'page_id' => 'BIGINT',
|
||
'story_id' => 'BIGINT',
|
||
'epk_id' => 'BIGINT',
|
||
'card_type' => 'VARCHAR',
|
||
'title' => 'VARCHAR',
|
||
'description' => 'TEXT',
|
||
'image_id' => 'BIGINT',
|
||
'config' => 'TEXT',
|
||
'cta_text' => 'VARCHAR',
|
||
'cta_url' => 'VARCHAR',
|
||
'button_bg_color' => 'VARCHAR',
|
||
'button_text_color' => 'VARCHAR',
|
||
'sort_order' => 'INT',
|
||
'status' => 'VARCHAR',
|
||
'start_at' => 'DATETIME',
|
||
'end_at' => 'DATETIME',
|
||
'vendor_id' => 'BIGINT',
|
||
'show_click_count' => 'TINYINT',
|
||
'ab_enabled' => 'TINYINT',
|
||
'liff_enabled' => 'TINYINT',
|
||
'created_at' => 'DATETIME',
|
||
'updated_at' => 'DATETIME',
|
||
'click_count' => 'BIGINT',
|
||
'last_click_at' => 'DATETIME',
|
||
),
|
||
'indexes' => array(
|
||
'page_id_status_sort' => array( 'page_id', 'status', 'sort_order' ),
|
||
'story_id_status_sort' => array( 'story_id', 'status', 'sort_order' ),
|
||
'epk_id_status_sort' => array( 'epk_id', 'status', 'sort_order' ),
|
||
'card_type' => array( 'card_type' ),
|
||
'vendor_status_sort' => array( 'vendor_id', 'status', 'sort_order' ),
|
||
'status_schedule' => array( 'status', 'start_at', 'end_at' ),
|
||
),
|
||
)
|
||
);
|
||
$registry->register(
|
||
'2meet-infocards',
|
||
array(
|
||
'table_name' => '2meet_theme_styles',
|
||
'primary_key' => 'id',
|
||
'post_type_link' => null,
|
||
'expected_columns' => array(
|
||
'id' => 'BIGINT',
|
||
'name' => 'VARCHAR',
|
||
'slug' => 'VARCHAR',
|
||
'config' => 'TEXT',
|
||
'sort_order' => 'INT',
|
||
'is_active' => 'TINYINT',
|
||
'created_at' => 'DATETIME',
|
||
'updated_at' => 'DATETIME',
|
||
),
|
||
'indexes' => array(
|
||
'slug' => array( 'slug' ),
|
||
),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Doctor check: verifies row count + sample read latency on vendor_profiles.
|
||
*
|
||
* @return array{ok:bool, message:string}
|
||
*/
|
||
public static function doctor_vendor_profiles(): array {
|
||
global $wpdb;
|
||
try {
|
||
$exists = (bool) $wpdb->get_var(
|
||
$wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->prefix . '2meet_vendor_profiles' )
|
||
);
|
||
if ( ! $exists ) {
|
||
return array(
|
||
'ok' => false,
|
||
'message' => 'wp_2meet_vendor_profiles table missing',
|
||
);
|
||
}
|
||
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->prefix}2meet_vendor_profiles`" );
|
||
return array(
|
||
'ok' => true,
|
||
'message' => "wp_2meet_vendor_profiles: {$count} rows",
|
||
);
|
||
} catch ( \Throwable $e ) {
|
||
return array(
|
||
'ok' => false,
|
||
'message' => 'doctor failed: ' . $e->getMessage(),
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Cache key for a vendor profile blob (used by inject_profile_styles).
|
||
*
|
||
* @param int $vendor_id Vendor post ID.
|
||
*/
|
||
public static function vendor_profile_cache_key( int $vendor_id ): string {
|
||
return 'wpdo_vendor_profile_' . $vendor_id;
|
||
}
|
||
}
|