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>
		
			
				
	
	
		
			431 lines
		
	
	
		
			No EOL
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			431 lines
		
	
	
		
			No EOL
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * HVAC Announcements Display Handler
 | |
|  *
 | |
|  * @package HVAC_Community_Events
 | |
|  * @since 1.0.0
 | |
|  */
 | |
| 
 | |
| if (!defined('ABSPATH')) {
 | |
|     exit;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Class HVAC_Announcements_Display
 | |
|  *
 | |
|  * Handles frontend display and formatting of announcements
 | |
|  */
 | |
| class HVAC_Announcements_Display {
 | |
|     
 | |
|     /**
 | |
|      * Instance of this class
 | |
|      *
 | |
|      * @var HVAC_Announcements_Display
 | |
|      */
 | |
|     private static $instance = null;
 | |
|     
 | |
|     /**
 | |
|      * Get instance of this class
 | |
|      *
 | |
|      * @return HVAC_Announcements_Display
 | |
|      */
 | |
|     public static function get_instance() {
 | |
|         if (null === self::$instance) {
 | |
|             self::$instance = new self();
 | |
|         }
 | |
|         return self::$instance;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Constructor
 | |
|      */
 | |
|     private function __construct() {
 | |
|         $this->init_hooks();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Initialize hooks
 | |
|      */
 | |
|     private function init_hooks() {
 | |
|         // Register shortcodes
 | |
|         add_shortcode('hvac_announcements_timeline', array($this, 'render_timeline_shortcode'));
 | |
|         add_shortcode('hvac_announcements_list', array($this, 'render_list_shortcode'));
 | |
|         add_shortcode('hvac_google_drive_embed', array($this, 'render_google_drive_shortcode'));
 | |
|         
 | |
|         // Filter for UAGB post timeline to include our post type
 | |
|         add_filter('uagb_post_timeline_query_args', array($this, 'modify_uagb_query'), 10, 2);
 | |
|         
 | |
|         // Enqueue scripts when shortcode is used
 | |
|         add_action('wp_enqueue_scripts', array($this, 'maybe_enqueue_scripts'));
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Render announcements timeline shortcode
 | |
|      *
 | |
|      * @param array $atts Shortcode attributes
 | |
|      * @return string
 | |
|      */
 | |
|     public function render_timeline_shortcode($atts) {
 | |
|         // Check permissions
 | |
|         if (!HVAC_Announcements_Permissions::current_user_can_read()) {
 | |
|             return '<p>' . __('You do not have permission to view announcements.', 'hvac') . '</p>';
 | |
|         }
 | |
|         
 | |
|         $atts = shortcode_atts(array(
 | |
|             'posts_per_page' => 10,
 | |
|             'orderby' => 'date',
 | |
|             'order' => 'DESC',
 | |
|         ), $atts);
 | |
|         
 | |
|         // Query announcements
 | |
|         $args = array(
 | |
|             'post_type' => HVAC_Announcements_CPT::get_post_type(),
 | |
|             'posts_per_page' => intval($atts['posts_per_page']),
 | |
|             'orderby' => sanitize_text_field($atts['orderby']),
 | |
|             'order' => sanitize_text_field($atts['order']),
 | |
|             'post_status' => 'publish',
 | |
|         );
 | |
|         
 | |
|         $query = new WP_Query($args);
 | |
|         
 | |
|         ob_start();
 | |
|         ?>
 | |
|         <div class="hvac-announcements-timeline">
 | |
|             <?php if ($query->have_posts()) : ?>
 | |
|                 <div class="timeline-wrapper">
 | |
|                     <?php while ($query->have_posts()) : $query->the_post(); ?>
 | |
|                         <article class="timeline-item">
 | |
|                             <div class="timeline-marker"></div>
 | |
|                             <div class="timeline-content">
 | |
|                                 <header class="timeline-header">
 | |
|                                     <h3 class="timeline-title">
 | |
|                                         <a href="#" class="announcement-link" data-id="<?php echo esc_attr(get_the_ID()); ?>">
 | |
|                                             <?php the_title(); ?>
 | |
|                                         </a>
 | |
|                                     </h3>
 | |
|                                     <div class="timeline-meta">
 | |
|                                         <span class="timeline-date"><?php echo esc_html(get_the_date()); ?></span>
 | |
|                                         <span class="timeline-author"><?php echo esc_html(get_the_author()); ?></span>
 | |
|                                     </div>
 | |
|                                 </header>
 | |
|                                 
 | |
|                                 <?php if (has_post_thumbnail()) : ?>
 | |
|                                     <div class="timeline-thumbnail">
 | |
|                                         <?php the_post_thumbnail('medium'); ?>
 | |
|                                     </div>
 | |
|                                 <?php endif; ?>
 | |
|                                 
 | |
|                                 <div class="timeline-excerpt">
 | |
|                                     <?php the_excerpt(); ?>
 | |
|                                 </div>
 | |
|                                 
 | |
|                                 <?php
 | |
|                                 $categories = wp_get_post_terms(get_the_ID(), HVAC_Announcements_CPT::get_category_taxonomy());
 | |
|                                 if (!empty($categories)) :
 | |
|                                 ?>
 | |
|                                     <div class="timeline-categories">
 | |
|                                         <?php foreach ($categories as $category) : ?>
 | |
|                                             <span class="category-badge"><?php echo esc_html($category->name); ?></span>
 | |
|                                         <?php endforeach; ?>
 | |
|                                     </div>
 | |
|                                 <?php endif; ?>
 | |
|                             </div>
 | |
|                         </article>
 | |
|                     <?php endwhile; ?>
 | |
|                 </div>
 | |
|                 
 | |
|                 <?php if ($query->max_num_pages > 1) : ?>
 | |
|                     <div class="timeline-pagination">
 | |
|                         <button class="load-more-announcements" data-page="2" data-max="<?php echo esc_attr($query->max_num_pages); ?>">
 | |
|                             <?php _e('Load More Announcements', 'hvac'); ?>
 | |
|                         </button>
 | |
|                     </div>
 | |
|                 <?php endif; ?>
 | |
|             <?php else : ?>
 | |
|                 <div class="no-announcements">
 | |
|                     <p><?php _e('No announcements have been posted yet.', 'hvac'); ?></p>
 | |
|                 </div>
 | |
|             <?php endif; ?>
 | |
|             
 | |
|             <?php wp_reset_postdata(); ?>
 | |
|         </div>
 | |
|         
 | |
|         <!-- Modal for viewing announcement -->
 | |
|         <div id="announcement-modal" class="hvac-modal" style="display: none;">
 | |
|             <div class="modal-content">
 | |
|                 <span class="modal-close">×</span>
 | |
|                 <div class="modal-body">
 | |
|                     <!-- Content loaded via AJAX -->
 | |
|                 </div>
 | |
|             </div>
 | |
|         </div>
 | |
|         <?php
 | |
|         
 | |
|         return ob_get_clean();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Render announcements list shortcode
 | |
|      *
 | |
|      * @param array $atts Shortcode attributes
 | |
|      * @return string
 | |
|      */
 | |
|     public function render_list_shortcode($atts) {
 | |
|         // Check permissions
 | |
|         if (!HVAC_Announcements_Permissions::current_user_can_read()) {
 | |
|             return '<p>' . __('You do not have permission to view announcements.', 'hvac') . '</p>';
 | |
|         }
 | |
|         
 | |
|         $atts = shortcode_atts(array(
 | |
|             'posts_per_page' => 5,
 | |
|             'show_excerpt' => 'yes',
 | |
|             'show_date' => 'yes',
 | |
|             'show_author' => 'no',
 | |
|         ), $atts);
 | |
|         
 | |
|         // Query announcements
 | |
|         $args = array(
 | |
|             'post_type' => HVAC_Announcements_CPT::get_post_type(),
 | |
|             'posts_per_page' => intval($atts['posts_per_page']),
 | |
|             'orderby' => 'date',
 | |
|             'order' => 'DESC',
 | |
|             'post_status' => 'publish',
 | |
|         );
 | |
|         
 | |
|         $query = new WP_Query($args);
 | |
|         
 | |
|         ob_start();
 | |
|         ?>
 | |
|         <div class="hvac-announcements-list">
 | |
|             <?php if ($query->have_posts()) : ?>
 | |
|                 <ul class="announcements-list">
 | |
|                     <?php while ($query->have_posts()) : $query->the_post(); ?>
 | |
|                         <li class="announcement-item">
 | |
|                             <h4 class="announcement-title">
 | |
|                                 <a href="#" class="announcement-link" data-id="<?php echo esc_attr(get_the_ID()); ?>">
 | |
|                                     <?php the_title(); ?>
 | |
|                                 </a>
 | |
|                             </h4>
 | |
|                             
 | |
|                             <?php if ($atts['show_date'] === 'yes' || $atts['show_author'] === 'yes') : ?>
 | |
|                                 <div class="announcement-meta">
 | |
|                                     <?php if ($atts['show_date'] === 'yes') : ?>
 | |
|                                         <span class="announcement-date"><?php echo esc_html(get_the_date()); ?></span>
 | |
|                                     <?php endif; ?>
 | |
|                                     
 | |
|                                     <?php if ($atts['show_author'] === 'yes') : ?>
 | |
|                                         <span class="announcement-author"><?php echo esc_html(get_the_author()); ?></span>
 | |
|                                     <?php endif; ?>
 | |
|                                 </div>
 | |
|                             <?php endif; ?>
 | |
|                             
 | |
|                             <?php if ($atts['show_excerpt'] === 'yes') : ?>
 | |
|                                 <div class="announcement-excerpt">
 | |
|                                     <?php the_excerpt(); ?>
 | |
|                                 </div>
 | |
|                             <?php endif; ?>
 | |
|                         </li>
 | |
|                     <?php endwhile; ?>
 | |
|                 </ul>
 | |
|             <?php else : ?>
 | |
|                 <p><?php _e('No announcements have been posted yet.', 'hvac'); ?></p>
 | |
|             <?php endif; ?>
 | |
|             
 | |
|             <?php wp_reset_postdata(); ?>
 | |
|         </div>
 | |
|         <?php
 | |
|         
 | |
|         return ob_get_clean();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Render Google Drive embed shortcode
 | |
|      *
 | |
|      * @param array $atts Shortcode attributes
 | |
|      * @return string
 | |
|      */
 | |
|     public function render_google_drive_shortcode($atts) {
 | |
|         // Check permissions
 | |
|         if (!HVAC_Announcements_Permissions::is_trainer()) {
 | |
|             return '<p>' . __('You do not have permission to view training resources.', 'hvac') . '</p>';
 | |
|         }
 | |
|         
 | |
|         $atts = shortcode_atts(array(
 | |
|             'url' => 'https://drive.google.com/drive/folders/16uDRkFcaEqKUxfBek9VbfbAIeFV77nZG?usp=drive_link',
 | |
|             'height' => '600',
 | |
|             'width' => '100%',
 | |
|         ), $atts);
 | |
|         
 | |
|         // Convert sharing URL to embed URL
 | |
|         $embed_url = $this->convert_drive_url_to_embed($atts['url']);
 | |
|         
 | |
|         ob_start();
 | |
|         ?>
 | |
|         <div class="hvac-google-drive-embed">
 | |
|             <iframe 
 | |
|                 src="<?php echo esc_url($embed_url); ?>"
 | |
|                 width="<?php echo esc_attr($atts['width']); ?>"
 | |
|                 height="<?php echo esc_attr($atts['height']); ?>"
 | |
|                 frameborder="0"
 | |
|                 allowfullscreen="true"
 | |
|                 mozallowfullscreen="true"
 | |
|                 webkitallowfullscreen="true">
 | |
|             </iframe>
 | |
|         </div>
 | |
|         <?php
 | |
|         
 | |
|         return ob_get_clean();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Convert Google Drive sharing URL to embed URL
 | |
|      *
 | |
|      * @param string $url Sharing URL
 | |
|      * @return string Embed URL
 | |
|      */
 | |
|     private function convert_drive_url_to_embed($url) {
 | |
|         // Extract folder ID from URL
 | |
|         if (preg_match('/\/folders\/([a-zA-Z0-9-_]+)/', $url, $matches)) {
 | |
|             $folder_id = $matches[1];
 | |
|             return 'https://drive.google.com/embeddedfolderview?id=' . $folder_id . '#list';
 | |
|         }
 | |
|         
 | |
|         // Return original URL if pattern doesn't match
 | |
|         return $url;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Modify UAGB query to include our post type
 | |
|      *
 | |
|      * @param array $query_args Query arguments
 | |
|      * @param array $attributes Block attributes
 | |
|      * @return array
 | |
|      */
 | |
|     public function modify_uagb_query($query_args, $attributes) {
 | |
|         // Check if this is for announcements
 | |
|         if (isset($attributes['post_type']) && $attributes['post_type'] === 'hvac_announcement') {
 | |
|             $query_args['post_type'] = HVAC_Announcements_CPT::get_post_type();
 | |
|             
 | |
|             // Only show published posts to non-master trainers
 | |
|             if (!HVAC_Announcements_Permissions::is_master_trainer()) {
 | |
|                 $query_args['post_status'] = 'publish';
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         return $query_args;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get single announcement content for modal
 | |
|      *
 | |
|      * @param int $post_id Announcement ID
 | |
|      * @return string
 | |
|      */
 | |
|     public static function get_announcement_content($post_id) {
 | |
|         $post = get_post($post_id);
 | |
|         
 | |
|         if (!$post || $post->post_type !== HVAC_Announcements_CPT::get_post_type()) {
 | |
|             return '';
 | |
|         }
 | |
|         
 | |
|         // Check permissions
 | |
|         if (!HVAC_Announcements_Permissions::current_user_can_read()) {
 | |
|             return '';
 | |
|         }
 | |
|         
 | |
|         // Only allow viewing of published posts unless user is a master trainer
 | |
|         if ($post->post_status !== 'publish' && !HVAC_Announcements_Permissions::is_master_trainer()) {
 | |
|             return '';
 | |
|         }
 | |
|         
 | |
|         ob_start();
 | |
|         ?>
 | |
|         <article class="announcement-full">
 | |
|             <header class="announcement-header">
 | |
|                 <h2><?php echo esc_html($post->post_title); ?></h2>
 | |
|                 <div class="announcement-meta">
 | |
|                     <span class="date"><?php echo esc_html(get_the_date('F j, Y', $post)); ?></span>
 | |
|                     <span class="author"><?php echo esc_html(get_the_author_meta('display_name', $post->post_author)); ?></span>
 | |
|                 </div>
 | |
|             </header>
 | |
|             
 | |
|             <?php if (has_post_thumbnail($post->ID)) : ?>
 | |
|                 <div class="announcement-featured-image">
 | |
|                     <?php echo get_the_post_thumbnail($post->ID, 'large'); ?>
 | |
|                 </div>
 | |
|             <?php endif; ?>
 | |
|             
 | |
|             <div class="announcement-content">
 | |
|                 <?php echo apply_filters('the_content', $post->post_content); ?>
 | |
|             </div>
 | |
|             
 | |
|             <?php
 | |
|             $categories = wp_get_post_terms($post->ID, HVAC_Announcements_CPT::get_category_taxonomy());
 | |
|             $tags = wp_get_post_terms($post->ID, HVAC_Announcements_CPT::get_tag_taxonomy());
 | |
|             
 | |
|             if (!empty($categories) || !empty($tags)) :
 | |
|             ?>
 | |
|                 <footer class="announcement-footer">
 | |
|                     <?php if (!empty($categories)) : ?>
 | |
|                         <div class="announcement-categories">
 | |
|                             <strong><?php _e('Categories:', 'hvac'); ?></strong>
 | |
|                             <?php
 | |
|                             $category_names = wp_list_pluck($categories, 'name');
 | |
|                             echo esc_html(implode(', ', $category_names));
 | |
|                             ?>
 | |
|                         </div>
 | |
|                     <?php endif; ?>
 | |
|                     
 | |
|                     <?php if (!empty($tags)) : ?>
 | |
|                         <div class="announcement-tags">
 | |
|                             <strong><?php _e('Tags:', 'hvac'); ?></strong>
 | |
|                             <?php
 | |
|                             $tag_names = wp_list_pluck($tags, 'name');
 | |
|                             echo esc_html(implode(', ', $tag_names));
 | |
|                             ?>
 | |
|                         </div>
 | |
|                     <?php endif; ?>
 | |
|                 </footer>
 | |
|             <?php endif; ?>
 | |
|         </article>
 | |
|         <?php
 | |
|         
 | |
|         return ob_get_clean();
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Maybe enqueue scripts if announcements are displayed
 | |
|      */
 | |
|     public function maybe_enqueue_scripts() {
 | |
|         global $post;
 | |
|         
 | |
|         // Check if any of our shortcodes are present
 | |
|         if (is_a($post, 'WP_Post') && (
 | |
|             has_shortcode($post->post_content, 'hvac_announcements_timeline') ||
 | |
|             has_shortcode($post->post_content, 'hvac_announcements_list')
 | |
|         )) {
 | |
|             // Enqueue the view script
 | |
|             wp_enqueue_script(
 | |
|                 'hvac-announcements-view',
 | |
|                 plugin_dir_url(dirname(__FILE__)) . 'assets/js/hvac-announcements-view.js',
 | |
|                 array('jquery'),
 | |
|                 defined('HVAC_VERSION') ? HVAC_VERSION : '1.0.0',
 | |
|                 true
 | |
|             );
 | |
|             
 | |
|             // Localize script
 | |
|             wp_localize_script('hvac-announcements-view', 'hvac_ajax', array(
 | |
|                 'ajax_url' => admin_url('admin-ajax.php'),
 | |
|                 'nonce' => wp_create_nonce('hvac_announcements_nonce'),
 | |
|             ));
 | |
|             
 | |
|             // Also enqueue the CSS
 | |
|             wp_enqueue_style(
 | |
|                 'hvac-announcements',
 | |
|                 plugin_dir_url(dirname(__FILE__)) . 'assets/css/hvac-announcements.css',
 | |
|                 array(),
 | |
|                 defined('HVAC_VERSION') ? HVAC_VERSION : '1.0.0'
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| }
 |