upskill-event-manager/includes/class-hvac-announcements-display.php
Ben c20b461e7d feat: Implement secure Trainer Announcements system with comprehensive features
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>
2025-08-20 13:34:15 -03:00

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">&times;</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'
);
}
}
}