$fingerprint, 'critical' => (int) ( $summary['critical_count'] ?? 0 ), ) ); } } return $sent; } // ─── settings accessors(optional 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 ); } }