chore: initial snapshot of 2meet-data-optimizer v0.1.0

Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
2026-07-31 05:06:36 +08:00
commit d36bb954d1
206 changed files with 66538 additions and 0 deletions
+245
View File
@@ -0,0 +1,245 @@
<?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
*/
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 )
);
}
}