api = $api; $this->table = $wpdb->prefix . 'ticket_tailor_events'; $this->cache_duration = get_option('ticket_tailor_cache_duration', 3600); // 1 hour default } /** * Get all events (from cache or API) - PERFORMANCE FIX: Added limit parameter */ public function get_events($force_refresh = false, $limit = null) { if ($force_refresh) { return $this->sync_all_events(); } global $wpdb; // Check if cache is still valid $cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration); // Build query with optional limit $query = $wpdb->prepare( "SELECT event_data FROM {$this->table} WHERE last_synced > %s ORDER BY last_synced DESC", $cache_valid_time ); // PERFORMANCE FIX: Add LIMIT if specified if ($limit !== null && is_numeric($limit)) { $query .= $wpdb->prepare(" LIMIT %d", absint($limit)); } $cached_events = $wpdb->get_results($query); if (!empty($cached_events)) { $events = array(); foreach ($cached_events as $row) { $event_data = json_decode($row->event_data, true); if ($event_data) { $events[] = $event_data; } } return $events; } // Cache is expired or empty, fetch from API $all_events = $this->sync_all_events(); // Apply limit to synced events if specified if ($limit !== null && is_array($all_events)) { return array_slice($all_events, 0, absint($limit)); } return $all_events; } /** * Get single event */ public function get_event($event_id, $force_refresh = false) { $event_id = sanitize_text_field($event_id); if (!$force_refresh) { global $wpdb; $cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration); $cached = $wpdb->get_var( $wpdb->prepare( "SELECT event_data FROM {$this->table} WHERE event_id = %s AND last_synced > %s", $event_id, $cache_valid_time ) ); if ($cached) { $event_data = json_decode($cached, true); if ($event_data) { return $event_data; } } } // Fetch from API $event = $this->api->get_event($event_id); if (is_wp_error($event)) { return $event; } // Cache it $this->cache_event($event); return $event; } /** * Sync all events from API - ENTERPRISE: Race condition protection */ public function sync_all_events() { if (!$this->api->is_configured()) { return new WP_Error('no_api_key', __('API key not configured', 'ticket-tailor')); } // ENTERPRISE: Acquire lock to prevent concurrent syncs $lock_key = 'ticket_tailor_sync_events_lock'; $lock_acquired = $this->acquire_sync_lock($lock_key); if (!$lock_acquired) { return new WP_Error( 'sync_in_progress', __('Event sync already in progress. Please wait.', 'ticket-tailor'), array('lock_key' => $lock_key) ); } try { $events = $this->api->get_all_paginated('events'); if (is_wp_error($events)) { $this->release_sync_lock($lock_key); return $events; } // ENTERPRISE: Use transaction for atomic cache updates global $wpdb; $wpdb->query('START TRANSACTION'); try { // Cache all events $cached_count = 0; foreach ($events as $event) { if ($this->cache_event($event)) { $cached_count++; } } // Update last sync time update_option('ticket_tailor_last_event_sync', time()); $wpdb->query('COMMIT'); error_log(sprintf( 'Ticket Tailor: Successfully synced %d events (%d cached)', count($events), $cached_count )); } catch (Exception $e) { $wpdb->query('ROLLBACK'); $this->release_sync_lock($lock_key); return new WP_Error( 'sync_transaction_failed', sprintf('Event sync transaction failed: %s', $e->getMessage()) ); } $this->release_sync_lock($lock_key); return $events; } catch (Exception $e) { $this->release_sync_lock($lock_key); return new WP_Error( 'sync_failed', sprintf('Event sync failed: %s', $e->getMessage()) ); } } /** * Acquire sync lock - ENTERPRISE: Prevent concurrent syncs */ private function acquire_sync_lock($lock_key, $timeout = 300) { // Try to set transient with 5-minute expiry // If it already exists, returns false $acquired = set_transient($lock_key, time(), $timeout); if (!$acquired) { // Check if lock is stale (older than timeout) $lock_time = get_transient($lock_key); if ($lock_time && (time() - $lock_time) > $timeout) { // Stale lock, force release and reacquire delete_transient($lock_key); return set_transient($lock_key, time(), $timeout); } return false; } return true; } /** * Release sync lock - ENTERPRISE */ private function release_sync_lock($lock_key) { return delete_transient($lock_key); } /** * Cache a single event */ private function cache_event($event) { if (empty($event['id'])) { return false; } global $wpdb; $event_id = sanitize_text_field($event['id']); $event_data = wp_json_encode($event); // Check if exists $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$this->table} WHERE event_id = %s", $event_id ) ); if ($exists) { // Update existing return $wpdb->update( $this->table, array( 'event_data' => $event_data, 'last_synced' => current_time('mysql', 1), ), array('event_id' => $event_id), array('%s', '%s'), array('%s') ); } else { // Insert new return $wpdb->insert( $this->table, array( 'event_id' => $event_id, 'event_data' => $event_data, 'last_synced' => current_time('mysql', 1), ), array('%s', '%s', '%s') ); } } /** * Get upcoming events */ public function get_upcoming_events($limit = 10) { $events = $this->get_events(); if (is_wp_error($events)) { return $events; } // Filter and sort by start date $upcoming = array_filter($events, function($event) { if (!isset($event['start']['iso'])) { return false; } $start_time = strtotime($event['start']['iso']); return $start_time >= time(); }); // Sort by start date usort($upcoming, function($a, $b) { $time_a = strtotime($a['start']['iso'] ?? 0); $time_b = strtotime($b['start']['iso'] ?? 0); return $time_a - $time_b; }); return array_slice($upcoming, 0, $limit); } /** * Get past events */ public function get_past_events($limit = 10) { $events = $this->get_events(); if (is_wp_error($events)) { return $events; } // Filter and sort by start date $past = array_filter($events, function($event) { if (!isset($event['start']['iso'])) { return false; } $start_time = strtotime($event['start']['iso']); return $start_time < time(); }); // Sort by start date (newest first) usort($past, function($a, $b) { $time_a = strtotime($a['start']['iso'] ?? 0); $time_b = strtotime($b['start']['iso'] ?? 0); return $time_b - $time_a; }); return array_slice($past, 0, $limit); } /** * Search events */ public function search_events($search_term) { $events = $this->get_events(); if (is_wp_error($events)) { return $events; } $search_term = strtolower($search_term); return array_filter($events, function($event) use ($search_term) { $name = strtolower($event['name'] ?? ''); $description = strtolower($event['description'] ?? ''); return strpos($name, $search_term) !== false || strpos($description, $search_term) !== false; }); } /** * Get events by category */ public function get_events_by_category($category) { $events = $this->get_events(); if (is_wp_error($events)) { return $events; } $category = strtolower($category); return array_filter($events, function($event) use ($category) { $event_category = strtolower($event['category'] ?? ''); return $event_category === $category; }); } /** * Get event ticket types */ public function get_event_tickets($event_id) { $event_id = sanitize_text_field($event_id); // Try to get from transient first $cache_key = 'tt_tickets_' . $event_id; $cached = get_transient($cache_key); if ($cached !== false) { return $cached; } // Fetch from API $tickets = $this->api->get_ticket_types($event_id); if (is_wp_error($tickets)) { return $tickets; } // Cache for 15 minutes (tickets change less frequently) set_transient($cache_key, $tickets, 900); return $tickets; } /** * Clear event cache - SECURITY FIX: Use prepared statements */ public function clear_cache($event_id = null) { global $wpdb; if ($event_id) { $event_id = sanitize_text_field($event_id); $wpdb->delete($this->table, array('event_id' => $event_id), array('%s')); // Also clear ticket cache delete_transient('tt_tickets_' . $event_id); } else { // SECURITY FIX: Validate table name before TRUNCATE if (preg_match('/^[a-zA-Z0-9_]+$/', $this->table)) { $wpdb->query("TRUNCATE TABLE `{$this->table}`"); } // Clear all ticket transients - SECURITY FIX: Use prepared statement $wpdb->query($wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like('_transient_tt_tickets_') . '%' )); } return true; } /** * Get cache statistics */ public function get_cache_stats() { global $wpdb; $total_events = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table}"); $cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration); $valid_events = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s", $cache_valid_time ) ); $last_sync = get_option('ticket_tailor_last_event_sync', 0); return array( 'total_cached' => (int) $total_events, 'valid_cached' => (int) $valid_events, 'expired_cached' => (int) $total_events - (int) $valid_events, 'last_sync' => $last_sync, 'cache_duration' => $this->cache_duration, ); } /** * Get event statistics efficiently - PERFORMANCE OPTIMIZATION * Uses aggregated database queries instead of loading all events */ public function get_event_statistics() { global $wpdb; $cache_key = 'tt_event_stats'; $cached = wp_cache_get($cache_key); if ($cached !== false) { return $cached; } $cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration); // Get total events count $total_events = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s", $cache_valid_time ) ); // Get upcoming events count (parse JSON to check dates) $all_events = $wpdb->get_results( $wpdb->prepare( "SELECT event_data FROM {$this->table} WHERE last_synced > %s", $cache_valid_time ) ); $upcoming_count = 0; $now = time(); foreach ($all_events as $row) { $event = json_decode($row->event_data, true); if (!empty($event['start']['iso'])) { $start_time = strtotime($event['start']['iso']); if ($start_time >= $now) { $upcoming_count++; } } } $stats = array( 'total_events' => (int) $total_events, 'upcoming_events' => $upcoming_count, ); // Cache for 5 minutes wp_cache_set($cache_key, $stats, '', 300); return $stats; } }