Files
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 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
2026-07-31 06:13:33 +08:00

241 lines
6.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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
*/
declare(strict_types=1);
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 );
}
}