fix(2meet-data-optimizer): refuse aeav_only and cleanup on HivePress / WooCommerce sites
- Add TMDO_Promotion_Guard: HivePress models read every meta key with an empty-key get_*_meta() and WooCommerce does the same for products (and writes stock/sales with direct SQL), so on those sites native meta must stay authoritative. Promotion to aeav_only is refused in TMDO_Mode_Manager::set() (covers REST, CLI, phases, auto-promoter) for post/user/term/comment with HivePress and post/user with WooCommerce; TMDO_Post_Migration::cleanup() and the cleanup phase refuse too. Demotion stays open; filter tmdo_promotion_guard_allow overrides. - Admin notice lists guarded entities already in aeav_only with the restore-native command (no forced demotion on read). - Verify: PromotionGuardTest 8 OK; unit 668 OK, integration 480 OK, phpcs (phpcs.xml) 0, stan OK; dev36 live: 4 promotions + post-cleanup refused, wpdo_bridge_modes unchanged - PR: PR-053
This commit is contained in:
@@ -140,6 +140,11 @@ final class TMDO_Mode_Manager {
|
||||
);
|
||||
}
|
||||
|
||||
$guarded = TMDO_Promotion_Guard::check_promotion( $entity_type, $new_mode );
|
||||
if ( $guarded ) {
|
||||
return $guarded;
|
||||
}
|
||||
|
||||
$all = self::all();
|
||||
$all[ $entity_type ] = $new_mode;
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/**
|
||||
* TMDO_Promotion_Guard — keeps native meta authoritative on HivePress /
|
||||
* WooCommerce sites (ADR-004 "search-index mode").
|
||||
*
|
||||
* Both plugins read native meta without going through a keyed
|
||||
* get_*_meta() call: HivePress models load every key with get_*_meta( $id ),
|
||||
* WooCommerce does the same for products and writes stock / sales with direct
|
||||
* SQL. The Hook Bus cannot serve those reads from flat tables, so on these
|
||||
* sites an entity must never reach aeav_only and native rows must never be
|
||||
* cleaned up. Demotion always stays open.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuses aeav_only promotion and native-meta cleanup where an integration
|
||||
* reads native meta directly.
|
||||
*/
|
||||
final class TMDO_Promotion_Guard {
|
||||
|
||||
/**
|
||||
* Entity types each integration reads natively.
|
||||
*
|
||||
* HivePress models (Post, User, Term, Comment) all load meta with an empty
|
||||
* key; WooCommerce does so for products and customers.
|
||||
*/
|
||||
private const GUARDED = array(
|
||||
'HivePress' => array( 'post', 'user', 'term', 'comment' ),
|
||||
'WooCommerce' => array( 'post', 'user' ),
|
||||
);
|
||||
|
||||
/**
|
||||
* Check a mode change. Only promotion to aeav_only is guarded.
|
||||
*
|
||||
* @param string $entity_type Entity type.
|
||||
* @param string $new_mode Target mode.
|
||||
* @return WP_Error|null Error when refused.
|
||||
*/
|
||||
public static function check_promotion( string $entity_type, string $new_mode ): ?WP_Error {
|
||||
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY !== $new_mode ) {
|
||||
return null;
|
||||
}
|
||||
return self::refuse( $entity_type, TMDO_Mode_Manager::MODE_AEAV_ONLY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a cleanup of the entity's native managed meta rows.
|
||||
*
|
||||
* @param string $entity_type Entity type.
|
||||
* @return WP_Error|null Error when refused.
|
||||
*/
|
||||
public static function check_cleanup( string $entity_type ): ?WP_Error {
|
||||
return self::refuse( $entity_type, 'cleanup' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarded entity types that are already in aeav_only (promoted before the
|
||||
* guard existed). Native meta may be stale there.
|
||||
*
|
||||
* @return array<string, string[]> Entity type → integration names.
|
||||
*/
|
||||
public static function violations(): array {
|
||||
$out = array();
|
||||
foreach ( TMDO_Mode_Manager::all() as $type => $mode ) {
|
||||
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY !== $mode ) {
|
||||
continue;
|
||||
}
|
||||
$integrations = self::integrations_for( $type );
|
||||
if ( ! empty( $integrations ) ) {
|
||||
$out[ $type ] = $integrations;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active integrations that read this entity type natively.
|
||||
*
|
||||
* @param string $entity_type Entity type.
|
||||
* @return string[]
|
||||
*/
|
||||
private static function integrations_for( string $entity_type ): array {
|
||||
$active = array(
|
||||
'HivePress' => TMDO_Compatibility::is_hivepress_active(),
|
||||
'WooCommerce' => TMDO_Compatibility::is_woocommerce_active(),
|
||||
);
|
||||
|
||||
$names = array();
|
||||
foreach ( self::GUARDED as $name => $types ) {
|
||||
if ( $active[ $name ] && in_array( $entity_type, $types, true ) ) {
|
||||
$names[] = $name;
|
||||
}
|
||||
}
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the refusal, unless no integration applies or a filter allows it.
|
||||
*
|
||||
* @param string $entity_type Entity type.
|
||||
* @param string $action 'aeav_only' or 'cleanup'.
|
||||
* @return WP_Error|null
|
||||
*/
|
||||
private static function refuse( string $entity_type, string $action ): ?WP_Error {
|
||||
$integrations = self::integrations_for( $entity_type );
|
||||
if ( empty( $integrations ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow a guarded aeav_only promotion or cleanup anyway.
|
||||
*
|
||||
* @param bool $allow Default false.
|
||||
* @param string $entity_type Entity type.
|
||||
* @param string $action 'aeav_only' or 'cleanup'.
|
||||
* @param string[] $integrations Integrations that read this entity natively.
|
||||
*/
|
||||
if ( apply_filters( 'tmdo_promotion_guard_allow', false, $entity_type, $action, $integrations ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'promotion_guarded',
|
||||
sprintf(
|
||||
/* translators: 1: integration names, 2: entity type, 3: aeav_only or cleanup */
|
||||
__( '%1$s 直接讀取原生 meta,%2$s 不能執行 %3$s:這類站台以原生 meta 為唯一資料來源(搜尋索引模式)。', '2meet-data-optimizer' ),
|
||||
implode( ' / ', $integrations ),
|
||||
$entity_type,
|
||||
$action
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -228,6 +228,10 @@ final class TMDO_Post_Migration {
|
||||
'Refusing post cleanup — mode is ' . esc_html( $mode ) . ', must be aeav_only'
|
||||
);
|
||||
}
|
||||
$guarded = TMDO_Promotion_Guard::check_cleanup( self::ENTITY_TYPE );
|
||||
if ( $guarded ) {
|
||||
throw new RuntimeException( esc_html( $guarded->get_error_message() ) );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$keys = self::get_managed_keys();
|
||||
|
||||
@@ -46,6 +46,10 @@ class TMDO_Phase_Cleanup extends TMDO_Migration_Phase_Base {
|
||||
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY !== $mode ) {
|
||||
throw new \RuntimeException( "Refusing cleanup — mode is {$mode}, must be aeav_only" );
|
||||
}
|
||||
$guarded = TMDO_Promotion_Guard::check_cleanup( $this->entity_type );
|
||||
if ( $guarded ) {
|
||||
throw new \RuntimeException( $guarded->get_error_message() );
|
||||
}
|
||||
if ( ! empty( $job['options']['auto_backup'] ) && empty( $job['backup_path'] ) ) {
|
||||
throw new \RuntimeException( 'Refusing cleanup — auto_backup requested but no backup_path on record' );
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user