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:
@@ -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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user