This commit introduces a complete announcement management system for HVAC trainers with enterprise-grade security, performance optimization, and email notifications. ## Core Features - Custom post type for trainer announcements with categories and tags - Role-based permissions (master trainers can create/edit, all trainers can read) - AJAX-powered admin interface with real-time updates - Modal popup viewing for announcements on frontend - Automated email notifications when announcements are published - Google Drive integration for training resources ## Security Enhancements - Fixed critical capability mapping bug preventing proper permission checks - Added content disclosure protection for draft/private announcements - Fixed XSS vulnerabilities with proper output escaping and sanitization - Implemented permission checks on all AJAX endpoints - Added rate limiting to prevent abuse (30 requests/minute) - Email validation before sending notifications ## Performance Optimizations - Implemented intelligent caching for user queries (5-minute TTL) - Added cache versioning for announcement lists (2-minute TTL) - Automatic cache invalidation on content changes - Batch email processing to prevent timeouts (50 emails per batch) - Retry mechanism for failed email sends (max 3 attempts) ## Technical Implementation - Singleton pattern for all manager classes - WordPress coding standards compliance - Proper nonce verification on all AJAX requests - Comprehensive error handling and logging - Mobile-responsive UI with smooth animations - WCAG accessibility compliance ## Components Added - 6 PHP classes for modular architecture - 2 page templates (master announcements, trainer resources) - Admin and frontend JavaScript with jQuery integration - Comprehensive CSS for both admin and frontend - Email notification system with HTML templates - Complete documentation and implementation plans This system provides a secure, scalable foundation for trainer communications while following WordPress best practices and maintaining high code quality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
425 lines
No EOL
14 KiB
PHP
425 lines
No EOL
14 KiB
PHP
<?php
|
|
/**
|
|
* HVAC Announcements Email Handler
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class HVAC_Announcements_Email
|
|
*
|
|
* Handles email notifications for announcements
|
|
*/
|
|
class HVAC_Announcements_Email {
|
|
|
|
/**
|
|
* Instance of this class
|
|
*
|
|
* @var HVAC_Announcements_Email
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Batch size for email sending
|
|
*
|
|
* @var int
|
|
*/
|
|
private $batch_size;
|
|
|
|
/**
|
|
* Get instance of this class
|
|
*
|
|
* @return HVAC_Announcements_Email
|
|
*/
|
|
public static function get_instance() {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
// Make batch size filterable
|
|
$this->batch_size = apply_filters('hvac_announcement_email_batch_size', 50);
|
|
$this->init_hooks();
|
|
}
|
|
|
|
/**
|
|
* Initialize hooks
|
|
*/
|
|
private function init_hooks() {
|
|
// Watch for status changes
|
|
add_action('transition_post_status', array($this, 'handle_status_transition'), 10, 3);
|
|
|
|
// Cron action for batch sending
|
|
add_action('hvac_send_announcement_email_batch', array($this, 'send_email_batch'), 10, 2);
|
|
}
|
|
|
|
/**
|
|
* Handle post status transitions
|
|
*
|
|
* @param string $new_status New post status
|
|
* @param string $old_status Old post status
|
|
* @param WP_Post $post Post object
|
|
*/
|
|
public function handle_status_transition($new_status, $old_status, $post) {
|
|
// Only handle our post type
|
|
if ($post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
|
|
return;
|
|
}
|
|
|
|
// Only send email when publishing for the first time
|
|
if ($new_status === 'publish' && $old_status !== 'publish') {
|
|
// Check if email was already sent for this announcement
|
|
$email_sent = get_post_meta($post->ID, '_hvac_announcement_email_sent', true);
|
|
|
|
if (!$email_sent) {
|
|
$this->queue_announcement_emails($post->ID);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queue announcement emails for batch processing
|
|
*
|
|
* @param int $post_id Announcement post ID
|
|
*/
|
|
private function queue_announcement_emails($post_id) {
|
|
// Get active trainers
|
|
$trainers = HVAC_Announcements_Permissions::get_active_trainers();
|
|
|
|
if (empty($trainers)) {
|
|
return;
|
|
}
|
|
|
|
// Store recipient IDs
|
|
$recipient_ids = array();
|
|
foreach ($trainers as $trainer) {
|
|
if (HVAC_Announcements_Permissions::user_should_receive_emails($trainer->ID)) {
|
|
$recipient_ids[] = $trainer->ID;
|
|
}
|
|
}
|
|
|
|
// Save recipient list
|
|
update_post_meta($post_id, '_hvac_announcement_email_recipients', $recipient_ids);
|
|
update_post_meta($post_id, '_hvac_announcement_email_send_date', current_time('mysql'));
|
|
|
|
// Process in batches
|
|
$batches = array_chunk($recipient_ids, $this->batch_size);
|
|
|
|
foreach ($batches as $index => $batch) {
|
|
// Schedule immediate sending (can be delayed using wp_schedule_single_event)
|
|
wp_schedule_single_event(
|
|
time() + ($index * 10), // Stagger by 10 seconds per batch
|
|
'hvac_send_announcement_email_batch',
|
|
array($post_id, $batch)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send email batch
|
|
*
|
|
* @param int $post_id Announcement post ID
|
|
* @param array $recipient_ids Array of user IDs
|
|
*/
|
|
public function send_email_batch($post_id, $recipient_ids) {
|
|
$post = get_post($post_id);
|
|
|
|
if (!$post || $post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
|
|
return;
|
|
}
|
|
|
|
// Get email content
|
|
$subject = $this->get_email_subject($post);
|
|
$body = $this->get_email_body($post);
|
|
$headers = $this->get_email_headers();
|
|
|
|
$successful_sends = array();
|
|
$failed_sends = array();
|
|
|
|
foreach ($recipient_ids as $user_id) {
|
|
$user = get_user_by('id', $user_id);
|
|
|
|
if (!$user || !$user->user_email) {
|
|
$failed_sends[] = $user_id;
|
|
continue;
|
|
}
|
|
|
|
// Validate email address
|
|
if (!is_email($user->user_email)) {
|
|
$failed_sends[] = $user_id;
|
|
|
|
// Log invalid email
|
|
$this->log_email_send($post_id, $user_id, 'invalid_email');
|
|
continue;
|
|
}
|
|
|
|
// Send email
|
|
$sent = wp_mail($user->user_email, $subject, $body, $headers);
|
|
|
|
if ($sent) {
|
|
$successful_sends[] = $user_id;
|
|
|
|
// Log successful send
|
|
$this->log_email_send($post_id, $user_id, 'success');
|
|
} else {
|
|
$failed_sends[] = $user_id;
|
|
|
|
// Log failed send
|
|
$this->log_email_send($post_id, $user_id, 'failed');
|
|
|
|
// Schedule retry (max 3 attempts)
|
|
$this->maybe_schedule_retry($post_id, $user_id);
|
|
}
|
|
}
|
|
|
|
// Mark as sent if all batches are complete
|
|
$all_sent = get_post_meta($post_id, '_hvac_announcement_email_sent', true);
|
|
if (!$all_sent && empty($failed_sends)) {
|
|
update_post_meta($post_id, '_hvac_announcement_email_sent', true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get email subject
|
|
*
|
|
* @param WP_Post $post Announcement post
|
|
* @return string
|
|
*/
|
|
private function get_email_subject($post) {
|
|
$subject = sprintf(
|
|
'Upskill HVAC Trainer Announcement: %s',
|
|
$post->post_title
|
|
);
|
|
|
|
return apply_filters('hvac_announcement_email_subject', $subject, $post);
|
|
}
|
|
|
|
/**
|
|
* Get email body
|
|
*
|
|
* @param WP_Post $post Announcement post
|
|
* @return string
|
|
*/
|
|
private function get_email_body($post) {
|
|
// Get template
|
|
$template_path = plugin_dir_path(dirname(__FILE__)) . 'templates/email/announcement-notification.php';
|
|
|
|
if (!file_exists($template_path)) {
|
|
// Fallback to simple HTML
|
|
return $this->get_fallback_email_body($post);
|
|
}
|
|
|
|
// Start output buffering
|
|
ob_start();
|
|
|
|
// Set up template variables
|
|
$announcement_title = $post->post_title;
|
|
$announcement_content = apply_filters('the_content', $post->post_content);
|
|
$publish_date = get_the_date('F j, Y', $post);
|
|
$site_url = home_url();
|
|
$resources_url = home_url('/trainer/resources/');
|
|
|
|
// Get featured image
|
|
$featured_image = '';
|
|
if (has_post_thumbnail($post->ID)) {
|
|
$featured_image = get_the_post_thumbnail($post->ID, 'large', array(
|
|
'style' => 'max-width: 100%; height: auto; display: block; margin: 20px 0;'
|
|
));
|
|
}
|
|
|
|
// Get categories and tags
|
|
$categories = wp_get_post_terms($post->ID, HVAC_Announcements_CPT::get_category_taxonomy(), array('fields' => 'names'));
|
|
$tags = wp_get_post_terms($post->ID, HVAC_Announcements_CPT::get_tag_taxonomy(), array('fields' => 'names'));
|
|
|
|
$categories_tags = '';
|
|
if (!empty($categories) || !empty($tags)) {
|
|
$categories_tags = '<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0;">';
|
|
|
|
if (!empty($categories)) {
|
|
$categories_tags .= '<p><strong>Categories:</strong> ' . implode(', ', $categories) . '</p>';
|
|
}
|
|
|
|
if (!empty($tags)) {
|
|
$categories_tags .= '<p><strong>Tags:</strong> ' . implode(', ', $tags) . '</p>';
|
|
}
|
|
|
|
$categories_tags .= '</div>';
|
|
}
|
|
|
|
// Include template
|
|
include $template_path;
|
|
|
|
// Get output
|
|
$body = ob_get_clean();
|
|
|
|
return apply_filters('hvac_announcement_email_body', $body, $post);
|
|
}
|
|
|
|
/**
|
|
* Get fallback email body if template doesn't exist
|
|
*
|
|
* @param WP_Post $post Announcement post
|
|
* @return string
|
|
*/
|
|
private function get_fallback_email_body($post) {
|
|
$html = '<!DOCTYPE html>';
|
|
$html .= '<html><head><meta charset="UTF-8"></head><body>';
|
|
$html .= '<div style="max-width: 600px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif;">';
|
|
|
|
// Header
|
|
$html .= '<div style="background: #003366; color: white; padding: 20px; text-align: center;">';
|
|
$html .= '<h1>Upskill HVAC Trainer Announcement</h1>';
|
|
$html .= '</div>';
|
|
|
|
// Content
|
|
$html .= '<div style="padding: 30px; background: #ffffff; border: 1px solid #e0e0e0;">';
|
|
$html .= '<h2>' . esc_html($post->post_title) . '</h2>';
|
|
$html .= '<p style="color: #666; font-size: 14px;">Posted on ' . get_the_date('F j, Y', $post) . '</p>';
|
|
|
|
// Featured image
|
|
if (has_post_thumbnail($post->ID)) {
|
|
$html .= get_the_post_thumbnail($post->ID, 'large', array(
|
|
'style' => 'max-width: 100%; height: auto; display: block; margin: 20px 0;'
|
|
));
|
|
}
|
|
|
|
// Content
|
|
$html .= '<div style="margin-top: 20px;">';
|
|
$html .= apply_filters('the_content', $post->post_content);
|
|
$html .= '</div>';
|
|
$html .= '</div>';
|
|
|
|
// Footer
|
|
$html .= '<div style="padding: 20px; background: #f5f5f5; text-align: center; font-size: 12px; color: #666;">';
|
|
$html .= '<p>You received this email because you are registered as an HVAC Trainer.</p>';
|
|
$html .= '<p><a href="' . home_url('/trainer/resources/') . '">View all announcements</a></p>';
|
|
$html .= '</div>';
|
|
|
|
$html .= '</div></body></html>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Get email headers
|
|
*
|
|
* @return array
|
|
*/
|
|
private function get_email_headers() {
|
|
$admin_email = get_option('admin_email');
|
|
|
|
// Validate admin email, use fallback if invalid
|
|
if (!is_email($admin_email)) {
|
|
// Try to get first administrator's email as fallback
|
|
$admins = get_users(array('role' => 'administrator', 'number' => 1));
|
|
if (!empty($admins) && is_email($admins[0]->user_email)) {
|
|
$admin_email = $admins[0]->user_email;
|
|
} else {
|
|
// Use a generic noreply address as last resort
|
|
$domain = parse_url(home_url(), PHP_URL_HOST);
|
|
$admin_email = 'noreply@' . $domain;
|
|
}
|
|
}
|
|
|
|
$headers = array(
|
|
'Content-Type: text/html; charset=UTF-8',
|
|
'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>',
|
|
);
|
|
|
|
return apply_filters('hvac_announcement_email_headers', $headers);
|
|
}
|
|
|
|
/**
|
|
* Log email send attempt
|
|
*
|
|
* @param int $post_id Announcement post ID
|
|
* @param int $user_id User ID
|
|
* @param string $status Status (success/failed)
|
|
*/
|
|
private function log_email_send($post_id, $user_id, $status) {
|
|
$log = get_post_meta($post_id, '_hvac_announcement_email_log', true);
|
|
|
|
if (!is_array($log)) {
|
|
$log = array();
|
|
}
|
|
|
|
$log[] = array(
|
|
'user_id' => $user_id,
|
|
'status' => $status,
|
|
'timestamp' => current_time('mysql'),
|
|
);
|
|
|
|
update_post_meta($post_id, '_hvac_announcement_email_log', $log);
|
|
}
|
|
|
|
/**
|
|
* Maybe schedule retry for failed email
|
|
*
|
|
* @param int $post_id Announcement post ID
|
|
* @param int $user_id User ID
|
|
*/
|
|
private function maybe_schedule_retry($post_id, $user_id) {
|
|
$retry_count = get_user_meta($user_id, '_hvac_announcement_email_retry_' . $post_id, true);
|
|
|
|
if (!$retry_count) {
|
|
$retry_count = 0;
|
|
}
|
|
|
|
// Max 3 retries
|
|
if ($retry_count < 3) {
|
|
$retry_count++;
|
|
update_user_meta($user_id, '_hvac_announcement_email_retry_' . $post_id, $retry_count);
|
|
|
|
// Schedule retry in 5 minutes
|
|
wp_schedule_single_event(
|
|
time() + (5 * 60),
|
|
'hvac_send_announcement_email_batch',
|
|
array($post_id, array($user_id))
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get email send status for an announcement
|
|
*
|
|
* @param int $post_id Announcement post ID
|
|
* @return array
|
|
*/
|
|
public static function get_email_status($post_id) {
|
|
$email_sent = get_post_meta($post_id, '_hvac_announcement_email_sent', true);
|
|
$recipients = get_post_meta($post_id, '_hvac_announcement_email_recipients', true);
|
|
$send_date = get_post_meta($post_id, '_hvac_announcement_email_send_date', true);
|
|
$log = get_post_meta($post_id, '_hvac_announcement_email_log', true);
|
|
|
|
$successful = 0;
|
|
$failed = 0;
|
|
|
|
if (is_array($log)) {
|
|
foreach ($log as $entry) {
|
|
if ($entry['status'] === 'success') {
|
|
$successful++;
|
|
} else {
|
|
$failed++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'sent' => $email_sent,
|
|
'total_recipients' => is_array($recipients) ? count($recipients) : 0,
|
|
'successful' => $successful,
|
|
'failed' => $failed,
|
|
'send_date' => $send_date,
|
|
);
|
|
}
|
|
} |