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>
586 lines
No EOL
20 KiB
PHP
586 lines
No EOL
20 KiB
PHP
<?php
|
|
/**
|
|
* HVAC Announcements AJAX Handler
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class HVAC_Announcements_Ajax
|
|
*
|
|
* Handles AJAX requests for announcement CRUD operations
|
|
*/
|
|
class HVAC_Announcements_Ajax {
|
|
|
|
/**
|
|
* Instance of this class
|
|
*
|
|
* @var HVAC_Announcements_Ajax
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Get instance of this class
|
|
*
|
|
* @return HVAC_Announcements_Ajax
|
|
*/
|
|
public static function get_instance() {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
$this->init_hooks();
|
|
}
|
|
|
|
/**
|
|
* Initialize AJAX hooks
|
|
*/
|
|
private function init_hooks() {
|
|
// AJAX actions for logged-in users
|
|
add_action('wp_ajax_hvac_get_announcements', array($this, 'get_announcements'));
|
|
add_action('wp_ajax_hvac_get_announcement', array($this, 'get_announcement'));
|
|
add_action('wp_ajax_hvac_create_announcement', array($this, 'create_announcement'));
|
|
add_action('wp_ajax_hvac_update_announcement', array($this, 'update_announcement'));
|
|
add_action('wp_ajax_hvac_delete_announcement', array($this, 'delete_announcement'));
|
|
add_action('wp_ajax_hvac_get_announcement_categories', array($this, 'get_categories'));
|
|
add_action('wp_ajax_hvac_get_announcement_tags', array($this, 'get_tags'));
|
|
add_action('wp_ajax_hvac_view_announcement', array($this, 'view_announcement'));
|
|
|
|
// Clear cache when announcements are modified
|
|
add_action('save_post_' . HVAC_Announcements_CPT::get_post_type(), array($this, 'clear_announcements_cache'));
|
|
add_action('delete_post', array($this, 'maybe_clear_announcements_cache'));
|
|
add_action('transition_post_status', array($this, 'clear_announcements_cache_on_status_change'), 10, 3);
|
|
}
|
|
|
|
/**
|
|
* Clear all announcements caches
|
|
*/
|
|
public function clear_announcements_cache() {
|
|
// Clear all cached announcement lists by flushing the group
|
|
// Since we can't iterate cache keys in standard WP, we'll use a version key approach
|
|
$version = wp_cache_get('announcements_cache_version', 'hvac_announcements');
|
|
if (false === $version) {
|
|
$version = 1;
|
|
} else {
|
|
$version++;
|
|
}
|
|
wp_cache_set('announcements_cache_version', $version, 'hvac_announcements', 0);
|
|
}
|
|
|
|
/**
|
|
* Maybe clear announcements cache when a post is deleted
|
|
*
|
|
* @param int $post_id Post ID
|
|
*/
|
|
public function maybe_clear_announcements_cache($post_id) {
|
|
$post = get_post($post_id);
|
|
if ($post && $post->post_type === HVAC_Announcements_CPT::get_post_type()) {
|
|
$this->clear_announcements_cache();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear cache when announcement status changes
|
|
*
|
|
* @param string $new_status New status
|
|
* @param string $old_status Old status
|
|
* @param WP_Post $post Post object
|
|
*/
|
|
public function clear_announcements_cache_on_status_change($new_status, $old_status, $post) {
|
|
if ($post->post_type === HVAC_Announcements_CPT::get_post_type()) {
|
|
$this->clear_announcements_cache();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check rate limiting for AJAX requests
|
|
*
|
|
* @return bool True if within limits, sends error and exits if exceeded
|
|
*/
|
|
private function check_rate_limit() {
|
|
$user_id = get_current_user_id();
|
|
$transient_key = 'hvac_ajax_rate_' . $user_id;
|
|
$request_count = get_transient($transient_key);
|
|
|
|
if (false === $request_count) {
|
|
$request_count = 0;
|
|
}
|
|
|
|
// Allow 30 requests per minute
|
|
if ($request_count >= 30) {
|
|
wp_send_json_error('Rate limit exceeded. Please wait a moment and try again.');
|
|
exit;
|
|
}
|
|
|
|
// Increment and set transient for 60 seconds
|
|
set_transient($transient_key, $request_count + 1, 60);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get paginated announcements
|
|
*/
|
|
public function get_announcements() {
|
|
// Check rate limiting
|
|
$this->check_rate_limit();
|
|
|
|
// Verify nonce
|
|
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Invalid security token');
|
|
}
|
|
|
|
// Check permissions
|
|
if (!HVAC_Announcements_Permissions::current_user_can_read()) {
|
|
wp_send_json_error('Insufficient permissions');
|
|
}
|
|
|
|
// Get parameters
|
|
$page = isset($_POST['page']) ? intval($_POST['page']) : 1;
|
|
$per_page = isset($_POST['per_page']) ? intval($_POST['per_page']) : 20;
|
|
$status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'any';
|
|
$search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
|
|
|
|
// Build cache key based on parameters, user role, and cache version
|
|
$is_master = HVAC_Announcements_Permissions::is_master_trainer();
|
|
$cache_version = wp_cache_get('announcements_cache_version', 'hvac_announcements');
|
|
if (false === $cache_version) {
|
|
$cache_version = 1;
|
|
}
|
|
|
|
$cache_key = 'hvac_announcements_v' . $cache_version . '_' . md5(serialize(array(
|
|
'page' => $page,
|
|
'per_page' => $per_page,
|
|
'status' => $status,
|
|
'search' => $search,
|
|
'is_master' => $is_master
|
|
)));
|
|
|
|
// Try to get from cache
|
|
$cached_response = wp_cache_get($cache_key, 'hvac_announcements');
|
|
if ($cached_response !== false && empty($search)) { // Don't cache search results
|
|
wp_send_json_success($cached_response);
|
|
return;
|
|
}
|
|
|
|
// Build query args
|
|
$args = array(
|
|
'post_type' => HVAC_Announcements_CPT::get_post_type(),
|
|
'posts_per_page' => $per_page,
|
|
'paged' => $page,
|
|
'orderby' => 'date',
|
|
'order' => 'DESC',
|
|
);
|
|
|
|
// Filter by status
|
|
if ($status !== 'any') {
|
|
$args['post_status'] = $status;
|
|
} else {
|
|
$args['post_status'] = array('publish', 'draft', 'private');
|
|
}
|
|
|
|
// Add search
|
|
if (!empty($search)) {
|
|
$args['s'] = $search;
|
|
}
|
|
|
|
// For non-master trainers, only show published announcements
|
|
if (!$is_master) {
|
|
$args['post_status'] = 'publish';
|
|
}
|
|
|
|
// Query announcements
|
|
$query = new WP_Query($args);
|
|
|
|
$announcements = array();
|
|
if ($query->have_posts()) {
|
|
while ($query->have_posts()) {
|
|
$query->the_post();
|
|
|
|
$categories = wp_get_post_terms(get_the_ID(), HVAC_Announcements_CPT::get_category_taxonomy(), array('fields' => 'names'));
|
|
$tags = wp_get_post_terms(get_the_ID(), HVAC_Announcements_CPT::get_tag_taxonomy(), array('fields' => 'names'));
|
|
|
|
$announcements[] = array(
|
|
'id' => get_the_ID(),
|
|
'title' => get_the_title(),
|
|
'excerpt' => wp_kses_post(get_the_excerpt()), // Sanitize HTML content
|
|
'status' => get_post_status(),
|
|
'date' => get_the_date('Y-m-d H:i:s'),
|
|
'author' => get_the_author(),
|
|
'categories' => $categories,
|
|
'tags' => $tags,
|
|
'featured_image' => get_the_post_thumbnail_url(get_the_ID(), 'thumbnail'),
|
|
'can_edit' => HVAC_Announcements_Permissions::current_user_can_edit(get_the_ID()),
|
|
'can_delete' => HVAC_Announcements_Permissions::current_user_can_delete(get_the_ID()),
|
|
);
|
|
}
|
|
wp_reset_postdata();
|
|
}
|
|
|
|
$response = array(
|
|
'announcements' => $announcements,
|
|
'total' => $query->found_posts,
|
|
'pages' => $query->max_num_pages,
|
|
'current_page' => $page,
|
|
);
|
|
|
|
// Cache the response if not a search query (cache for 2 minutes)
|
|
if (empty($search)) {
|
|
wp_cache_set($cache_key, $response, 'hvac_announcements', 120);
|
|
}
|
|
|
|
wp_send_json_success($response);
|
|
}
|
|
|
|
/**
|
|
* Get single announcement for editing
|
|
*/
|
|
public function get_announcement() {
|
|
// Check rate limiting
|
|
$this->check_rate_limit();
|
|
|
|
// Verify nonce
|
|
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Invalid security token');
|
|
}
|
|
|
|
$post_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
|
|
|
if (!$post_id) {
|
|
wp_send_json_error('Invalid announcement ID');
|
|
}
|
|
|
|
// Check permissions
|
|
if (!HVAC_Announcements_Permissions::current_user_can_edit($post_id)) {
|
|
wp_send_json_error('Insufficient permissions');
|
|
}
|
|
|
|
$post = get_post($post_id);
|
|
|
|
if (!$post || $post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
|
|
wp_send_json_error('Announcement not found');
|
|
}
|
|
|
|
$categories = wp_get_post_terms($post_id, HVAC_Announcements_CPT::get_category_taxonomy(), array('fields' => 'ids'));
|
|
$tags = wp_get_post_terms($post_id, HVAC_Announcements_CPT::get_tag_taxonomy(), array('fields' => 'names'));
|
|
|
|
$announcement = array(
|
|
'id' => $post->ID,
|
|
'title' => $post->post_title,
|
|
'content' => $post->post_content,
|
|
'excerpt' => $post->post_excerpt,
|
|
'status' => $post->post_status,
|
|
'date' => $post->post_date,
|
|
'categories' => $categories,
|
|
'tags' => implode(', ', $tags),
|
|
'featured_image_id' => get_post_thumbnail_id($post->ID),
|
|
'featured_image_url' => get_the_post_thumbnail_url($post->ID, 'medium'),
|
|
);
|
|
|
|
wp_send_json_success($announcement);
|
|
}
|
|
|
|
/**
|
|
* Create new announcement
|
|
*/
|
|
public function create_announcement() {
|
|
// Check rate limiting
|
|
$this->check_rate_limit();
|
|
|
|
// Verify nonce
|
|
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Invalid security token');
|
|
}
|
|
|
|
// Check permissions
|
|
if (!HVAC_Announcements_Permissions::current_user_can_create()) {
|
|
wp_send_json_error('Insufficient permissions');
|
|
}
|
|
|
|
// Validate required fields
|
|
if (empty($_POST['title'])) {
|
|
wp_send_json_error('Title is required');
|
|
}
|
|
|
|
// Prepare post data
|
|
$post_data = array(
|
|
'post_title' => sanitize_text_field($_POST['title']),
|
|
'post_content' => wp_kses_post($_POST['content']),
|
|
'post_excerpt' => sanitize_textarea_field($_POST['excerpt']),
|
|
'post_status' => sanitize_text_field($_POST['status']),
|
|
'post_type' => HVAC_Announcements_CPT::get_post_type(),
|
|
'post_author' => get_current_user_id(),
|
|
);
|
|
|
|
// Set publish date if provided
|
|
if (!empty($_POST['publish_date'])) {
|
|
$post_data['post_date'] = sanitize_text_field($_POST['publish_date']);
|
|
$post_data['post_date_gmt'] = get_gmt_from_date($post_data['post_date']);
|
|
}
|
|
|
|
// Create post
|
|
$post_id = wp_insert_post($post_data);
|
|
|
|
if (is_wp_error($post_id)) {
|
|
wp_send_json_error($post_id->get_error_message());
|
|
}
|
|
|
|
// Set categories
|
|
if (!empty($_POST['categories'])) {
|
|
$categories = array_map('intval', (array) $_POST['categories']);
|
|
wp_set_post_terms($post_id, $categories, HVAC_Announcements_CPT::get_category_taxonomy());
|
|
}
|
|
|
|
// Set tags
|
|
if (!empty($_POST['tags'])) {
|
|
$tags = array_map('trim', explode(',', sanitize_text_field($_POST['tags'])));
|
|
wp_set_post_terms($post_id, $tags, HVAC_Announcements_CPT::get_tag_taxonomy());
|
|
}
|
|
|
|
// Set featured image
|
|
if (!empty($_POST['featured_image_id'])) {
|
|
set_post_thumbnail($post_id, intval($_POST['featured_image_id']));
|
|
}
|
|
|
|
wp_send_json_success(array(
|
|
'id' => $post_id,
|
|
'message' => __('Announcement created successfully', 'hvac'),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Update existing announcement
|
|
*/
|
|
public function update_announcement() {
|
|
// Check rate limiting
|
|
$this->check_rate_limit();
|
|
|
|
// Verify nonce
|
|
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Invalid security token');
|
|
}
|
|
|
|
$post_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
|
|
|
if (!$post_id) {
|
|
wp_send_json_error('Invalid announcement ID');
|
|
}
|
|
|
|
// Check permissions
|
|
if (!HVAC_Announcements_Permissions::current_user_can_edit($post_id)) {
|
|
wp_send_json_error('Insufficient permissions');
|
|
}
|
|
|
|
// Validate required fields
|
|
if (empty($_POST['title'])) {
|
|
wp_send_json_error('Title is required');
|
|
}
|
|
|
|
// Prepare post data
|
|
$post_data = array(
|
|
'ID' => $post_id,
|
|
'post_title' => sanitize_text_field($_POST['title']),
|
|
'post_content' => wp_kses_post($_POST['content']),
|
|
'post_excerpt' => sanitize_textarea_field($_POST['excerpt']),
|
|
'post_status' => sanitize_text_field($_POST['status']),
|
|
);
|
|
|
|
// Set publish date if provided
|
|
if (!empty($_POST['publish_date'])) {
|
|
$post_data['post_date'] = sanitize_text_field($_POST['publish_date']);
|
|
$post_data['post_date_gmt'] = get_gmt_from_date($post_data['post_date']);
|
|
}
|
|
|
|
// Update post
|
|
$result = wp_update_post($post_data);
|
|
|
|
if (is_wp_error($result)) {
|
|
wp_send_json_error($result->get_error_message());
|
|
}
|
|
|
|
// Update categories
|
|
if (isset($_POST['categories'])) {
|
|
$categories = array_map('intval', (array) $_POST['categories']);
|
|
wp_set_post_terms($post_id, $categories, HVAC_Announcements_CPT::get_category_taxonomy());
|
|
}
|
|
|
|
// Update tags
|
|
if (isset($_POST['tags'])) {
|
|
$tags = array_map('trim', explode(',', sanitize_text_field($_POST['tags'])));
|
|
wp_set_post_terms($post_id, $tags, HVAC_Announcements_CPT::get_tag_taxonomy());
|
|
}
|
|
|
|
// Update featured image
|
|
if (isset($_POST['featured_image_id'])) {
|
|
if (empty($_POST['featured_image_id'])) {
|
|
delete_post_thumbnail($post_id);
|
|
} else {
|
|
set_post_thumbnail($post_id, intval($_POST['featured_image_id']));
|
|
}
|
|
}
|
|
|
|
wp_send_json_success(array(
|
|
'id' => $post_id,
|
|
'message' => __('Announcement updated successfully', 'hvac'),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Delete announcement
|
|
*/
|
|
public function delete_announcement() {
|
|
// Check rate limiting
|
|
$this->check_rate_limit();
|
|
|
|
// Verify nonce
|
|
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Invalid security token');
|
|
}
|
|
|
|
$post_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
|
|
|
if (!$post_id) {
|
|
wp_send_json_error('Invalid announcement ID');
|
|
}
|
|
|
|
// Check permissions
|
|
if (!HVAC_Announcements_Permissions::current_user_can_delete($post_id)) {
|
|
wp_send_json_error('Insufficient permissions');
|
|
}
|
|
|
|
// Delete post
|
|
$result = wp_delete_post($post_id, true); // Force delete
|
|
|
|
if (!$result) {
|
|
wp_send_json_error('Failed to delete announcement');
|
|
}
|
|
|
|
wp_send_json_success(array(
|
|
'message' => __('Announcement deleted successfully', 'hvac'),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Get categories for select options
|
|
*/
|
|
public function get_categories() {
|
|
// Check rate limiting
|
|
$this->check_rate_limit();
|
|
|
|
// Verify nonce
|
|
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Invalid security token');
|
|
}
|
|
|
|
// Check permissions - only users who can create announcements need category list
|
|
if (!HVAC_Announcements_Permissions::current_user_can_create()) {
|
|
wp_send_json_error('Insufficient permissions');
|
|
}
|
|
|
|
$categories = get_terms(array(
|
|
'taxonomy' => HVAC_Announcements_CPT::get_category_taxonomy(),
|
|
'hide_empty' => false,
|
|
));
|
|
|
|
$options = array();
|
|
if (!is_wp_error($categories)) {
|
|
foreach ($categories as $category) {
|
|
$options[] = array(
|
|
'id' => $category->term_id,
|
|
'name' => $category->name,
|
|
);
|
|
}
|
|
}
|
|
|
|
wp_send_json_success($options);
|
|
}
|
|
|
|
/**
|
|
* Get tags for autocomplete
|
|
*/
|
|
public function get_tags() {
|
|
// Check rate limiting
|
|
$this->check_rate_limit();
|
|
|
|
// Verify nonce
|
|
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Invalid security token');
|
|
}
|
|
|
|
// Check permissions - only users who can create announcements need tag list
|
|
if (!HVAC_Announcements_Permissions::current_user_can_create()) {
|
|
wp_send_json_error('Insufficient permissions');
|
|
}
|
|
|
|
$tags = get_terms(array(
|
|
'taxonomy' => HVAC_Announcements_CPT::get_tag_taxonomy(),
|
|
'hide_empty' => false,
|
|
));
|
|
|
|
$tag_names = array();
|
|
if (!is_wp_error($tags)) {
|
|
foreach ($tags as $tag) {
|
|
$tag_names[] = $tag->name;
|
|
}
|
|
}
|
|
|
|
wp_send_json_success($tag_names);
|
|
}
|
|
|
|
/**
|
|
* Get announcement content for modal viewing
|
|
*/
|
|
public function view_announcement() {
|
|
// Check rate limiting
|
|
$this->check_rate_limit();
|
|
|
|
// Verify nonce
|
|
if (!check_ajax_referer('hvac_announcements_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Invalid security token');
|
|
}
|
|
|
|
$post_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
|
|
|
if (!$post_id) {
|
|
wp_send_json_error('Invalid announcement ID');
|
|
}
|
|
|
|
// Check permissions - only need read permission for viewing
|
|
if (!HVAC_Announcements_Permissions::current_user_can_read()) {
|
|
wp_send_json_error('Insufficient permissions');
|
|
}
|
|
|
|
// Check post status - only allow published posts for non-master trainers
|
|
$post = get_post($post_id);
|
|
if (!$post || $post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
|
|
wp_send_json_error('Announcement not found');
|
|
}
|
|
|
|
// Only allow viewing of published posts unless user is a master trainer
|
|
if ($post->post_status !== 'publish' && !HVAC_Announcements_Permissions::is_master_trainer()) {
|
|
wp_send_json_error('You do not have permission to view this announcement');
|
|
}
|
|
|
|
// Get announcement content using the Display class method
|
|
$content = HVAC_Announcements_Display::get_announcement_content($post_id);
|
|
|
|
if (empty($content)) {
|
|
wp_send_json_error('Announcement not found or you do not have permission to view it');
|
|
}
|
|
|
|
wp_send_json_success(array(
|
|
'content' => $content
|
|
));
|
|
}
|
|
} |