d040400da8
4 個 permission_callback => '__return_true' 的公開端點先前回傳 hot+cold 全部欄位,未註冊 show_in_rest 的私密欄位隨之外洩(A v3.3.1,CVSS 5.3)。 - Schema_Registry::register() 新增 show_in_rest 預設 true - 新增 get_rest_visible_hot_columns() / get_rest_visible_cold_keys() - get_listing() 以 array_intersect_key 過濾,保留 id / post_type, 並提供 wpdo_rest_listing_visible_fields / tmdo_* filter 供調整 - listings_from_zone_a() 同樣以 visible_cols 過濾 unit 379 / integration 398 GREEN Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
324 lines
8.7 KiB
PHP
324 lines
8.7 KiB
PHP
<?php
|
|
/**
|
|
* Central registry for zone field configurations.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Central registry for zone field configurations.
|
|
*
|
|
* Plugins register their meta_key → zone mappings here.
|
|
* The registry drives table creation, migration, interceptors, and query routing.
|
|
*
|
|
* Usage:
|
|
* $registry = TMDO_Schema_Registry::instance();
|
|
* $registry->register( 'hivepress', [
|
|
* 'post_type' => 'hp_listing',
|
|
* 'meta_key' => 'hp_price',
|
|
* 'zone' => 'hot',
|
|
* 'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0',
|
|
* 'column' => 'hp_price',
|
|
* 'indexed' => true,
|
|
* ]);
|
|
*/
|
|
class TMDO_Schema_Registry {
|
|
|
|
/**
|
|
* Singleton instance.
|
|
*
|
|
* @var self|null
|
|
*/
|
|
private static ?self $instance = null;
|
|
|
|
/**
|
|
* Registered field mappings.
|
|
*
|
|
* Structure: [ 'post_type:meta_key' => field_config, ... ]
|
|
*
|
|
* @var array<string, array>
|
|
*/
|
|
private array $fields = array();
|
|
|
|
/**
|
|
* Zone → post_type → columns map for Zone A (hot) table creation.
|
|
*
|
|
* @var array<string, array<string, array>>
|
|
*/
|
|
private array $hot_columns = array();
|
|
|
|
/**
|
|
* Zone C (cold) fields grouped by post_type.
|
|
*
|
|
* @var array<string, string[]>
|
|
*/
|
|
private array $cold_fields = array();
|
|
|
|
/**
|
|
* Zone B (warm) fields with TTL config.
|
|
*
|
|
* @var array<string, array>
|
|
*/
|
|
private array $warm_fields = array();
|
|
|
|
/**
|
|
* Private constructor — use instance() to get the singleton.
|
|
*/
|
|
private function __construct() {}
|
|
|
|
/**
|
|
* Get the singleton instance.
|
|
*/
|
|
public static function instance(): self {
|
|
if ( null === self::$instance ) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Register a field mapping.
|
|
*
|
|
* @param string $provider Provider name (e.g. 'hivepress', 'woocommerce').
|
|
* @param array $config Field configuration:
|
|
* - post_type (string) WordPress post type.
|
|
* - meta_key (string) The wp_postmeta meta_key.
|
|
* - zone (string) hot|warm|cold|archive
|
|
* - data_type (string) SQL column type for Zone A (e.g. 'decimal(10,2) NOT NULL DEFAULT 0').
|
|
* - column (string) Column name in zone table (defaults to sanitized meta_key).
|
|
* - indexed (bool) Whether to add an index (Zone A only).
|
|
* - ttl (int) TTL in seconds (Zone B only, null = no expiry).
|
|
* - cache_group (string) Object cache group (Zone C only).
|
|
* - cache_ttl (int) Cache TTL in seconds (Zone C only).
|
|
* - show_in_rest (bool) Whether to expose via GET /wpdo/v1/listings. Default true.
|
|
*/
|
|
public function register( string $provider, array $config ): void {
|
|
$config = wp_parse_args(
|
|
$config,
|
|
array(
|
|
'post_type' => '',
|
|
'entity_type' => '',
|
|
'meta_key' => '',
|
|
'zone' => 'hot',
|
|
'data_type' => 'longtext',
|
|
'column' => '',
|
|
'indexed' => false,
|
|
'ttl' => null,
|
|
'cache_group' => '',
|
|
'cache_ttl' => HOUR_IN_SECONDS,
|
|
'provider' => $provider,
|
|
'show_in_rest' => true,
|
|
)
|
|
);
|
|
|
|
// v2.1.2 fix: normalize post_type vs entity_type. Some partner plugins
|
|
// (e.g. 2meet-liff) register fields against entities other than posts
|
|
// (user / term / comment) using `entity_type` instead of `post_type`.
|
|
// Without normalization, those fields end up with empty-string post_type,
|
|
// polluting `get_hot_post_types()` output. Use entity_type as bucket key.
|
|
if ( empty( $config['post_type'] ) && ! empty( $config['entity_type'] ) ) {
|
|
$config['post_type'] = sanitize_key( $config['entity_type'] );
|
|
}
|
|
|
|
if ( empty( $config['column'] ) ) {
|
|
$config['column'] = sanitize_key( $config['meta_key'] );
|
|
}
|
|
|
|
$key = $config['post_type'] . ':' . $config['meta_key'];
|
|
$config['provider'] = $provider;
|
|
$this->fields[ $key ] = $config;
|
|
|
|
// Index by zone for efficient lookup.
|
|
switch ( $config['zone'] ) {
|
|
case 'hot':
|
|
$this->hot_columns[ $config['post_type'] ][ $config['column'] ] = $config['data_type'];
|
|
break;
|
|
|
|
case 'warm':
|
|
$this->warm_fields[ $config['meta_key'] ] = $config;
|
|
break;
|
|
|
|
case 'cold':
|
|
$this->cold_fields[ $config['post_type'] ][] = $config['meta_key'];
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk register multiple fields.
|
|
*
|
|
* @param string $provider Provider name.
|
|
* @param array $configs Array of field configs.
|
|
*/
|
|
public function register_many( string $provider, array $configs ): void {
|
|
foreach ( $configs as $config ) {
|
|
$this->register( $provider, $config );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the zone configuration for a specific field.
|
|
*
|
|
* @param string $post_type Post type.
|
|
* @param string $meta_key Meta key.
|
|
* @return array|null Field config or null if not registered.
|
|
*/
|
|
public function get_field( string $post_type, string $meta_key ): ?array {
|
|
$key = $post_type . ':' . $meta_key;
|
|
return $this->fields[ $key ] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get all fields for a specific zone.
|
|
*
|
|
* @param string $zone Zone identifier (hot/warm/cold/archive).
|
|
* @return array Array of field configs.
|
|
*/
|
|
public function get_zone_fields( string $zone ): array {
|
|
return array_filter( $this->fields, fn( $f ) => $f['zone'] === $zone );
|
|
}
|
|
|
|
/**
|
|
* Get all fields for a specific zone and post type.
|
|
*
|
|
* @param string $zone Zone identifier.
|
|
* @param string $post_type Post type.
|
|
* @return array
|
|
*/
|
|
public function get_zone_fields_for_type( string $zone, string $post_type ): array {
|
|
return array_filter(
|
|
$this->fields,
|
|
fn( $f ) => $f['zone'] === $zone && $f['post_type'] === $post_type
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get hot column definitions for a post type (for table creation).
|
|
*
|
|
* @param string $post_type Post type.
|
|
* @return array<string, string> Column name => SQL type.
|
|
*/
|
|
public function get_hot_columns( string $post_type ): array {
|
|
return $this->hot_columns[ $post_type ] ?? array();
|
|
}
|
|
|
|
/**
|
|
* Get all post types that have hot zone fields.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public function get_hot_post_types(): array {
|
|
return array_keys( $this->hot_columns );
|
|
}
|
|
|
|
/**
|
|
* Get all post types that have cold zone fields.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public function get_cold_post_types(): array {
|
|
return array_keys( $this->cold_fields );
|
|
}
|
|
|
|
/**
|
|
* Get cold field meta keys for a post type.
|
|
*
|
|
* @param string $post_type Post type.
|
|
* @return string[] Array of meta keys.
|
|
*/
|
|
public function get_cold_meta_keys( string $post_type ): array {
|
|
return array_unique( $this->cold_fields[ $post_type ] ?? array() );
|
|
}
|
|
|
|
/**
|
|
* Returns hot column names for a post type where show_in_rest is true.
|
|
* Used by REST endpoints to restrict publicly exposed fields.
|
|
*
|
|
* @param string $post_type Post type.
|
|
* @return string[] Column names that are REST-visible.
|
|
*/
|
|
public function get_rest_visible_hot_columns( string $post_type ): array {
|
|
$visible = array();
|
|
foreach ( $this->fields as $field ) {
|
|
if ( 'hot' === $field['zone'] && $field['post_type'] === $post_type && ! empty( $field['show_in_rest'] ) ) {
|
|
$visible[] = $field['column'];
|
|
}
|
|
}
|
|
return $visible;
|
|
}
|
|
|
|
/**
|
|
* Returns cold meta keys for a post type where show_in_rest is true.
|
|
* Used by REST endpoints to restrict publicly exposed fields.
|
|
*
|
|
* @param string $post_type Post type.
|
|
* @return string[] Meta keys that are REST-visible.
|
|
*/
|
|
public function get_rest_visible_cold_keys( string $post_type ): array {
|
|
$visible = array();
|
|
foreach ( $this->fields as $field ) {
|
|
if ( 'cold' === $field['zone'] && $field['post_type'] === $post_type && ! empty( $field['show_in_rest'] ) ) {
|
|
$visible[] = $field['meta_key'];
|
|
}
|
|
}
|
|
return array_unique( $visible );
|
|
}
|
|
|
|
/**
|
|
* Get warm field configuration.
|
|
*
|
|
* @param string $meta_key Meta key.
|
|
* @return array|null
|
|
*/
|
|
public function get_warm_field( string $meta_key ): ?array {
|
|
return $this->warm_fields[ $meta_key ] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Check which zone a meta_key belongs to for a given post type.
|
|
*
|
|
* @param string $post_type Post type.
|
|
* @param string $meta_key Meta key.
|
|
* @return string|null Zone name or null if not registered.
|
|
*/
|
|
public function get_field_zone( string $post_type, string $meta_key ): ?string {
|
|
$field = $this->get_field( $post_type, $meta_key );
|
|
return $field['zone'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get all registered fields (for admin dashboard / analysis).
|
|
*
|
|
* @return array<string, array>
|
|
*/
|
|
public function all(): array {
|
|
return $this->fields;
|
|
}
|
|
|
|
/**
|
|
* Get summary statistics for the admin dashboard.
|
|
*
|
|
* @return array{hot: int, warm: int, cold: int, archive: int, total: int}
|
|
*/
|
|
public function get_stats(): array {
|
|
$stats = array(
|
|
'hot' => 0,
|
|
'warm' => 0,
|
|
'cold' => 0,
|
|
'archive' => 0,
|
|
'total' => 0,
|
|
);
|
|
foreach ( $this->fields as $field ) {
|
|
if ( isset( $stats[ $field['zone'] ] ) ) {
|
|
++$stats[ $field['zone'] ];
|
|
}
|
|
++$stats['total'];
|
|
}
|
|
return $stats;
|
|
}
|
|
}
|