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
@@ -0,0 +1,249 @@
<?php // phpcs:ignore Squiz.Classes.ClassFileName.NoMatch -- Abstract class; filename intentionally uses 'abstract-class-' prefix.
/**
* TMDO_Abstract_Notifier — Base for all health-alert notifiers (v2.5.0 M15).
*
* Subclasses (Email / Slack / Telegram / Discord) inherit:
* - wpdo/health_alert_critical action subscription
* - opt-in toggle (`wpdo_{channel}_enabled`)
* - 24h-default throttle keyed by md5 fingerprint of summary
* - shared subject / body builders (overridable)
* - severity subscription (critical_only / critical_and_recommended)
*
* Subclass只 implements `actually_send( $subject, $body, $context ): bool`
* and `channel_id(): string` (used as option-key prefix).
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Abstract base — concrete subclasses below.
*/
abstract class TMDO_Abstract_Notifier {
/** Default throttle window. */
public const DEFAULT_THROTTLE_HOURS = 24;
/**
* Channel ID — used as option key prefix (e.g. 'email', 'slack').
*
* @return string
*/
abstract public static function channel_id(): string;
/**
* Channel-specific delivery. Implementations should return false on
* transient failure so caller can re-throttle attempts.
*
* @param string $subject Subject / title (Slack uses as text-prefix).
* @param string $body Body text (channel-specific markdown).
* @param array $context Optional context (recipient/webhook URL/etc.).
* @return bool
*/
abstract public static function actually_send( string $subject, string $body, array $context ): bool;
/**
* Hook subclass into `wpdo/health_alert_critical`.
*
* @return void
*/
public static function register(): void {
add_action( 'wpdo/health_alert_critical', array( static::class, 'maybe_send' ), 10, 1 );
}
/**
* Decide whether to send + throttle + dispatch.
*
* @param array $summary Summary array from Health_Cron::run().
* @return bool
*/
public static function maybe_send( array $summary ): bool {
if ( ! static::is_enabled() ) {
return false;
}
// Severity subscription check.
$severity_filter = static::severity_filter();
if ( 'critical_only' === $severity_filter && (int) ( $summary['critical_count'] ?? 0 ) === 0 ) {
return false;
}
// Channel-specific recipient/config sanity check.
$context = static::build_context();
if ( null === $context ) {
return false;
}
// Throttle.
$fingerprint = static::fingerprint( $summary );
if ( static::is_throttled( $fingerprint ) ) {
return false;
}
// Build + send.
$subject = static::build_subject( $summary );
$body = static::build_body( $summary );
$sent = static::actually_send( $subject, $body, $context );
if ( $sent ) {
static::mark_sent( $fingerprint );
if ( class_exists( 'TMDO_Logger' ) ) {
TMDO_Logger::info(
sprintf( '%s_alert_sent', static::channel_id() ),
array(
'fingerprint' => $fingerprint,
'critical' => (int) ( $summary['critical_count'] ?? 0 ),
)
);
}
}
return $sent;
}
// ─── settings accessorsoptional override)────────────────────────
/**
* Whether this notification channel is enabled.
*
* @return bool
*/
public static function is_enabled(): bool {
return '1' === (string) get_option( static::option_key( 'enabled' ), '0' );
}
/**
* Throttle window in hours (clamped 1..168).
*
* @return int
*/
public static function throttle_hours(): int {
$v = (int) get_option( static::option_key( 'throttle_hours' ), self::DEFAULT_THROTTLE_HOURS );
return max( 1, min( 168, $v ) );
}
/**
* Severity subscription filter.
*
* @return string 'critical_only' or 'critical_and_recommended'
*/
public static function severity_filter(): string {
$v = (string) get_option( static::option_key( 'severity' ), 'critical_only' );
return in_array( $v, array( 'critical_only', 'critical_and_recommended' ), true ) ? $v : 'critical_only';
}
/**
* Build channel-specific context (e.g. webhook URL or recipient address).
* Return null to skip send (e.g. invalid email or empty webhook).
*
* @return array|null
*/
public static function build_context(): ?array {
return array();
}
// ─── shared subject / body builders ───────────────────────────────
/**
* Build alert subject line.
*
* @param array $summary Summary array from Health_Cron::run().
* @return string
*/
public static function build_subject( array $summary ): string {
$site = (string) get_option( 'blogname', 'WordPress' );
$crit = (int) ( $summary['critical_count'] ?? 0 );
$first = '';
foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) {
if ( 'critical' === ( $t['status'] ?? '' ) ) {
$first = $slug;
break;
}
}
return sprintf( '[%s] WPDO 警告:%d 項 critical (%s)', $site, $crit, $first );
}
/**
* Generic plain-text body. Subclasses can override to add channel-specific
* formatting (Slack mrkdwn, Discord markdown, Telegram MarkdownV2, etc.).
*
* @param array $summary Summary.
* @return string
*/
public static function build_body( array $summary ): string {
$lines = array();
$lines[] = sprintf( '站台:%s', home_url( '/' ) );
$lines[] = sprintf( '檢查時間:%s UTC', (string) ( $summary['ran_at'] ?? '?' ) );
$lines[] = sprintf(
'結果:%d critical / %d recommended',
(int) ( $summary['critical_count'] ?? 0 ),
(int) ( $summary['recommended_count'] ?? 0 )
);
$lines[] = '';
$lines[] = '失敗的檢查:';
foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) {
if ( in_array( ( $t['status'] ?? '' ), array( 'critical', 'recommended' ), true ) ) {
$lines[] = sprintf(
' [%s] %s — %s',
strtoupper( (string) ( $t['status'] ?? '' ) ),
$slug,
(string) ( $t['description'] ?? '' )
);
}
}
$lines[] = '';
$lines[] = '查看詳情:' . admin_url( 'tools.php?page=wp-data-optimizer&tab=doctor' );
return implode( "\n", $lines );
}
// ─── private helpers ─────────────────────────────────────────────
/**
* Per-channel option key (e.g. wpdo_email_enabled, wpdo_slack_webhook).
*
* @param string $field 'enabled' / 'throttle_hours' / 'severity' / etc.
* @return string
*/
protected static function option_key( string $field ): string {
return sprintf( 'wpdo_%s_%s', static::channel_id(), $field );
}
/**
* Stable fingerprint from the alert content for deduplication.
*
* @param array $summary Summary array from Health_Cron::run().
* @return string md5 hash.
*/
protected static function fingerprint( array $summary ): string {
$relevant = array(
'critical_count' => (int) ( $summary['critical_count'] ?? 0 ),
'first_critical' => null,
);
foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) {
if ( 'critical' === ( $t['status'] ?? '' ) ) {
$relevant['first_critical'] = $slug;
break;
}
}
return md5( wp_json_encode( $relevant ) );
}
/**
* Check whether an alert with this fingerprint was recently sent.
*
* @param string $fingerprint Alert fingerprint (md5).
* @return bool
*/
protected static function is_throttled( string $fingerprint ): bool {
$key = sprintf( 'wpdo_%s_sent_%s', static::channel_id(), $fingerprint );
return false !== get_transient( $key );
}
/**
* Record that an alert was sent (sets throttle transient).
*
* @param string $fingerprint Alert fingerprint (md5).
* @return void
*/
protected static function mark_sent( string $fingerprint ): void {
$key = sprintf( 'wpdo_%s_sent_%s', static::channel_id(), $fingerprint );
set_transient( $key, time(), static::throttle_hours() * HOUR_IN_SECONDS );
}
}
@@ -0,0 +1,76 @@
<?php
/**
* TMDO_Discord_Notifier — Discord webhook channel (v2.5.0 M15).
*
* Settings: wpdo_discord_enabled, wpdo_discord_webhook,
* wpdo_discord_throttle_hours, wpdo_discord_severity.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Discord channel.
*/
class TMDO_Discord_Notifier extends TMDO_Abstract_Notifier {
/**
* Channel identifier used as option key prefix.
*
* @return string
*/
public static function channel_id(): string {
return 'discord';
}
/**
* Build Discord-specific context (webhook URL).
*
* @return array|null Null if webhook URL is missing or invalid.
*/
public static function build_context(): ?array {
$url = class_exists( 'TMDO_Crypto' )
? TMDO_Crypto::get_option( 'wpdo_discord_webhook' )
: (string) get_option( 'wpdo_discord_webhook', '' );
if ( '' === $url || strpos( $url, 'https://discord.com/api/webhooks/' ) !== 0 ) {
return null;
}
return array( 'webhook_url' => $url );
}
/**
* Send alert via Discord webhook.
*
* @param string $subject Alert subject / title.
* @param string $body Alert body text.
* @param array $context Channel context (webhook_url).
* @return bool True on HTTP 2xx response.
*/
public static function actually_send( string $subject, string $body, array $context ): bool {
$url = (string) ( $context['webhook_url'] ?? '' );
if ( '' === $url ) {
return false;
}
// Discord max content length = 2000 chars.
$content = "**{$subject}**\n```\n" . substr( $body, 0, 1800 ) . "\n```";
$payload = wp_json_encode( array( 'content' => $content ) );
$resp = wp_remote_post(
$url,
array(
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => $payload,
'timeout' => 5,
'blocking' => true,
)
);
if ( is_wp_error( $resp ) ) {
return false;
}
$code = (int) wp_remote_retrieve_response_code( $resp );
// Discord returns 204 on success.
return $code >= 200 && $code < 300;
}
}
@@ -0,0 +1,238 @@
<?php
/**
* TMDO_Email_Notifier — Threshold-based email alerts (v2.4.0 M10).
*
* Subscribes to action `wpdo/health_alert_critical` (fired by Health_Cron M6
* when critical_count > 0). Sends a plain-text email to the configured
* recipient(s) with a 24h throttle key (per-alert-fingerprint) so admins
* don't get spammed.
*
* Default OFF — admin must explicitly enable via Settings tab.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Email notifier — stateless static API.
*/
class TMDO_Email_Notifier {
public const OPT_ENABLED = 'wpdo_email_alerts_enabled';
public const OPT_RECIPIENT = 'wpdo_alert_email';
public const OPT_THROTTLE_HRS = 'wpdo_alert_throttle_hours';
/** Default throttle window. */
public const DEFAULT_THROTTLE_HOURS = 24;
/**
* Register subscriber.
*
* @return void
*/
public static function register(): void {
add_action( 'wpdo/health_alert_critical', array( __CLASS__, 'maybe_send' ), 10, 1 );
}
/**
* Decide + send.
*
* @param array $summary Health summary from Health_Cron::run().
* @return bool true on send, false on skip / fail.
*/
public static function maybe_send( array $summary ): bool {
if ( ! self::is_enabled() ) {
return false;
}
$recipient = self::recipient();
if ( ! is_email( $recipient ) ) {
return false;
}
$fingerprint = self::fingerprint( $summary );
if ( self::is_throttled( $fingerprint ) ) {
return false;
}
$subject = self::build_subject( $summary );
$body = self::build_body( $summary );
$sent = wp_mail( $recipient, $subject, $body );
if ( $sent ) {
self::mark_sent( $fingerprint );
if ( class_exists( 'TMDO_Logger' ) ) {
TMDO_Logger::info(
'email_alert_sent',
array(
'fingerprint' => $fingerprint,
'critical' => (int) ( $summary['critical_count'] ?? 0 ),
'recipient' => $recipient,
)
);
}
}
return (bool) $sent;
}
// ─── settings accessors ─────────────────────────────────────────────
/**
* Whether email alerts are enabled.
*
* @return bool
*/
public static function is_enabled(): bool {
return '1' === (string) get_option( self::OPT_ENABLED, '0' );
}
/**
* Alert recipient email address (falls back to admin_email).
*
* @return string
*/
public static function recipient(): string {
$v = (string) get_option( self::OPT_RECIPIENT, '' );
if ( '' === $v ) {
$v = (string) get_option( 'admin_email', '' );
}
return $v;
}
/**
* Throttle window in hours (clamped 1..168).
*
* @return int
*/
public static function throttle_hours(): int {
$v = (int) get_option( self::OPT_THROTTLE_HRS, self::DEFAULT_THROTTLE_HOURS );
return max( 1, min( 168, $v ) ); // Clamp 1h..1week.
}
// ─── private helpers ──────────────────────────────────────────────
/**
* Stable fingerprint from the alert content (so identical incidents
* dedupe within the throttle window).
*
* @param array $summary Summary array.
* @return string md5 hash.
*/
private static function fingerprint( array $summary ): string {
$relevant = array(
'critical_count' => (int) ( $summary['critical_count'] ?? 0 ),
'first_critical' => null,
);
foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) {
if ( 'critical' === ( $t['status'] ?? '' ) ) {
$relevant['first_critical'] = $slug;
break;
}
}
return md5( wp_json_encode( $relevant ) );
}
/**
* Check whether an alert with this fingerprint was recently sent.
*
* @param string $fingerprint Alert fingerprint (md5).
* @return bool
*/
private static function is_throttled( string $fingerprint ): bool {
$key = 'wpdo_alert_sent_' . $fingerprint;
return false !== get_transient( $key );
}
/**
* Record that an alert was sent (sets throttle transient).
*
* @param string $fingerprint Alert fingerprint (md5).
* @return void
*/
private static function mark_sent( string $fingerprint ): void {
$key = 'wpdo_alert_sent_' . $fingerprint;
set_transient( $key, time(), self::throttle_hours() * HOUR_IN_SECONDS );
}
/**
* Build subject. Site name + critical count + first slug.
*
* @param array $summary Summary array.
* @return string
*/
private static function build_subject( array $summary ): string {
$site = (string) get_option( 'blogname', 'WordPress' );
$crit = (int) ( $summary['critical_count'] ?? 0 );
$first = '';
foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) {
if ( 'critical' === ( $t['status'] ?? '' ) ) {
$first = $slug;
break;
}
}
return sprintf(
/* translators: 1: site name, 2: critical count, 3: first critical test slug */
__( '[%1$s] WPDO 警告:%2$d 項 critical (%3$s)', '2meet-data-optimizer' ),
$site,
$crit,
$first
);
}
/**
* Build plain-text body.
*
* @param array $summary Summary array.
* @return string
*/
private static function build_body( array $summary ): string {
$site_url = home_url( '/' );
$health_url = admin_url( 'site-health.php' );
$wpdo_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=doctor' );
$lines = array();
$lines[] = __( 'WP Data Optimizer 自動健康檢查發現 critical 警告。', '2meet-data-optimizer' );
$lines[] = '';
$lines[] = sprintf( '站台:%s', $site_url );
$lines[] = sprintf(
/* translators: %s: timestamp */
__( '檢查時間:%s UTC', '2meet-data-optimizer' ),
(string) ( $summary['ran_at'] ?? '?' )
);
$lines[] = sprintf(
/* translators: 1: critical count, 2: recommended count */
__( '結果:%1$d critical / %2$d recommended', '2meet-data-optimizer' ),
(int) ( $summary['critical_count'] ?? 0 ),
(int) ( $summary['recommended_count'] ?? 0 )
);
$lines[] = '';
$lines[] = __( '失敗的檢查:', '2meet-data-optimizer' );
foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) {
if ( in_array( ( $t['status'] ?? '' ), array( 'critical', 'recommended' ), true ) ) {
$lines[] = sprintf(
' [%s] %s — %s',
strtoupper( (string) $t['status'] ),
$slug,
(string) ( $t['description'] ?? '' )
);
}
}
$lines[] = '';
$lines[] = __( '建議行動:', '2meet-data-optimizer' );
$lines[] = ' · ' . sprintf(
/* translators: %s: WPDO Doctor admin URL */
__( '立即查看 WPDO Doctor%s', '2meet-data-optimizer' ),
$wpdo_url
);
$lines[] = ' · ' . sprintf(
/* translators: %s: WP Site Health admin URL */
__( '或 WP Site Health%s', '2meet-data-optimizer' ),
$health_url
);
$lines[] = '';
$lines[] = sprintf(
/* translators: %d: hours */
__( '註:相同警告在 %d 小時內不會重發;前往設定可調整 throttle / 收件人 / 關閉。', '2meet-data-optimizer' ),
self::throttle_hours()
);
return implode( "\n", $lines );
}
}
@@ -0,0 +1,78 @@
<?php
/**
* TMDO_Slack_Notifier — Slack incoming webhook channel (v2.5.0 M15).
*
* Settings: wpdo_slack_enabled, wpdo_slack_webhook, wpdo_slack_throttle_hours,
* wpdo_slack_severity. Webhook URL must start with https://hooks.slack.com/.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Slack channel.
*/
class TMDO_Slack_Notifier extends TMDO_Abstract_Notifier {
/**
* Channel identifier used as option key prefix.
*
* @return string
*/
public static function channel_id(): string {
return 'slack';
}
/**
* Build Slack-specific context (webhook URL).
*
* @return array|null Null if webhook URL is missing or invalid.
*/
public static function build_context(): ?array {
$url = class_exists( 'TMDO_Crypto' )
? TMDO_Crypto::get_option( 'wpdo_slack_webhook' )
: (string) get_option( 'wpdo_slack_webhook', '' );
if ( '' === $url || strpos( $url, 'https://hooks.slack.com/' ) !== 0 ) {
return null;
}
return array( 'webhook_url' => $url );
}
/**
* Send alert via Slack incoming webhook.
*
* @param string $subject Alert subject / title.
* @param string $body Alert body text.
* @param array $context Channel context (webhook_url).
* @return bool True on HTTP 2xx response.
*/
public static function actually_send( string $subject, string $body, array $context ): bool {
$url = (string) ( $context['webhook_url'] ?? '' );
if ( '' === $url ) {
return false;
}
// Slack mrkdwn — bold subject + plaintext body in code block for readability.
$payload = wp_json_encode(
array(
'text' => "*{$subject}*\n```\n{$body}\n```",
)
);
$resp = wp_remote_post(
$url,
array(
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => $payload,
'timeout' => 5,
'blocking' => true,
)
);
if ( is_wp_error( $resp ) ) {
return false;
}
$code = (int) wp_remote_retrieve_response_code( $resp );
return $code >= 200 && $code < 300;
}
}
@@ -0,0 +1,95 @@
<?php
/**
* TMDO_Telegram_Notifier — Telegram bot channel (v2.5.0 M15).
*
* Settings: wpdo_telegram_enabled, wpdo_telegram_bot_token,
* wpdo_telegram_chat_id, wpdo_telegram_throttle_hours, wpdo_telegram_severity.
*
* Bot token is sensitive: stored as-is in wp_options for now (admin-only).
* Future hardening: integrate TMDO_Crypto encryption (note in v2.5+).
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Telegram channel.
*/
class TMDO_Telegram_Notifier extends TMDO_Abstract_Notifier {
/**
* Channel identifier used as option key prefix.
*
* @return string
*/
public static function channel_id(): string {
return 'telegram';
}
/**
* Build Telegram-specific context (bot token + chat ID).
*
* @return array|null Null if token or chat ID is missing or invalid.
*/
public static function build_context(): ?array {
$token = class_exists( 'TMDO_Crypto' )
? TMDO_Crypto::get_option( 'wpdo_telegram_bot_token' )
: (string) get_option( 'wpdo_telegram_bot_token', '' );
$chat = (string) get_option( 'wpdo_telegram_chat_id', '' );
if ( '' === $token || '' === $chat ) {
return null;
}
// Bot tokens are formatted as 123456:ABC-DEF...; minimal validation.
if ( ! preg_match( '/^\d+:[A-Za-z0-9_\-]{20,}$/', $token ) ) {
return null;
}
return array(
'token' => $token,
'chat_id' => $chat,
);
}
/**
* Send alert via Telegram Bot API.
*
* @param string $subject Alert subject / title.
* @param string $body Alert body text.
* @param array $context Channel context (token, chat_id).
* @return bool True on HTTP 2xx response.
*/
public static function actually_send( string $subject, string $body, array $context ): bool {
$token = (string) ( $context['token'] ?? '' );
$chat = (string) ( $context['chat_id'] ?? '' );
if ( '' === $token || '' === $chat ) {
return false;
}
// Telegram MarkdownV2 has many escaped chars; use plain text mode for safety.
$text = "🚨 {$subject}\n\n{$body}";
// Telegram message limit = 4096 chars.
$text = substr( $text, 0, 4000 );
$url = sprintf( 'https://api.telegram.org/bot%s/sendMessage', rawurlencode( $token ) );
$resp = wp_remote_post(
$url,
array(
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => wp_json_encode(
array(
'chat_id' => $chat,
'text' => $text,
)
),
'timeout' => 5,
'blocking' => true,
)
);
if ( is_wp_error( $resp ) ) {
return false;
}
$code = (int) wp_remote_retrieve_response_code( $resp );
return $code >= 200 && $code < 300;
}
}