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 */ 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|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 */ 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 */ 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( '

%s %s

', 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 $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 ) ); } }