30, 'hvac_search_organizers' => 30, 'hvac_validate_form' => 60, 'hvac_save_draft' => 20, 'hvac_upload_image' => 10, ]; /** * Constructor */ private function __construct() { $this->cache = HVAC_Event_Cache::instance(); $this->init_hooks(); } /** * Initialize WordPress hooks */ private function init_hooks(): void { // Optimized AJAX endpoints add_action('wp_ajax_hvac_search_venues', [$this, 'ajax_search_venues']); add_action('wp_ajax_hvac_search_organizers', [$this, 'ajax_search_organizers']); add_action('wp_ajax_hvac_validate_form', [$this, 'ajax_validate_form']); add_action('wp_ajax_hvac_save_draft', [$this, 'ajax_save_draft']); add_action('wp_ajax_hvac_upload_image', [$this, 'ajax_upload_image']); add_action('wp_ajax_hvac_get_timezone_list', [$this, 'ajax_get_timezone_list']); // Admin-only endpoints add_action('wp_ajax_hvac_cache_stats', [$this, 'ajax_cache_stats']); // Enqueue optimized AJAX scripts add_action('wp_enqueue_scripts', [$this, 'enqueue_ajax_scripts']); } /** * Enqueue optimized AJAX scripts */ public function enqueue_ajax_scripts(): void { // Only load on event-related pages if (!$this->should_load_ajax_scripts()) { return; } wp_enqueue_script( 'hvac-ajax-optimizer', HVAC_PLUGIN_URL . 'assets/js/hvac-ajax-optimizer.js', ['jquery'], HVAC_VERSION, true ); // Localize script with AJAX configuration wp_localize_script('hvac-ajax-optimizer', 'hvacAjax', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('hvac_ajax_nonce'), 'debounceDelay' => 300, 'cacheEnabled' => true, 'rateLimits' => $this->ajax_rate_limits, ]); } /** * Check if AJAX scripts should be loaded * * @return bool */ private function should_load_ajax_scripts(): bool { global $post; // Load on event form pages if (is_page() && $post) { $template = get_page_template_slug($post->ID); if (strpos($template, 'event') !== false) { return true; } } // Load on trainer/master trainer dashboards if (is_user_logged_in()) { $user = wp_get_current_user(); if (in_array('hvac_trainer', $user->roles) || in_array('hvac_master_trainer', $user->roles)) { return true; } } return false; } /** * Apply rate limiting to AJAX requests * * @param string $action AJAX action name * @return bool True if request is allowed, false if rate limited */ private function apply_rate_limiting(string $action): bool { if (!isset($this->ajax_rate_limits[$action])) { return true; } $limit = $this->ajax_rate_limits[$action]; $user_id = get_current_user_id(); $client_ip = $this->get_client_ip(); // Create rate limit key based on user ID or IP $rate_key = $user_id > 0 ? "user_{$user_id}_{$action}" : "ip_{$client_ip}_{$action}"; $current_time = time(); $window_start = $current_time - 60; // 1 minute window // Get existing rate limit data $rate_data = get_transient("hvac_rate_limit_{$rate_key}") ?: []; // Remove old entries outside the time window $rate_data = array_filter($rate_data, fn($timestamp) => $timestamp > $window_start); // Check if limit exceeded if (count($rate_data) >= $limit) { return false; } // Add current request timestamp $rate_data[] = $current_time; // Store updated rate limit data set_transient("hvac_rate_limit_{$rate_key}", $rate_data, 120); return true; } /** * Get client IP address * * @return string */ private function get_client_ip(): string { $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR']; foreach ($ip_keys as $key) { if (!empty($_SERVER[$key])) { $ip = sanitize_text_field($_SERVER[$key]); // Handle comma-separated IPs (from proxies) if (strpos($ip, ',') !== false) { $ip = trim(explode(',', $ip)[0]); } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } return sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? ''); } /** * AJAX handler for venue search with caching */ public function ajax_search_venues(): void { // Security and rate limiting if (!wp_verify_nonce($_GET['nonce'] ?? '', 'hvac_ajax_nonce')) { wp_send_json_error(['message' => 'Security check failed']); return; } if (!$this->apply_rate_limiting('hvac_search_venues')) { wp_send_json_error(['message' => 'Rate limit exceeded']); return; } $search_query = sanitize_text_field($_GET['q'] ?? ''); if (empty($search_query) || strlen($search_query) < 2) { wp_send_json_error(['message' => 'Search query too short']); return; } // Check cache first $cache_key = md5($search_query . '_' . get_current_user_id()); $cached_results = $this->cache->get_venue_search($cache_key); if ($cached_results !== false) { wp_send_json_success([ 'venues' => $cached_results, 'cached' => true ]); return; } // Search venues $venues = $this->search_venues($search_query); // Cache results $this->cache->cache_venue_search($cache_key, $venues); wp_send_json_success([ 'venues' => $venues, 'cached' => false ]); } /** * AJAX handler for organizer search with caching */ public function ajax_search_organizers(): void { // Security and rate limiting if (!wp_verify_nonce($_GET['nonce'] ?? '', 'hvac_ajax_nonce')) { wp_send_json_error(['message' => 'Security check failed']); return; } if (!$this->apply_rate_limiting('hvac_search_organizers')) { wp_send_json_error(['message' => 'Rate limit exceeded']); return; } $search_query = sanitize_text_field($_GET['q'] ?? ''); if (empty($search_query) || strlen($search_query) < 2) { wp_send_json_error(['message' => 'Search query too short']); return; } // Search organizers $organizers = $this->search_organizers($search_query); wp_send_json_success(['organizers' => $organizers]); } /** * AJAX handler for form validation */ public function ajax_validate_form(): void { // Security and rate limiting if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ajax_nonce')) { wp_send_json_error(['message' => 'Security check failed']); return; } if (!$this->apply_rate_limiting('hvac_validate_form')) { wp_send_json_error(['message' => 'Rate limit exceeded']); return; } $form_data = $_POST['form_data'] ?? []; // Create form builder for validation $form_builder = new HVAC_Event_Form_Builder('hvac_ajax_validation'); $form_builder->create_event_form(); // Validate the form data $errors = $form_builder->validate($form_data); wp_send_json_success([ 'valid' => empty($errors), 'errors' => $errors ]); } /** * AJAX handler for saving draft events */ public function ajax_save_draft(): void { // Security and rate limiting if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ajax_nonce')) { wp_send_json_error(['message' => 'Security check failed']); return; } if (!$this->apply_rate_limiting('hvac_save_draft')) { wp_send_json_error(['message' => 'Rate limit exceeded']); return; } // Check user permissions if (!is_user_logged_in()) { wp_send_json_error(['message' => 'Authentication required']); return; } $form_data = $_POST['form_data'] ?? []; // Generate unique form ID for this user session $form_id = 'draft_' . get_current_user_id() . '_' . time(); // Cache the form data $cached = $this->cache->cache_form_data($form_id, $form_data); if ($cached) { wp_send_json_success([ 'message' => 'Draft saved successfully', 'draft_id' => $form_id ]); } else { wp_send_json_error(['message' => 'Failed to save draft']); } } /** * AJAX handler for image upload */ public function ajax_upload_image(): void { // Security and rate limiting if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ajax_nonce')) { wp_send_json_error(['message' => 'Security check failed']); return; } if (!$this->apply_rate_limiting('hvac_upload_image')) { wp_send_json_error(['message' => 'Rate limit exceeded']); return; } // Check user permissions if (!current_user_can('upload_files')) { wp_send_json_error(['message' => 'Insufficient permissions']); return; } if (!isset($_FILES['image'])) { wp_send_json_error(['message' => 'No image uploaded']); return; } // Handle file upload require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $attachment_id = media_handle_upload('image', 0); if (is_wp_error($attachment_id)) { wp_send_json_error(['message' => $attachment_id->get_error_message()]); return; } $attachment_url = wp_get_attachment_url($attachment_id); $attachment_meta = wp_get_attachment_metadata($attachment_id); wp_send_json_success([ 'attachment_id' => $attachment_id, 'url' => $attachment_url, 'meta' => $attachment_meta ]); } /** * AJAX handler for timezone list (cached) */ public function ajax_get_timezone_list(): void { // Security check if (!wp_verify_nonce($_GET['nonce'] ?? '', 'hvac_ajax_nonce')) { wp_send_json_error(['message' => 'Security check failed']); return; } // Try cache first $timezone_list = $this->cache->get_timezone_list(); if ($timezone_list === false) { // Generate timezone list $zones = wp_timezone_choice('UTC'); $timezone_list = []; if (preg_match_all('/