76c01e44df
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入 並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
248 lines
7.0 KiB
PHP
248 lines
7.0 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_Conflict_Monitor — production-time conflict detection for WPDO.
|
|
*
|
|
* Wraps TMDO_Hook_Bus_Bridge::detect_intra_wpdo_conflicts() and the UAE-port
|
|
* TMDO_Conflict_Detector (UAEPG cross-plugin scan) into a single facade with:
|
|
*
|
|
* - admin_init scan + admin_notices warning when conflicts detected
|
|
* - admin_bar warning chip when conflicts > 0 (Part C.1 enforcer requirement)
|
|
* - Persistent log to wpdo_audit table for ops review
|
|
* - Single-source-of-truth `get_all_conflicts()` for CLI / REST surfaces
|
|
*
|
|
* This wrapper is the live monitor; the engine/class-tmdo-conflict-detector.php
|
|
* is the deeper UAEPG-aware scanner. The split keeps WPDO callable without
|
|
* UAEPG present.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
// phpcs:disable Squiz.Commenting.FunctionComment.Missing,Squiz.Commenting.InlineComment.InvalidEndChar,Generic.Commenting.DocComment.MissingShort,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.PHP.YodaConditions,Generic.CodeAnalysis.EmptyStatement -- v2.0.0 partner integrations: pure registration helpers + intentional silent catches.
|
|
|
|
/**
|
|
* Production conflict monitor with admin surface integration.
|
|
*/
|
|
final class TMDO_Conflict_Monitor {
|
|
|
|
/**
|
|
* Cached conflict list per request.
|
|
*
|
|
* @var array<int, array{type:string, hook?:string, priority?:int, callback?:string, entity_type?:string, meta_key?:string}>|null
|
|
*/
|
|
private static ?array $cache = null;
|
|
|
|
/**
|
|
* Register all hooks for production monitor surface.
|
|
*
|
|
* Called from TMDO_Core::run() after all interceptors register.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function register_hooks(): void {
|
|
// init:30 — runs after both wpdo_register_fields (init:20) and the
|
|
// UAE-port Conflict_Detector (init:25), so we observe a complete picture.
|
|
add_action( 'init', array( self::class, 'scan' ), 30 );
|
|
|
|
if ( is_admin() ) {
|
|
add_action( 'admin_notices', array( self::class, 'maybe_render_admin_notice' ) );
|
|
add_action( 'admin_bar_menu', array( self::class, 'maybe_render_admin_bar' ), 999 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run a full scan and cache the result for the request.
|
|
*
|
|
* Aggregates findings from:
|
|
* 1. TMDO_Hook_Bus_Bridge::detect_intra_wpdo_conflicts() — same-hook callback overlap
|
|
* 2. TMDO_Conflict_Detector::scan() — UAEPG cross-plugin field overlap (if present)
|
|
*
|
|
* @return array<int, array>
|
|
*/
|
|
public static function scan(): array {
|
|
if ( null !== self::$cache ) {
|
|
return self::$cache;
|
|
}
|
|
|
|
$conflicts = array();
|
|
|
|
if ( class_exists( 'TMDO_Hook_Bus_Bridge' ) ) {
|
|
foreach ( TMDO_Hook_Bus_Bridge::detect_intra_wpdo_conflicts() as $finding ) {
|
|
$conflicts[] = array_merge( array( 'type' => 'hook_overlap' ), $finding );
|
|
}
|
|
}
|
|
|
|
if ( class_exists( 'TMDO_Conflict_Detector' ) ) {
|
|
foreach ( TMDO_Conflict_Detector::scan() as $finding ) {
|
|
$conflicts[] = array_merge( array( 'type' => 'uaepg_overlap' ), $finding );
|
|
}
|
|
}
|
|
|
|
self::$cache = $conflicts;
|
|
|
|
// Persist a single summary row to wpdo_audit when conflicts exist.
|
|
if ( $conflicts ) {
|
|
self::persist_audit_summary( $conflicts );
|
|
}
|
|
|
|
return $conflicts;
|
|
}
|
|
|
|
/**
|
|
* Get the cached conflict list, scanning lazily if needed.
|
|
*
|
|
* @return array<int, array>
|
|
*/
|
|
public static function get_all_conflicts(): array {
|
|
return self::$cache ?? self::scan();
|
|
}
|
|
|
|
/**
|
|
* Conflict count by type.
|
|
*
|
|
* @return array{total:int, hook_overlap:int, uaepg_overlap:int}
|
|
*/
|
|
public static function get_summary(): array {
|
|
$conflicts = self::get_all_conflicts();
|
|
$summary = array(
|
|
'total' => count( $conflicts ),
|
|
'hook_overlap' => 0,
|
|
'uaepg_overlap' => 0,
|
|
);
|
|
foreach ( $conflicts as $c ) {
|
|
$type = $c['type'] ?? 'unknown';
|
|
if ( isset( $summary[ $type ] ) ) {
|
|
++$summary[ $type ];
|
|
}
|
|
}
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* Reset request cache. Test helper.
|
|
*
|
|
* @internal
|
|
*/
|
|
public static function reset_cache(): void {
|
|
self::$cache = null;
|
|
}
|
|
|
|
/**
|
|
* Render an admin notice when conflicts are present.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function maybe_render_admin_notice(): void {
|
|
if ( ! TMDO_Capability::current_user_can_admin() ) {
|
|
return;
|
|
}
|
|
$summary = self::get_summary();
|
|
if ( $summary['total'] === 0 ) {
|
|
return;
|
|
}
|
|
|
|
$message = sprintf(
|
|
/* translators: %d: conflict count */
|
|
esc_html__( 'WPDO Conflict Detector:偵測到 %d 個欄位 / hook 衝突 — 可能造成資料靜默遺失。', '2meet-data-optimizer' ),
|
|
$summary['total']
|
|
);
|
|
$link = esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=conflicts' ) );
|
|
|
|
printf(
|
|
'<div class="notice notice-error"><p>%s <a href="%s">%s</a></p></div>',
|
|
esc_html( $message ),
|
|
esc_url( $link ),
|
|
esc_html__( '查看詳情', '2meet-data-optimizer' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render a warning chip in the admin bar.
|
|
*
|
|
* @param mixed $wp_admin_bar WP_Admin_Bar instance.
|
|
* @return void
|
|
*/
|
|
public static function maybe_render_admin_bar( $wp_admin_bar ): void {
|
|
if ( ! is_object( $wp_admin_bar ) || ! method_exists( $wp_admin_bar, 'add_node' ) ) {
|
|
return;
|
|
}
|
|
if ( ! TMDO_Capability::current_user_can_admin() ) {
|
|
return;
|
|
}
|
|
$summary = self::get_summary();
|
|
if ( $summary['total'] === 0 ) {
|
|
return;
|
|
}
|
|
|
|
$wp_admin_bar->add_node(
|
|
array(
|
|
'id' => 'wpdo-conflicts',
|
|
'title' => sprintf(
|
|
/* translators: %d: conflict count */
|
|
'⚠️ ' . esc_html__( 'Anti-EAV: %d conflicts', '2meet-data-optimizer' ),
|
|
$summary['total']
|
|
),
|
|
'href' => admin_url( 'tools.php?page=wp-data-optimizer&tab=conflicts' ),
|
|
'meta' => array( 'class' => 'wpdo-conflict-warning' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Persist a summary row to wpdo_audit (silently swallows errors when table missing).
|
|
*
|
|
* @param array<int,array> $conflicts All findings.
|
|
* @return void
|
|
*/
|
|
private static function persist_audit_summary( array $conflicts ): void {
|
|
try {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . 'wpdo_audit';
|
|
$wpdb->insert(
|
|
$table,
|
|
array(
|
|
'ts' => current_time( 'mysql' ),
|
|
'user_id' => 0,
|
|
'entity_type' => 'system',
|
|
'entity_id' => 0,
|
|
'meta_key' => '',
|
|
'op' => 'conflict_scan',
|
|
'value_before' => null,
|
|
'value_after' => wp_json_encode( $conflicts ),
|
|
'source' => 'monitor',
|
|
'trace_id' => self::generate_trace_id(),
|
|
),
|
|
array( '%s', '%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s' )
|
|
);
|
|
} catch ( \Throwable $e ) {
|
|
// wpdo_audit not yet installed (pre-v2 upgrade) — skip silently.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a UUID-like trace id (v4-ish, no external deps).
|
|
*
|
|
* @return string
|
|
*/
|
|
private static function generate_trace_id(): string {
|
|
// PHP 8.1+ random_bytes is always available.
|
|
try {
|
|
$bytes = random_bytes( 16 );
|
|
} catch ( \Throwable $e ) {
|
|
$bytes = pack( 'H*', md5( (string) microtime( true ) . wp_generate_password( 16, false ) ) );
|
|
}
|
|
$bytes[6] = chr( ( ord( $bytes[6] ) & 0x0f ) | 0x40 );
|
|
$bytes[8] = chr( ( ord( $bytes[8] ) & 0x3f ) | 0x80 );
|
|
return vsprintf(
|
|
'%s%s-%s-%s-%s-%s%s%s',
|
|
str_split( bin2hex( $bytes ), 4 )
|
|
);
|
|
}
|
|
}
|