From ca928bfffb770b3a3435add8351f3b2b6ce9b5d2 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 20 Feb 2026 13:50:51 -0400 Subject: [PATCH] feat: Staging email filter, venue geocoding, MapGeo improvements, trainers overview Accumulated changes from previous sessions (Feb 9-20): - Staging email filter to prevent test emails reaching real users - Version bump to 2.2.0 in plugin header - Venue geocoding enhancements and batch processing - Find Training page improvements (tab content, map data) - MapGeo integration hardening for Find a Trainer - Master trainers overview table improvements - AJAX handler additions for venue categories - SVG marker icon tweaks (trainer, venue) Co-Authored-By: Claude Opus 4.6 --- assets/images/marker-trainer.svg | Bin 339 -> 341 bytes assets/images/marker-venue.svg | Bin 360 -> 362 bytes hvac-community-events.php | 46 ++++- includes/class-hvac-ajax-handlers.php | 18 ++ .../class-hvac-master-trainers-overview.php | 79 +++++--- includes/class-hvac-venue-categories.php | 11 +- .../class-hvac-mapgeo-integration.php | 120 ++++++++---- .../class-hvac-find-training-page.php | 76 ++++++-- .../class-hvac-venue-geocoding.php | 180 +++++++++++++++++- 9 files changed, 453 insertions(+), 77 deletions(-) diff --git a/assets/images/marker-trainer.svg b/assets/images/marker-trainer.svg index 507fdbd53dd4e9645fab4f92069b04d0ead1afb9..0609cfad3ffcbd9d4d813b16e1d03eec7026aed5 100644 GIT binary patch delta 83 zcmcc2bd_mBk%_UALRw}{j;)e%nn9X*s)dq5aY<2rb}CT7FcAb{0=ngyDJ2=UN=AC7 X6PIYSnI>8QB`02Wg|a3qFd72@SECvJ delta 81 zcmcc0beU;Fk&%g#LRw}{j;)fifkBdSB9L2LQk0*a3KU2K1DJqrd1gvUhOLs(#AVuS P5UGjRU7@VWN{q$;M>!i# diff --git a/assets/images/marker-venue.svg b/assets/images/marker-venue.svg index 4ca8337b99719a99ea55fe1c25f10544bf58a2e6..ab902270aef2402c810ddf4f2bd8a8f76d499252 100644 GIT binary patch delta 52 zcmaFC^onUhqn3qbvZYa~l0tDwQGRx+t&*}~A_&3+bjveSN-}JfjPy(=uG|j*(peGQ delta 50 wcmaFG^nz(Zqehx(qM4Dgl0tDwQGRx+t&(yY7{CN{%QI6-GHjKMCa&HO0KjY#8UO$Q diff --git a/hvac-community-events.php b/hvac-community-events.php index 2f3341e2..344464eb 100644 --- a/hvac-community-events.php +++ b/hvac-community-events.php @@ -3,7 +3,7 @@ * Plugin Name: HVAC Community Events * Plugin URI: https://upskillhvac.com * Description: Custom plugin for HVAC trainer event management system - * Version: 2.1.7 + * Version: 2.2.0 * Author: Upskill HVAC * Author URI: https://upskillhvac.com * License: GPL-2.0+ @@ -17,6 +17,50 @@ if (!defined('ABSPATH')) { exit; } +/** + * Staging Email Filter + * Prevents emails from being sent to anyone except allowed addresses on staging. + * This protects real users from receiving test emails during development. + */ +function hvac_is_staging_environment() { + $host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : ''; + $staging_indicators = array( 'staging', 'upskill-staging', 'localhost', '127.0.0.1' ); + + foreach ( $staging_indicators as $indicator ) { + if ( stripos( $host, $indicator ) !== false ) { + return true; + } + } + return false; +} + +function hvac_staging_email_filter( $args ) { + if ( ! hvac_is_staging_environment() ) { + return $args; + } + + $allowed_emails = array( 'ben@tealmaker.com', 'ben@measurequick.com' ); + $to = $args['to']; + + // Extract email address + $email_address = is_array( $to ) ? ( isset( $to[0] ) ? $to[0] : '' ) : $to; + if ( preg_match( '/<([^>]+)>/', $email_address, $matches ) ) { + $email_address = $matches[1]; + } + $email_address = trim( strtolower( $email_address ) ); + + if ( ! in_array( $email_address, $allowed_emails, true ) ) { + error_log( sprintf( '[HVAC Staging] Blocked email to: %s | Subject: %s', + is_array( $to ) ? implode( ', ', $to ) : $to, $args['subject'] ) ); + $args['to'] = ''; + return $args; + } + + $args['subject'] = '[STAGING] ' . $args['subject']; + return $args; +} +add_filter( 'wp_mail', 'hvac_staging_email_filter', 1 ); + // Load the main plugin class require_once plugin_dir_path(__FILE__) . 'includes/class-hvac-plugin.php'; diff --git a/includes/class-hvac-ajax-handlers.php b/includes/class-hvac-ajax-handlers.php index c5b402fc..409a515f 100644 --- a/includes/class-hvac-ajax-handlers.php +++ b/includes/class-hvac-ajax-handlers.php @@ -1046,6 +1046,15 @@ class HVAC_Ajax_Handlers { return; } + // Verify reCAPTCHA + if (class_exists('HVAC_Recaptcha')) { + $recaptcha_response = sanitize_text_field($_POST['g-recaptcha-response'] ?? ''); + if (!HVAC_Recaptcha::instance()->verify_response($recaptcha_response)) { + wp_send_json_error(['message' => 'CAPTCHA verification failed. Please try again.'], 400); + return; + } + } + // Rate limiting - max 5 submissions per IP per hour $ip = $this->get_client_ip(); $rate_key = 'hvac_contact_rate_' . md5($ip); @@ -1202,6 +1211,15 @@ class HVAC_Ajax_Handlers { return; } + // Verify reCAPTCHA + if (class_exists('HVAC_Recaptcha')) { + $recaptcha_response = sanitize_text_field($_POST['g-recaptcha-response'] ?? ''); + if (!HVAC_Recaptcha::instance()->verify_response($recaptcha_response)) { + wp_send_json_error(['message' => 'CAPTCHA verification failed. Please try again.'], 400); + return; + } + } + // Rate limiting - max 5 submissions per IP per hour $ip = $this->get_client_ip(); $rate_key = 'hvac_venue_contact_rate_' . md5($ip); diff --git a/includes/class-hvac-master-trainers-overview.php b/includes/class-hvac-master-trainers-overview.php index e561dcec..9f623593 100644 --- a/includes/class-hvac-master-trainers-overview.php +++ b/includes/class-hvac-master-trainers-overview.php @@ -291,6 +291,8 @@ class HVAC_Master_Trainers_Overview { /** * AJAX handler for filtering trainers + * + * Uses get_trainers_table_data() from dashboard data class for reliable trainer listing */ public function ajax_filter_trainers() { // Verify nonce @@ -306,7 +308,6 @@ class HVAC_Master_Trainers_Overview { // Get filter parameters $args = array( 'status' => isset( $_POST['status'] ) ? sanitize_text_field( $_POST['status'] ) : 'all', - 'region' => isset( $_POST['region'] ) ? sanitize_text_field( $_POST['region'] ) : '', 'search' => isset( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '', 'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1, 'per_page' => isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 20, @@ -314,12 +315,21 @@ class HVAC_Master_Trainers_Overview { 'order' => isset( $_POST['order'] ) ? sanitize_text_field( $_POST['order'] ) : 'ASC', ); - // Get trainers data + // Use the reliable get_trainers_table_data method (same as Master Dashboard) if ( $this->dashboard_data ) { - $trainer_stats = $this->dashboard_data->get_trainer_statistics(); + $trainer_data = $this->dashboard_data->get_trainers_table_data( $args ); + $trainers = $trainer_data['trainers']; - // Format trainers for display - $formatted_trainers = $this->format_trainers_for_display( $trainer_stats['trainer_data'], $args ); + // Apply region filter if specified (not handled by get_trainers_table_data) + $region_filter = isset( $_POST['region'] ) ? sanitize_text_field( $_POST['region'] ) : ''; + if ( ! empty( $region_filter ) ) { + $trainers = array_filter( $trainers, function( $trainer ) use ( $region_filter ) { + $user_meta = get_user_meta( $trainer['id'] ); + $user_state = isset( $user_meta['billing_state'][0] ) ? $user_meta['billing_state'][0] : ''; + return $user_state === $region_filter; + } ); + $trainers = array_values( $trainers ); // Re-index array + } // Generate HTML table ob_start(); @@ -329,20 +339,20 @@ class HVAC_Master_Trainers_Overview { Name Email - Location Status Total Events + Revenue Registered Actions - + No trainers found matching your criteria. - + @@ -352,22 +362,22 @@ class HVAC_Master_Trainers_Overview { - - - - - + + + + $ + - + - View Profile @@ -379,14 +389,18 @@ class HVAC_Master_Trainers_Overview {
- Showing trainer(s) + Showing trainer(s) + + of total +
$html, - 'total_found' => count( $formatted_trainers ) + 'total_found' => count( $trainers ), + 'pagination' => $trainer_data['pagination'] ) ); } @@ -514,16 +528,35 @@ class HVAC_Master_Trainers_Overview { /** * Count trainers by status + * + * Uses HVAC_Trainer_Status for dynamic status calculation (active/inactive based on event activity) */ private function count_trainers_by_status( $status ) { + // Load trainer status class for dynamic status calculation + if ( ! class_exists( 'HVAC_Trainer_Status' ) ) { + require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-trainer-status.php'; + } + + // Get all trainers (both roles) $users = get_users( array( - 'role' => 'hvac_trainer', - 'meta_key' => 'hvac_trainer_status', - 'meta_value' => $status, - 'count_total' => true + 'role__in' => array( 'hvac_trainer', 'hvac_master_trainer' ), + 'fields' => 'ID' ) ); - - return is_array( $users ) ? count( $users ) : 0; + + if ( empty( $users ) ) { + return 0; + } + + // Count by dynamic status + $count = 0; + foreach ( $users as $user_id ) { + $user_status = HVAC_Trainer_Status::get_trainer_status( $user_id ); + if ( $user_status === $status ) { + $count++; + } + } + + return $count; } /** diff --git a/includes/class-hvac-venue-categories.php b/includes/class-hvac-venue-categories.php index b5180f78..5be04e99 100644 --- a/includes/class-hvac-venue-categories.php +++ b/includes/class-hvac-venue-categories.php @@ -45,8 +45,15 @@ class HVAC_Venue_Categories { * Constructor */ private function __construct() { - add_action('init', [$this, 'register_taxonomies'], 5); - add_action('init', [$this, 'create_default_terms'], 10); + // If init has already fired (we're being initialized late), register immediately + // This handles the case where the class is instantiated during 'init' priority >= 5 + if (did_action('init')) { + $this->register_taxonomies(); + $this->create_default_terms(); + } else { + add_action('init', [$this, 'register_taxonomies'], 5); + add_action('init', [$this, 'create_default_terms'], 10); + } } /** diff --git a/includes/find-trainer/class-hvac-mapgeo-integration.php b/includes/find-trainer/class-hvac-mapgeo-integration.php index 1baa42a2..bdbc575d 100644 --- a/includes/find-trainer/class-hvac-mapgeo-integration.php +++ b/includes/find-trainer/class-hvac-mapgeo-integration.php @@ -99,7 +99,11 @@ class HVAC_MapGeo_Integration { add_action('wp_ajax_hvac_search_trainers', [$this, 'ajax_search_trainers']); add_action('wp_ajax_nopriv_hvac_search_trainers', [$this, 'ajax_search_trainers']); - // Add JavaScript to handle MapGeo marker clicks - Priority 0 to ensure interceptor runs before localization + // Add JavaScript to handle MapGeo marker clicks - MUST run in wp_head BEFORE shortcode renders + // The IGM plugin outputs iMapsData during shortcode execution, so interceptor must be installed first + add_action('wp_head', [$this, 'add_mapgeo_interceptor'], 1); + + // Add click handlers in footer (these run after map initializes) add_action('wp_footer', [$this, 'add_mapgeo_click_handlers'], 0); } @@ -400,8 +404,84 @@ class HVAC_MapGeo_Integration { /** - * Add JavaScript to handle MapGeo custom click actions + * Add JavaScript interceptor in wp_head BEFORE shortcode renders + * This MUST run before IGM plugin outputs iMapsData */ + public function add_mapgeo_interceptor() { + // Only add on find trainer page + if (!is_page() || get_post_field('post_name') !== 'find-a-trainer') { + return; + } + + ?> + +