| 1 |
<?php |
| 2 |
/** |
| 3 |
* Email Template Helper |
| 4 |
* |
| 5 |
* Centralized HTML email builder for consistent styling |
| 6 |
* across all plugin notifications. |
| 7 |
* |
| 8 |
* @package Vigilante |
| 9 |
*/ |
| 10 |
|
| 11 |
// Prevent direct access |
| 12 |
if ( ! defined( 'ABSPATH' ) ) { |
| 13 |
exit; |
| 14 |
} |
| 15 |
|
| 16 |
/** |
| 17 |
* Class Vigilante_Email_Template |
| 18 |
* |
| 19 |
* Provides reusable HTML email components |
| 20 |
*/ |
| 21 |
class Vigilante_Email_Template { |
| 22 |
|
| 23 |
/** |
| 24 |
* Send an HTML email using the standard template |
| 25 |
* |
| 26 |
* Uses direct headers instead of wp_mail filters to prevent |
| 27 |
* filter contamination between consecutive wp_mail() calls. |
| 28 |
* |
| 29 |
* @param string|array $to Recipient(s). |
| 30 |
* @param string $subject Email subject. |
| 31 |
* @param string $title Header title. |
| 32 |
* @param string $body Body HTML (use helper methods to build). |
| 33 |
* @param bool $alert Whether this is an alert (red header accent). |
| 34 |
* @param string $from_name Optional custom From name. |
| 35 |
* @return bool |
| 36 |
*/ |
| 37 |
public static function send( $to, $subject, $title, $body, $alert = false, $from_name = '' ) { |
| 38 |
$html = self::wrap( $title, $body, $alert ); |
| 39 |
$headers = array( 'Content-Type: text/html; charset=UTF-8' ); |
| 40 |
|
| 41 |
// Set From header directly (avoids wp_mail_from_name filter pollution) |
| 42 |
if ( ! empty( $from_name ) ) { |
| 43 |
// Use WordPress default from email (same logic as wp_mail core) |
| 44 |
$sitename = wp_parse_url( network_home_url(), PHP_URL_HOST ); |
| 45 |
if ( 'www.' === substr( $sitename, 0, 4 ) ) { |
| 46 |
$sitename = substr( $sitename, 4 ); |
| 47 |
} |
| 48 |
$from_email = 'wordpress@' . $sitename; |
| 49 |
|
| 50 |
/** |
| 51 |
* Filters the from email for Vigilante emails. |
| 52 |
* |
| 53 |
* @param string $from_email Default from email. |
| 54 |
*/ |
| 55 |
$from_email = apply_filters( 'vigilante_email_from', $from_email ); |
| 56 |
|
| 57 |
$headers[] = 'From: ' . sanitize_text_field( $from_name ) . ' <' . sanitize_email( $from_email ) . '>'; |
| 58 |
} |
| 59 |
|
| 60 |
$result = wp_mail( $to, $subject, $html, $headers ); |
| 61 |
|
| 62 |
if ( ! $result && defined( 'WP_DEBUG' ) && WP_DEBUG ) { |
| 63 |
$recipient = is_array( $to ) ? implode( ', ', $to ) : $to; |
| 64 |
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging |
| 65 |
error_log( 'Vigilant email failed: to=' . $recipient . ' subject=' . $subject ); |
| 66 |
} |
| 67 |
|
| 68 |
return $result; |
| 69 |
} |
| 70 |
|
| 71 |
/** |
| 72 |
* Get centralized admin notification recipients |
| 73 |
* |
| 74 |
* Reads from the email settings section and builds the |
| 75 |
* recipient list. All admin notifications should use this |
| 76 |
* method instead of resolving recipients individually. |
| 77 |
* |
| 78 |
* @return array Array of valid email addresses. |
| 79 |
*/ |
| 80 |
public static function get_admin_recipients() { |
| 81 |
$options = get_option( 'vigilante_options', array() ); |
| 82 |
$email_settings = isset( $options['email'] ) ? $options['email'] : array(); |
| 83 |
|
| 84 |
$recipients = array(); |
| 85 |
|
| 86 |
// Include WordPress admin email if enabled (default: true) |
| 87 |
$send_to_admin = isset( $email_settings['send_to_admin_email'] ) |
| 88 |
? (bool) $email_settings['send_to_admin_email'] |
| 89 |
: true; |
| 90 |
|
| 91 |
if ( $send_to_admin ) { |
| 92 |
$admin_email = get_option( 'admin_email' ); |
| 93 |
if ( ! empty( $admin_email ) && is_email( $admin_email ) ) { |
| 94 |
$recipients[] = $admin_email; |
| 95 |
} |
| 96 |
} |
| 97 |
|
| 98 |
// Parse additional recipients (supports array and legacy string format) |
| 99 |
$additional_raw = isset( $email_settings['additional_recipients'] ) |
| 100 |
? $email_settings['additional_recipients'] |
| 101 |
: array(); |
| 102 |
|
| 103 |
// Normalize to array |
| 104 |
if ( is_string( $additional_raw ) ) { |
| 105 |
// Legacy string format or corrupted data: split by newlines, commas, semicolons |
| 106 |
$additional_list = preg_split( '/[\r\n,;]+/', trim( $additional_raw ) ); |
| 107 |
} else { |
| 108 |
$additional_list = (array) $additional_raw; |
| 109 |
} |
| 110 |
|
| 111 |
foreach ( $additional_list as $line ) { |
| 112 |
$email = sanitize_email( trim( $line ) ); |
| 113 |
if ( ! empty( $email ) && is_email( $email ) && ! in_array( $email, $recipients, true ) ) { |
| 114 |
$recipients[] = $email; |
| 115 |
} |
| 116 |
} |
| 117 |
|
| 118 |
// Fallback: only use admin email if settings were never configured |
| 119 |
// (send_to_admin_email key doesn't exist at all, not just false) |
| 120 |
if ( empty( $recipients ) && ! array_key_exists( 'send_to_admin_email', $email_settings ) ) { |
| 121 |
$fallback = get_option( 'admin_email' ); |
| 122 |
if ( ! empty( $fallback ) && is_email( $fallback ) ) { |
| 123 |
$recipients[] = $fallback; |
| 124 |
} |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Filters the admin notification recipients. |
| 129 |
* |
| 130 |
* Allows developers to modify the recipient list for |
| 131 |
* all administrative email notifications. |
| 132 |
* |
| 133 |
* @param array $recipients Array of email addresses. |
| 134 |
*/ |
| 135 |
return apply_filters( 'vigilante_notification_recipients', $recipients ); |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* Send a neutral test email to the configured recipients. |
| 140 |
* |
| 141 |
* Shared by every "Send test email" button (Notification settings, File |
| 142 |
* Integrity, Audit Alerts) so they all verify the same delivery path. |
| 143 |
* |
| 144 |
* @return bool True if the email was handed to wp_mail, false otherwise. |
| 145 |
*/ |
| 146 |
public static function send_test() { |
| 147 |
$recipients = self::get_admin_recipients(); |
| 148 |
if ( empty( $recipients ) ) { |
| 149 |
return false; |
| 150 |
} |
| 151 |
|
| 152 |
$subject = sprintf( |
| 153 |
/* translators: %s: site name */ |
| 154 |
__( '[Vigilant] Test email from %s', 'vigilante' ), |
| 155 |
wp_specialchars_decode( get_bloginfo( 'name' ) ) |
| 156 |
); |
| 157 |
|
| 158 |
$body = self::success_box( |
| 159 |
__( 'This is a test email from Vigilant. If you can read this, your notification recipients are set correctly and email delivery works.', 'vigilante' ) |
| 160 |
); |
| 161 |
$body .= self::data_table( |
| 162 |
array( |
| 163 |
__( 'Recipients', 'vigilante' ) => implode( ', ', $recipients ), |
| 164 |
) |
| 165 |
); |
| 166 |
|
| 167 |
return self::send( $recipients, $subject, __( 'Test email', 'vigilante' ), $body, false ); |
| 168 |
} |
| 169 |
|
| 170 |
/** |
| 171 |
* Wrap body content in the standard email shell |
| 172 |
* |
| 173 |
* @param string $title Header title. |
| 174 |
* @param string $body Body HTML. |
| 175 |
* @param bool $alert Red accent header. |
| 176 |
* @return string Full HTML email. |
| 177 |
*/ |
| 178 |
public static function wrap( $title, $body, $alert = false ) { |
| 179 |
$site_name = get_bloginfo( 'name' ); |
| 180 |
$header_bg = $alert ? '#d63638' : '#1d2327'; |
| 181 |
|
| 182 |
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>'; |
| 183 |
$html .= '<body style="margin:0;padding:0;background:#f0f0f1;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,Oxygen-Sans,Ubuntu,Cantarell,\'Helvetica Neue\',sans-serif;-webkit-text-size-adjust:100%;">'; |
| 184 |
$html .= '<table width="100%" cellpadding="0" cellspacing="0" style="background:#f0f0f1;padding:32px 0;">'; |
| 185 |
$html .= '<tr><td align="center">'; |
| 186 |
$html .= '<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:6px;overflow:hidden;border:1px solid #c3c4c7;">'; |
| 187 |
|
| 188 |
// Header |
| 189 |
$html .= '<tr><td style="background:' . esc_attr( $header_bg ) . ';padding:18px 28px;">'; |
| 190 |
$html .= '<table width="100%" cellpadding="0" cellspacing="0"><tr>'; |
| 191 |
$html .= '<td style="color:#ffffff;font-size:14px;font-weight:600;">' . esc_html__( 'Vigilant', 'vigilante' ) . '</td>'; |
| 192 |
$html .= '<td align="right" style="color:rgba(255,255,255,0.6);font-size:12px;">' . esc_html( $site_name ) . '</td>'; |
| 193 |
$html .= '</tr></table>'; |
| 194 |
$html .= '</td></tr>'; |
| 195 |
|
| 196 |
// Title |
| 197 |
if ( ! empty( $title ) ) { |
| 198 |
$html .= '<tr><td style="padding:24px 28px 0;">'; |
| 199 |
$html .= '<h1 style="margin:0;font-size:18px;font-weight:600;color:#1d2327;line-height:1.4;">' . esc_html( $title ) . '</h1>'; |
| 200 |
$html .= '</td></tr>'; |
| 201 |
} |
| 202 |
|
| 203 |
// Body |
| 204 |
$html .= '<tr><td style="padding:16px 28px 28px;">'; |
| 205 |
$html .= $body; |
| 206 |
$html .= '</td></tr>'; |
| 207 |
|
| 208 |
// Footer |
| 209 |
$html .= '<tr><td style="padding:16px 28px;background:#f6f7f7;border-top:1px solid #dcdcde;">'; |
| 210 |
$html .= '<p style="color:#787c82;font-size:11px;margin:0;text-align:center;line-height:1.5;">'; |
| 211 |
$html .= esc_html( $site_name ) . ' — ' . esc_url( home_url() ); |
| 212 |
$html .= '</p></td></tr>'; |
| 213 |
|
| 214 |
$html .= '</table></td></tr></table></body></html>'; |
| 215 |
|
| 216 |
return $html; |
| 217 |
} |
| 218 |
|
| 219 |
// ========================================================================= |
| 220 |
// Body content helpers |
| 221 |
// ========================================================================= |
| 222 |
|
| 223 |
/** |
| 224 |
* Paragraph |
| 225 |
* |
| 226 |
* @param string $text Text content (will be escaped). |
| 227 |
* @param string $color Text color. |
| 228 |
* @param bool $bold Whether to bold. |
| 229 |
* @return string |
| 230 |
*/ |
| 231 |
public static function p( $text, $color = '#1d2327', $bold = false ) { |
| 232 |
$weight = $bold ? 'font-weight:600;' : ''; |
| 233 |
return '<p style="color:' . esc_attr( $color ) . ';font-size:14px;line-height:1.6;margin:0 0 14px;' . $weight . '">' . esc_html( $text ) . '</p>'; |
| 234 |
} |
| 235 |
|
| 236 |
/** |
| 237 |
* Raw HTML paragraph (for content with links etc.) |
| 238 |
* |
| 239 |
* @param string $html HTML content (caller must escape). |
| 240 |
* @param string $color Text color. |
| 241 |
* @return string |
| 242 |
*/ |
| 243 |
public static function p_raw( $html, $color = '#1d2327' ) { |
| 244 |
return '<p style="color:' . esc_attr( $color ) . ';font-size:14px;line-height:1.6;margin:0 0 14px;">' . $html . '</p>'; |
| 245 |
} |
| 246 |
|
| 247 |
/** |
| 248 |
* Small text paragraph |
| 249 |
* |
| 250 |
* @param string $text Text content. |
| 251 |
* @return string |
| 252 |
*/ |
| 253 |
public static function small( $text ) { |
| 254 |
return '<p style="color:#787c82;font-size:12px;line-height:1.5;margin:0 0 14px;">' . esc_html( $text ) . '</p>'; |
| 255 |
} |
| 256 |
|
| 257 |
/** |
| 258 |
* Data table (key-value pairs) |
| 259 |
* |
| 260 |
* @param array $rows Associative array of label => value. |
| 261 |
* @return string |
| 262 |
*/ |
| 263 |
public static function data_table( $rows ) { |
| 264 |
$html = '<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin:0 0 16px;font-size:13px;">'; |
| 265 |
foreach ( $rows as $label => $value ) { |
| 266 |
$html .= '<tr>'; |
| 267 |
$html .= '<td style="padding:6px 12px 6px 0;color:#787c82;white-space:nowrap;vertical-align:top;">' . esc_html( $label ) . '</td>'; |
| 268 |
$html .= '<td style="padding:6px 0;color:#1d2327;word-break:break-all;">' . esc_html( $value ) . '</td>'; |
| 269 |
$html .= '</tr>'; |
| 270 |
} |
| 271 |
$html .= '</table>'; |
| 272 |
return $html; |
| 273 |
} |
| 274 |
|
| 275 |
/** |
| 276 |
* Highlighted info box (blue left border) |
| 277 |
* |
| 278 |
* @param string $text Text content. |
| 279 |
* @return string |
| 280 |
*/ |
| 281 |
public static function info_box( $text ) { |
| 282 |
return '<div style="background:#f0f6fc;border-left:4px solid #2271b1;border-radius:0 4px 4px 0;padding:12px 16px;margin:0 0 16px;">' |
| 283 |
. '<p style="color:#1d2327;font-size:13px;line-height:1.5;margin:0;">' . esc_html( $text ) . '</p></div>'; |
| 284 |
} |
| 285 |
|
| 286 |
/** |
| 287 |
* Warning box (orange left border) |
| 288 |
* |
| 289 |
* @param string $text Text content. |
| 290 |
* @return string |
| 291 |
*/ |
| 292 |
public static function warning_box( $text ) { |
| 293 |
return '<div style="background:#fdf8e8;border-left:4px solid #dba617;border-radius:0 4px 4px 0;padding:12px 16px;margin:0 0 16px;">' |
| 294 |
. '<p style="color:#1d2327;font-size:13px;line-height:1.5;margin:0;">' . esc_html( $text ) . '</p></div>'; |
| 295 |
} |
| 296 |
|
| 297 |
/** |
| 298 |
* Alert/error box (red left border) |
| 299 |
* |
| 300 |
* @param string $text Text content. |
| 301 |
* @return string |
| 302 |
*/ |
| 303 |
public static function alert_box( $text ) { |
| 304 |
return '<div style="background:#fcf0f1;border-left:4px solid #d63638;border-radius:0 4px 4px 0;padding:12px 16px;margin:0 0 16px;">' |
| 305 |
. '<p style="color:#1d2327;font-size:13px;line-height:1.5;margin:0;font-weight:500;">' . esc_html( $text ) . '</p></div>'; |
| 306 |
} |
| 307 |
|
| 308 |
/** |
| 309 |
* Success box (green left border) |
| 310 |
* |
| 311 |
* @param string $text Text content. |
| 312 |
* @return string |
| 313 |
*/ |
| 314 |
public static function success_box( $text ) { |
| 315 |
return '<div style="background:#edfaef;border-left:4px solid #00a32a;border-radius:0 4px 4px 0;padding:12px 16px;margin:0 0 16px;">' |
| 316 |
. '<p style="color:#1d2327;font-size:13px;line-height:1.5;margin:0;">' . esc_html( $text ) . '</p></div>'; |
| 317 |
} |
| 318 |
|
| 319 |
/** |
| 320 |
* Highlighted code/value display (e.g., verification codes) |
| 321 |
* |
| 322 |
* @param string $code Code or value to display. |
| 323 |
* @param string $label Optional label above the code. |
| 324 |
* @return string |
| 325 |
*/ |
| 326 |
public static function code_box( $code, $label = '' ) { |
| 327 |
$html = '<div style="text-align:center;margin:0 0 16px;">'; |
| 328 |
if ( ! empty( $label ) ) { |
| 329 |
$html .= '<p style="color:#787c82;font-size:13px;margin:0 0 8px;">' . esc_html( $label ) . '</p>'; |
| 330 |
} |
| 331 |
$html .= '<div style="background:#f0f6fc;border:2px solid #2271b1;border-radius:8px;padding:14px 24px;display:inline-block;">'; |
| 332 |
$html .= '<span style="font-size:28px;font-weight:700;letter-spacing:6px;font-family:Consolas,Monaco,monospace;color:#1d2327;">'; |
| 333 |
$html .= esc_html( $code ); |
| 334 |
$html .= '</span></div></div>'; |
| 335 |
return $html; |
| 336 |
} |
| 337 |
|
| 338 |
/** |
| 339 |
* URL display box (for login URLs, links, etc.) |
| 340 |
* |
| 341 |
* Unlike code_box, uses normal font size and word-break |
| 342 |
* to handle long URLs without overflowing. |
| 343 |
* |
| 344 |
* @param string $url URL to display. |
| 345 |
* @param string $label Optional label above the URL. |
| 346 |
* @return string |
| 347 |
*/ |
| 348 |
public static function url_box( $url, $label = '' ) { |
| 349 |
$html = '<div style="text-align:center;margin:0 0 16px;">'; |
| 350 |
if ( ! empty( $label ) ) { |
| 351 |
$html .= '<p style="color:#787c82;font-size:13px;margin:0 0 8px;">' . esc_html( $label ) . '</p>'; |
| 352 |
} |
| 353 |
$html .= '<div style="background:#f0f6fc;border:2px solid #2271b1;border-radius:8px;padding:14px 20px;">'; |
| 354 |
$html .= '<a href="' . esc_url( $url ) . '" style="font-size:16px;font-weight:600;font-family:Consolas,Monaco,monospace;color:#2271b1;text-decoration:none;word-break:break-all;">'; |
| 355 |
$html .= esc_html( $url ); |
| 356 |
$html .= '</a></div></div>'; |
| 357 |
return $html; |
| 358 |
} |
| 359 |
|
| 360 |
/** |
| 361 |
* CTA button |
| 362 |
* |
| 363 |
* @param string $url Button URL. |
| 364 |
* @param string $text Button text. |
| 365 |
* @param string $bg Background color. |
| 366 |
* @return string |
| 367 |
*/ |
| 368 |
public static function button( $url, $text, $bg = '#2271b1' ) { |
| 369 |
return '<p style="text-align:center;margin:20px 0 16px;">' |
| 370 |
. '<a href="' . esc_url( $url ) . '" style="display:inline-block;background:' . esc_attr( $bg ) . ';color:#ffffff;text-decoration:none;padding:10px 28px;border-radius:4px;font-size:14px;font-weight:600;">' |
| 371 |
. esc_html( $text ) . '</a></p>'; |
| 372 |
} |
| 373 |
|
| 374 |
/** |
| 375 |
* Simple unordered list |
| 376 |
* |
| 377 |
* @param array $items List items (HTML allowed, caller must escape). |
| 378 |
* @return string |
| 379 |
*/ |
| 380 |
public static function ul( $items ) { |
| 381 |
$html = '<ul style="color:#1d2327;font-size:13px;line-height:1.6;margin:0 0 16px;padding-left:20px;">'; |
| 382 |
foreach ( $items as $item ) { |
| 383 |
$html .= '<li style="margin-bottom:4px;">' . wp_kses( $item, array( 'a' => array( 'href' => array() ), 'strong' => array(), 'code' => array() ) ) . '</li>'; |
| 384 |
} |
| 385 |
$html .= '</ul>'; |
| 386 |
return $html; |
| 387 |
} |
| 388 |
|
| 389 |
/** |
| 390 |
* Horizontal divider |
| 391 |
* |
| 392 |
* @return string |
| 393 |
*/ |
| 394 |
public static function hr() { |
| 395 |
return '<hr style="border:none;border-top:1px solid #dcdcde;margin:20px 0;">'; |
| 396 |
} |
| 397 |
} |