- Changed headquarters country and state fields from text inputs to dropdown selections - Added dynamic state/province loading based on selected country (US/Canada) - Added 'Other' option for non-US/Canada countries with text input fallback - Properly handle org_headquarters_state_other field in backend processing - JavaScript handlers for dynamic country/state interaction - Consistent with Training Venue Information dropdown behavior Co-Authored-By: Ben Reed <ben@tealmaker.com>
		
			
				
	
	
		
			503 lines
		
	
	
		
			No EOL
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			503 lines
		
	
	
		
			No EOL
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * HVAC Database Query Monitor
 | |
|  * 
 | |
|  * Monitors and optimizes database queries for performance issues
 | |
|  * 
 | |
|  * @package HVAC_Community_Events
 | |
|  * @since 1.0.7
 | |
|  */
 | |
| 
 | |
| if (!defined('ABSPATH')) {
 | |
|     exit;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * HVAC_Query_Monitor class
 | |
|  */
 | |
| class HVAC_Query_Monitor {
 | |
|     
 | |
|     /**
 | |
|      * Slow query threshold in seconds
 | |
|      */
 | |
|     const SLOW_QUERY_THRESHOLD = 0.1;
 | |
|     
 | |
|     /**
 | |
|      * Query log option name
 | |
|      */
 | |
|     const LOG_OPTION = 'hvac_query_log';
 | |
|     
 | |
|     /**
 | |
|      * Maximum log entries to keep
 | |
|      */
 | |
|     const MAX_LOG_ENTRIES = 100;
 | |
|     
 | |
|     /**
 | |
|      * Logged queries
 | |
|      * 
 | |
|      * @var array
 | |
|      */
 | |
|     private static $queries = [];
 | |
|     
 | |
|     /**
 | |
|      * Query statistics
 | |
|      * 
 | |
|      * @var array
 | |
|      */
 | |
|     private static $stats = [
 | |
|         'total_queries' => 0,
 | |
|         'slow_queries' => 0,
 | |
|         'total_time' => 0,
 | |
|         'peak_memory' => 0
 | |
|     ];
 | |
|     
 | |
|     /**
 | |
|      * Initialize monitoring
 | |
|      */
 | |
|     public static function init() {
 | |
|         // Only enable in development or when explicitly requested
 | |
|         if (!self::should_monitor()) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Hook into WordPress query system
 | |
|         add_filter('query', [__CLASS__, 'log_query'], 999);
 | |
|         add_action('shutdown', [__CLASS__, 'analyze_queries']);
 | |
|         
 | |
|         // Admin hooks
 | |
|         if (is_admin()) {
 | |
|             add_action('admin_menu', [__CLASS__, 'add_admin_page']);
 | |
|             add_action('wp_ajax_hvac_clear_query_log', [__CLASS__, 'ajax_clear_log']);
 | |
|         }
 | |
|         
 | |
|         // WP-CLI integration (disabled - method not implemented)
 | |
|         // if (defined('WP_CLI') && WP_CLI) {
 | |
|         //     WP_CLI::add_command('hvac query-monitor', [__CLASS__, 'wp_cli_commands']);
 | |
|         // }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check if monitoring should be enabled
 | |
|      * 
 | |
|      * @return bool
 | |
|      */
 | |
|     private static function should_monitor() {
 | |
|         // Enable if WP_DEBUG is on
 | |
|         if (defined('WP_DEBUG') && WP_DEBUG) {
 | |
|             return true;
 | |
|         }
 | |
|         
 | |
|         // Enable if explicitly requested
 | |
|         if (defined('HVAC_QUERY_MONITOR') && HVAC_QUERY_MONITOR) {
 | |
|             return true;
 | |
|         }
 | |
|         
 | |
|         // Enable for admin users with specific parameter
 | |
|         if (is_admin() && current_user_can('manage_options') && isset($_GET['hvac_monitor'])) {
 | |
|             return true;
 | |
|         }
 | |
|         
 | |
|         return false;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Log database query
 | |
|      * 
 | |
|      * @param string $query SQL query
 | |
|      * @return string Original query
 | |
|      */
 | |
|     public static function log_query($query) {
 | |
|         $start_time = microtime(true);
 | |
|         
 | |
|         // Get calling function information
 | |
|         $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
 | |
|         $caller_info = self::get_caller_info($backtrace);
 | |
|         
 | |
|         // Execute query timing logic by hooking into WordPress
 | |
|         add_action('wp_footer', function() use ($query, $start_time, $caller_info) {
 | |
|             $end_time = microtime(true);
 | |
|             $execution_time = $end_time - $start_time;
 | |
|             
 | |
|             self::record_query($query, $execution_time, $caller_info);
 | |
|         }, 999);
 | |
|         
 | |
|         return $query;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Record query information
 | |
|      * 
 | |
|      * @param string $query SQL query
 | |
|      * @param float $execution_time Query execution time
 | |
|      * @param array $caller_info Caller information
 | |
|      */
 | |
|     private static function record_query($query, $execution_time, $caller_info) {
 | |
|         // Skip if query is too common or not from our plugin
 | |
|         if (!self::should_log_query($query, $caller_info)) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         $is_slow = $execution_time > self::SLOW_QUERY_THRESHOLD;
 | |
|         
 | |
|         $query_data = [
 | |
|             'query' => self::sanitize_query($query),
 | |
|             'execution_time' => round($execution_time, 4),
 | |
|             'is_slow' => $is_slow,
 | |
|             'caller' => $caller_info,
 | |
|             'memory_usage' => memory_get_usage(true),
 | |
|             'timestamp' => time(),
 | |
|             'url' => $_SERVER['REQUEST_URI'] ?? 'unknown'
 | |
|         ];
 | |
|         
 | |
|         self::$queries[] = $query_data;
 | |
|         
 | |
|         // Update statistics
 | |
|         self::$stats['total_queries']++;
 | |
|         self::$stats['total_time'] += $execution_time;
 | |
|         self::$stats['peak_memory'] = max(self::$stats['peak_memory'], $query_data['memory_usage']);
 | |
|         
 | |
|         if ($is_slow) {
 | |
|             self::$stats['slow_queries']++;
 | |
|             
 | |
|             // Log slow queries immediately
 | |
|             HVAC_Logger::warning(
 | |
|                 "Slow query detected ({$execution_time}s): " . substr($query, 0, 200),
 | |
|                 'Query Monitor'
 | |
|             );
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get caller information from backtrace
 | |
|      * 
 | |
|      * @param array $backtrace Debug backtrace
 | |
|      * @return array Caller info
 | |
|      */
 | |
|     private static function get_caller_info($backtrace) {
 | |
|         foreach ($backtrace as $trace) {
 | |
|             $file = $trace['file'] ?? '';
 | |
|             $function = $trace['function'] ?? '';
 | |
|             
 | |
|             // Look for HVAC plugin functions
 | |
|             if (strpos($file, 'hvac-community-events') !== false) {
 | |
|                 return [
 | |
|                     'file' => basename($file),
 | |
|                     'line' => $trace['line'] ?? 0,
 | |
|                     'function' => $function,
 | |
|                     'class' => $trace['class'] ?? ''
 | |
|                 ];
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Fallback to first available trace
 | |
|         $first = $backtrace[0] ?? [];
 | |
|         return [
 | |
|             'file' => basename($first['file'] ?? 'unknown'),
 | |
|             'line' => $first['line'] ?? 0,
 | |
|             'function' => $first['function'] ?? 'unknown',
 | |
|             'class' => $first['class'] ?? ''
 | |
|         ];
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check if query should be logged
 | |
|      * 
 | |
|      * @param string $query SQL query
 | |
|      * @param array $caller_info Caller information
 | |
|      * @return bool
 | |
|      */
 | |
|     private static function should_log_query($query, $caller_info) {
 | |
|         // Only log queries from our plugin
 | |
|         $hvac_files = ['hvac-', 'HVAC_'];
 | |
|         $is_hvac_query = false;
 | |
|         
 | |
|         foreach ($hvac_files as $prefix) {
 | |
|             if (strpos($caller_info['file'], $prefix) !== false || 
 | |
|                 strpos($caller_info['class'], $prefix) !== false) {
 | |
|                 $is_hvac_query = true;
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         if (!$is_hvac_query) {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         // Skip common WordPress core queries
 | |
|         $skip_patterns = [
 | |
|             'SELECT option_value FROM',
 | |
|             'SELECT autoload FROM',
 | |
|             'SELECT meta_value FROM.*_usermeta WHERE meta_key = \'wp_',
 | |
|             'UPDATE.*_options SET option_value'
 | |
|         ];
 | |
|         
 | |
|         foreach ($skip_patterns as $pattern) {
 | |
|             if (preg_match('/' . $pattern . '/i', $query)) {
 | |
|                 return false;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         return true;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Sanitize query for logging
 | |
|      * 
 | |
|      * @param string $query SQL query
 | |
|      * @return string Sanitized query
 | |
|      */
 | |
|     private static function sanitize_query($query) {
 | |
|         // Remove potential sensitive data
 | |
|         $query = preg_replace('/\'[^\']*\'/', "'[DATA]'", $query);
 | |
|         $query = preg_replace('/\b\d+\b/', '[NUM]', $query);
 | |
|         
 | |
|         return trim($query);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Analyze queries on shutdown
 | |
|      */
 | |
|     public static function analyze_queries() {
 | |
|         if (empty(self::$queries)) {
 | |
|             return;
 | |
|         }
 | |
|         
 | |
|         // Store queries in log
 | |
|         self::store_query_log();
 | |
|         
 | |
|         // Generate recommendations
 | |
|         $recommendations = self::generate_recommendations();
 | |
|         
 | |
|         if (!empty($recommendations)) {
 | |
|             HVAC_Logger::info(
 | |
|                 'Query optimization recommendations: ' . implode('; ', $recommendations),
 | |
|                 'Query Monitor'
 | |
|             );
 | |
|         }
 | |
|         
 | |
|         // Add debug output for admin users
 | |
|         if (current_user_can('manage_options') && isset($_GET['hvac_debug_queries'])) {
 | |
|             self::output_debug_info();
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Store query log
 | |
|      */
 | |
|     private static function store_query_log() {
 | |
|         $existing_log = get_option(self::LOG_OPTION, []);
 | |
|         
 | |
|         // Add new queries
 | |
|         $new_log = array_merge($existing_log, self::$queries);
 | |
|         
 | |
|         // Keep only recent entries
 | |
|         $new_log = array_slice($new_log, -self::MAX_LOG_ENTRIES);
 | |
|         
 | |
|         update_option(self::LOG_OPTION, $new_log);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Generate optimization recommendations
 | |
|      * 
 | |
|      * @return array Recommendations
 | |
|      */
 | |
|     private static function generate_recommendations() {
 | |
|         $recommendations = [];
 | |
|         
 | |
|         // Check for slow queries
 | |
|         if (self::$stats['slow_queries'] > 0) {
 | |
|             $recommendations[] = "Found " . self::$stats['slow_queries'] . " slow queries - consider adding indexes or caching";
 | |
|         }
 | |
|         
 | |
|         // Check total query time
 | |
|         if (self::$stats['total_time'] > 1.0) {
 | |
|             $recommendations[] = "Total query time is high (" . round(self::$stats['total_time'], 2) . "s) - consider query optimization";
 | |
|         }
 | |
|         
 | |
|         // Check for duplicate queries
 | |
|         $query_counts = array_count_values(array_column(self::$queries, 'query'));
 | |
|         $duplicates = array_filter($query_counts, function($count) { return $count > 1; });
 | |
|         
 | |
|         if (!empty($duplicates)) {
 | |
|             $recommendations[] = "Found " . count($duplicates) . " duplicate query patterns - consider caching";
 | |
|         }
 | |
|         
 | |
|         // Check memory usage
 | |
|         if (self::$stats['peak_memory'] > 50 * 1024 * 1024) { // 50MB
 | |
|             $recommendations[] = "High memory usage detected - consider result set optimization";
 | |
|         }
 | |
|         
 | |
|         return $recommendations;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Output debug information
 | |
|      */
 | |
|     private static function output_debug_info() {
 | |
|         echo "\n<!-- HVAC Query Monitor Debug -->\n";
 | |
|         echo "<div style='position:fixed;bottom:0;left:0;background:#000;color:#fff;padding:10px;max-width:50%;max-height:300px;overflow:auto;z-index:9999;font-size:11px;'>";
 | |
|         echo "<h4>HVAC Query Monitor</h4>";
 | |
|         echo "<p>Total Queries: " . self::$stats['total_queries'] . " | ";
 | |
|         echo "Slow Queries: " . self::$stats['slow_queries'] . " | ";
 | |
|         echo "Total Time: " . round(self::$stats['total_time'], 3) . "s | ";
 | |
|         echo "Peak Memory: " . size_format(self::$stats['peak_memory']) . "</p>";
 | |
|         
 | |
|         if (!empty(self::$queries)) {
 | |
|             echo "<details><summary>Query Details</summary>";
 | |
|             foreach (array_slice(self::$queries, -10) as $query) {
 | |
|                 echo "<div style='margin:5px 0;padding:5px;background:#333;'>";
 | |
|                 echo "<strong>" . $query['execution_time'] . "s</strong> - ";
 | |
|                 echo htmlspecialchars(substr($query['query'], 0, 100)) . "...";
 | |
|                 echo "<br><small>Called from: " . $query['caller']['file'] . ":" . $query['caller']['line'] . "</small>";
 | |
|                 echo "</div>";
 | |
|             }
 | |
|             echo "</details>";
 | |
|         }
 | |
|         echo "</div>\n";
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Add admin page
 | |
|      */
 | |
|     public static function add_admin_page() {
 | |
|         if (current_user_can('manage_options')) {
 | |
|             add_management_page(
 | |
|                 'HVAC Query Monitor',
 | |
|                 'HVAC Query Monitor',
 | |
|                 'manage_options',
 | |
|                 'hvac-query-monitor',
 | |
|                 [__CLASS__, 'admin_page']
 | |
|             );
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Admin page content
 | |
|      */
 | |
|     public static function admin_page() {
 | |
|         $log = get_option(self::LOG_OPTION, []);
 | |
|         $recent_log = array_slice($log, -20);
 | |
|         
 | |
|         ?>
 | |
|         <div class="wrap">
 | |
|             <h1>HVAC Query Monitor</h1>
 | |
|             
 | |
|             <div class="card">
 | |
|                 <h3>Query Statistics</h3>
 | |
|                 <p>Total logged queries: <?php echo count($log); ?></p>
 | |
|                 
 | |
|                 <?php if (!empty($log)): ?>
 | |
|                     <?php 
 | |
|                     $slow_count = count(array_filter($log, function($q) { return $q['is_slow']; }));
 | |
|                     $avg_time = array_sum(array_column($log, 'execution_time')) / count($log);
 | |
|                     ?>
 | |
|                     <p>Slow queries: <?php echo $slow_count; ?></p>
 | |
|                     <p>Average execution time: <?php echo round($avg_time, 4); ?>s</p>
 | |
|                 <?php endif; ?>
 | |
|                 
 | |
|                 <p>
 | |
|                     <a href="?page=hvac-query-monitor&action=clear" class="button button-secondary">Clear Log</a>
 | |
|                     <a href="?hvac_debug_queries=1" class="button button-primary" target="_blank">Enable Debug Output</a>
 | |
|                 </p>
 | |
|             </div>
 | |
|             
 | |
|             <?php if (!empty($recent_log)): ?>
 | |
|             <div class="card">
 | |
|                 <h3>Recent Queries (Last 20)</h3>
 | |
|                 <table class="wp-list-table widefat fixed striped">
 | |
|                     <thead>
 | |
|                         <tr>
 | |
|                             <th>Time</th>
 | |
|                             <th>Duration</th>
 | |
|                             <th>Query</th>
 | |
|                             <th>Caller</th>
 | |
|                             <th>URL</th>
 | |
|                         </tr>
 | |
|                     </thead>
 | |
|                     <tbody>
 | |
|                         <?php foreach (array_reverse($recent_log) as $query): ?>
 | |
|                         <tr <?php echo $query['is_slow'] ? 'style="background:#ffebee;"' : ''; ?>>
 | |
|                             <td><?php echo date('H:i:s', $query['timestamp']); ?></td>
 | |
|                             <td><?php echo $query['execution_time']; ?>s</td>
 | |
|                             <td title="<?php echo esc_attr($query['query']); ?>">
 | |
|                                 <?php echo esc_html(substr($query['query'], 0, 80)) . '...'; ?>
 | |
|                             </td>
 | |
|                             <td><?php echo esc_html($query['caller']['file'] . ':' . $query['caller']['line']); ?></td>
 | |
|                             <td><?php echo esc_html($query['url']); ?></td>
 | |
|                         </tr>
 | |
|                         <?php endforeach; ?>
 | |
|                     </tbody>
 | |
|                 </table>
 | |
|             </div>
 | |
|             <?php endif; ?>
 | |
|         </div>
 | |
|         <?php
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * AJAX handler to clear log
 | |
|      */
 | |
|     public static function ajax_clear_log() {
 | |
|         if (!current_user_can('manage_options') || 
 | |
|             !check_ajax_referer('hvac_ajax_nonce', 'nonce', false)) {
 | |
|             wp_send_json_error('Unauthorized');
 | |
|         }
 | |
|         
 | |
|         delete_option(self::LOG_OPTION);
 | |
|         wp_send_json_success('Query log cleared');
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Get query statistics
 | |
|      * 
 | |
|      * @return array Statistics
 | |
|      */
 | |
|     public static function get_stats() {
 | |
|         $log = get_option(self::LOG_OPTION, []);
 | |
|         
 | |
|         if (empty($log)) {
 | |
|             return [
 | |
|                 'total_queries' => 0,
 | |
|                 'slow_queries' => 0,
 | |
|                 'average_time' => 0,
 | |
|                 'recommendations' => []
 | |
|             ];
 | |
|         }
 | |
|         
 | |
|         $slow_queries = array_filter($log, function($q) { return $q['is_slow']; });
 | |
|         $total_time = array_sum(array_column($log, 'execution_time'));
 | |
|         
 | |
|         return [
 | |
|             'total_queries' => count($log),
 | |
|             'slow_queries' => count($slow_queries),
 | |
|             'average_time' => $total_time / count($log),
 | |
|             'total_time' => $total_time,
 | |
|             'recommendations' => self::generate_recommendations_from_log($log)
 | |
|         ];
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Generate recommendations from stored log
 | |
|      * 
 | |
|      * @param array $log Query log
 | |
|      * @return array Recommendations
 | |
|      */
 | |
|     private static function generate_recommendations_from_log($log) {
 | |
|         $recommendations = [];
 | |
|         
 | |
|         // Analyze patterns in stored log
 | |
|         $query_patterns = [];
 | |
|         foreach ($log as $query_data) {
 | |
|             $pattern = preg_replace('/\[DATA\]|\[NUM\]/', 'X', $query_data['query']);
 | |
|             $query_patterns[$pattern] = ($query_patterns[$pattern] ?? 0) + 1;
 | |
|         }
 | |
|         
 | |
|         // Find frequently repeated queries
 | |
|         $frequent_queries = array_filter($query_patterns, function($count) { return $count > 5; });
 | |
|         if (!empty($frequent_queries)) {
 | |
|             $recommendations[] = "Consider caching for " . count($frequent_queries) . " frequently repeated queries";
 | |
|         }
 | |
|         
 | |
|         return $recommendations;
 | |
|     }
 | |
| }
 |