upskill-event-manager/includes/class-hvac-announcements-ajax.php
Ben cc34abb5fe feat: implement announcement modal system with comprehensive documentation
- Add interactive modal popup for announcement 'Read More' functionality
- Fix nonce conflict by creating separate hvac_announcements_ajax object
- Implement secure AJAX handler with rate limiting and permission checks
- Add comprehensive modal CSS with smooth animations and responsive design
- Include accessibility features (ARIA, keyboard navigation, screen reader support)
- Create detailed documentation in docs/ANNOUNCEMENT-MODAL-SYSTEM.md
- Update API-REFERENCE.md with new modal endpoints and security details
- Add automated Playwright E2E testing for modal functionality
- All modal interactions working: click to open, X to close, ESC to close, outside click
- Production-ready with full error handling and content sanitization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 16:28:55 -03:00

588 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 data
$title = get_the_title($post);
$content = apply_filters('the_content', $post->post_content);
$date = get_the_date('F j, Y', $post);
$author = get_the_author_meta('display_name', $post->post_author);
wp_send_json_success(array(
'title' => $title,
'content' => $content,
'date' => $date,
'author' => $author
));
}
}