Event data cache */ private SplObjectStorage $eventCache; /** * @var ?int Current event ID being edited */ private ?int $currentEventId = null; /** * Constructor with property promotion */ private function __construct() { $this->eventCache = new SplObjectStorage(); $this->initHooks(); } /** * Initialize WordPress hooks */ private function initHooks(): void { add_action('init', [$this, 'registerRewriteRules']); add_action('template_redirect', [$this, 'handleFormSubmission']); add_filter('query_vars', [$this, 'addQueryVars']); // Single template_include hook at appropriate priority add_filter('template_include', [$this, 'loadTemplate'], 1000); add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']); } /** * Register rewrite rules for edit URL */ public function registerRewriteRules(): void { add_rewrite_rule( '^trainer/event/edit/?$', 'index.php?hvac_event_edit=1', 'top' ); } /** * Add custom query vars */ public function addQueryVars(array $vars): array { $vars[] = 'hvac_event_edit'; return $vars; } /** * Load custom template for event edit page */ public function loadTemplate(string $template): string { // Check if we're on the custom edit page $is_edit_page = false; $request_uri = $_SERVER['REQUEST_URI'] ?? ''; // Method 1: Check by page ID (configuration-based approach) if (defined('HVAC_EVENT_EDIT_PAGE_ID') && is_page(HVAC_EVENT_EDIT_PAGE_ID)) { $is_edit_page = true; } // Method 2: Check URL path elseif (strpos($request_uri, '/trainer/event/edit') !== false) { $is_edit_page = true; } // Method 3: Check query var elseif (get_query_var('hvac_event_edit') === '1') { $is_edit_page = true; } if ($is_edit_page) { $custom_template = HVAC_PLUGIN_DIR . 'templates/page-edit-event-custom.php'; if (file_exists($custom_template)) { return $custom_template; } } return $template; } /** * Enqueue minimal required assets (no JavaScript!) */ public function enqueueAssets(): void { if (!$this->isEditPage()) { return; } wp_enqueue_style( 'hvac-event-edit-custom', HVAC_PLUGIN_URL . 'assets/css/hvac-event-edit-custom.css', [], HVAC_PLUGIN_VERSION ); } /** * Check if current page is event edit page */ private function isEditPage(): bool { return get_query_var('hvac_event_edit') === '1' || (is_page() && get_page_template_slug() === 'templates/page-edit-event-custom.php'); } /** * Get event data using generator for memory efficiency * * @return Generator */ public function getEventData(int $eventId): Generator { // Check cache first $cacheKey = "event_data_{$eventId}"; $cached = wp_cache_get($cacheKey, self::CACHE_GROUP); if ($cached !== false && is_array($cached)) { foreach ($cached as $key => $value) { yield $key => $value; } return; } $event = get_post($eventId); if (!$event || $event->post_type !== 'tribe_events') { throw new InvalidArgumentException("Invalid event ID: {$eventId}"); } // Core data yield 'id' => $eventId; yield 'title' => $event->post_title; yield 'content' => $event->post_content; yield 'excerpt' => $event->post_excerpt; yield 'status' => $event->post_status; yield 'author' => $event->post_author; // Meta data - lazy load as needed $metaKeys = $this->getEventMetaKeys(); foreach ($metaKeys as $key) { yield $key => get_post_meta($eventId, $key, true); } // Venue data $venueId = get_post_meta($eventId, '_EventVenueID', true); if ($venueId) { yield 'venue' => $this->getVenueData((int) $venueId); } // Organizer data $organizerId = get_post_meta($eventId, '_EventOrganizerID', true); if ($organizerId) { yield 'organizer' => $this->getOrganizerData((int) $organizerId); } // Taxonomies yield 'categories' => wp_get_post_terms($eventId, 'tribe_events_cat', ['fields' => 'ids']); yield 'tags' => wp_get_post_terms($eventId, 'post_tag', ['fields' => 'ids']); // Cache the data for future use (convert generator to array) // Note: This is done after yielding to maintain generator efficiency // The next call will use the cached version $dataToCache = []; $dataToCache['id'] = $eventId; $dataToCache['title'] = $event->post_title; $dataToCache['content'] = $event->post_content; $dataToCache['excerpt'] = $event->post_excerpt; $dataToCache['status'] = $event->post_status; $dataToCache['author'] = $event->post_author; foreach ($this->getEventMetaKeys() as $key) { $dataToCache[$key] = get_post_meta($eventId, $key, true); } if ($venueId) { $dataToCache['venue'] = $this->getVenueData((int) $venueId); } if ($organizerId) { $dataToCache['organizer'] = $this->getOrganizerData((int) $organizerId); } $dataToCache['categories'] = wp_get_post_terms($eventId, 'tribe_events_cat', ['fields' => 'ids']); $dataToCache['tags'] = wp_get_post_terms($eventId, 'post_tag', ['fields' => 'ids']); wp_cache_set($cacheKey, $dataToCache, self::CACHE_GROUP, self::CACHE_TTL); } /** * Get TEC event meta keys */ private function getEventMetaKeys(): array { return [ '_EventStartDate', '_EventEndDate', '_EventStartDateUTC', '_EventEndDateUTC', '_EventTimezone', '_EventTimezoneAbbr', '_EventAllDay', '_EventDuration', '_EventCost', '_EventCurrencySymbol', '_EventCurrencyCode', '_EventURL', '_EventShowMap', '_EventShowMapLink', ]; } /** * Get venue data efficiently */ private function getVenueData(int $venueId): ArrayObject { $venue = get_post($venueId); if (!$venue || $venue->post_type !== 'tribe_venue') { return new ArrayObject(); } return new ArrayObject([ 'id' => $venueId, 'title' => $venue->post_title, 'address' => get_post_meta($venueId, '_VenueAddress', true), 'city' => get_post_meta($venueId, '_VenueCity', true), 'state' => get_post_meta($venueId, '_VenueStateProvince', true), 'zip' => get_post_meta($venueId, '_VenueZip', true), 'country' => get_post_meta($venueId, '_VenueCountry', true), 'phone' => get_post_meta($venueId, '_VenuePhone', true), 'url' => get_post_meta($venueId, '_VenueURL', true), ], ArrayObject::ARRAY_AS_PROPS); } /** * Get organizer data efficiently */ private function getOrganizerData(int $organizerId): ArrayObject { $organizer = get_post($organizerId); if (!$organizer || $organizer->post_type !== 'tribe_organizer') { return new ArrayObject(); } return new ArrayObject([ 'id' => $organizerId, 'title' => $organizer->post_title, 'phone' => get_post_meta($organizerId, '_OrganizerPhone', true), 'website' => get_post_meta($organizerId, '_OrganizerWebsite', true), 'email' => get_post_meta($organizerId, '_OrganizerEmail', true), ], ArrayObject::ARRAY_AS_PROPS); } /** * Handle form submission with comprehensive validation */ public function handleFormSubmission(): void { if (!$this->isEditPage() || $_SERVER['REQUEST_METHOD'] !== 'POST') { return; } // Verify nonce if (!isset($_POST[self::NONCE_FIELD]) || !wp_verify_nonce($_POST[self::NONCE_FIELD], self::NONCE_ACTION)) { wp_die('Security check failed'); } // Check capabilities $eventId = isset($_POST['event_id']) ? (int) $_POST['event_id'] : 0; if (!$this->canUserEditEvent($eventId)) { wp_die('Insufficient permissions'); } try { $eventId = $this->saveEvent($_POST); // Clear cache wp_cache_delete("event_data_{$eventId}", self::CACHE_GROUP); // Redirect with success message wp_safe_redirect(add_query_arg([ 'event_id' => $eventId, 'updated' => 'true' ], home_url('/trainer/event/edit/'))); exit; } catch (Exception $e) { // Log error and show message error_log('Event save error: ' . $e->getMessage()); wp_die('Error saving event: ' . esc_html($e->getMessage())); } } /** * Save event with validation and sanitization */ private function saveEvent(array $data): int { $eventId = isset($data['event_id']) ? (int) $data['event_id'] : 0; // Prepare post data $postData = [ 'ID' => $eventId, 'post_type' => 'tribe_events', 'post_title' => sanitize_text_field($data['post_title'] ?? ''), 'post_content' => wp_kses_post($data['post_content'] ?? ''), 'post_excerpt' => sanitize_textarea_field($data['post_excerpt'] ?? ''), 'post_status' => in_array($data['post_status'] ?? '', ['publish', 'draft']) ? $data['post_status'] : 'draft', ]; // Validate required fields if (empty($postData['post_title'])) { throw new InvalidArgumentException('Event title is required'); } // Insert or update if ($eventId > 0) { $result = wp_update_post($postData, true); } else { $postData['post_author'] = get_current_user_id(); $result = wp_insert_post($postData, true); } if (is_wp_error($result)) { throw new RuntimeException($result->get_error_message()); } $eventId = (int) $result; // Update meta fields $this->updateEventMeta($eventId, $data); // Update venue $this->updateVenue($eventId, $data); // Update organizer $this->updateOrganizer($eventId, $data); // Update taxonomies if (isset($data['event_categories'])) { wp_set_post_terms($eventId, array_map('intval', $data['event_categories']), 'tribe_events_cat'); } return $eventId; } /** * Update event meta fields with validation */ private function updateEventMeta(int $eventId, array $data): void { // Date/time fields $startDate = $this->parseDateTime($data['EventStartDate'] ?? '', $data['EventStartTime'] ?? ''); $endDate = $this->parseDateTime($data['EventEndDate'] ?? '', $data['EventEndTime'] ?? ''); if ($startDate && $endDate) { if ($endDate < $startDate) { throw new InvalidArgumentException('End date must be after start date'); } update_post_meta($eventId, '_EventStartDate', $startDate->format('Y-m-d H:i:s')); update_post_meta($eventId, '_EventEndDate', $endDate->format('Y-m-d H:i:s')); update_post_meta($eventId, '_EventStartDateUTC', get_gmt_from_date($startDate->format('Y-m-d H:i:s'))); update_post_meta($eventId, '_EventEndDateUTC', get_gmt_from_date($endDate->format('Y-m-d H:i:s'))); update_post_meta($eventId, '_EventDuration', $endDate->getTimestamp() - $startDate->getTimestamp()); } // Other meta fields $metaFields = [ '_EventAllDay' => isset($data['EventAllDay']) ? '1' : '0', '_EventCost' => sanitize_text_field($data['EventCost'] ?? ''), '_EventCurrencySymbol' => sanitize_text_field($data['EventCurrencySymbol'] ?? '$'), '_EventURL' => esc_url_raw($data['EventURL'] ?? ''), '_EventShowMap' => isset($data['EventShowMap']) ? '1' : '0', '_EventShowMapLink' => isset($data['EventShowMapLink']) ? '1' : '0', '_EventTimezone' => sanitize_text_field($data['EventTimezone'] ?? get_option('timezone_string')), ]; foreach ($metaFields as $key => $value) { update_post_meta($eventId, $key, $value); } } /** * Parse date and time into DateTime object */ private function parseDateTime(string $date, string $time): ?DateTime { if (empty($date)) { return null; } $dateTimeStr = $date; if (!empty($time)) { $dateTimeStr .= ' ' . $time; } else { $dateTimeStr .= ' 00:00:00'; } try { return new DateTime($dateTimeStr, wp_timezone()); } catch (Exception $e) { return null; } } /** * Update or create venue */ private function updateVenue(int $eventId, array $data): void { $venueId = isset($data['venue_id']) ? (int) $data['venue_id'] : 0; // Use existing venue if ($venueId > 0 && isset($data['use_existing_venue'])) { update_post_meta($eventId, '_EventVenueID', $venueId); return; } // Create new venue if data provided if (!empty($data['venue_name'])) { $venueData = [ 'post_type' => 'tribe_venue', 'post_title' => sanitize_text_field($data['venue_name']), 'post_status' => 'publish', ]; if ($venueId > 0) { $venueData['ID'] = $venueId; $venueId = wp_update_post($venueData); } else { $venueId = wp_insert_post($venueData); } if ($venueId && !is_wp_error($venueId)) { // Update venue meta update_post_meta($venueId, '_VenueAddress', sanitize_text_field($data['venue_address'] ?? '')); update_post_meta($venueId, '_VenueCity', sanitize_text_field($data['venue_city'] ?? '')); update_post_meta($venueId, '_VenueStateProvince', sanitize_text_field($data['venue_state'] ?? '')); update_post_meta($venueId, '_VenueZip', sanitize_text_field($data['venue_zip'] ?? '')); update_post_meta($venueId, '_VenueCountry', sanitize_text_field($data['venue_country'] ?? '')); update_post_meta($venueId, '_VenuePhone', sanitize_text_field($data['venue_phone'] ?? '')); update_post_meta($venueId, '_VenueURL', esc_url_raw($data['venue_url'] ?? '')); update_post_meta($eventId, '_EventVenueID', $venueId); } } } /** * Update or create organizer */ private function updateOrganizer(int $eventId, array $data): void { $organizerId = isset($data['organizer_id']) ? (int) $data['organizer_id'] : 0; // Use existing organizer if ($organizerId > 0 && isset($data['use_existing_organizer'])) { update_post_meta($eventId, '_EventOrganizerID', $organizerId); return; } // Create new organizer if data provided if (!empty($data['organizer_name'])) { $organizerData = [ 'post_type' => 'tribe_organizer', 'post_title' => sanitize_text_field($data['organizer_name']), 'post_status' => 'publish', ]; if ($organizerId > 0) { $organizerData['ID'] = $organizerId; $organizerId = wp_update_post($organizerData); } else { $organizerId = wp_insert_post($organizerData); } if ($organizerId && !is_wp_error($organizerId)) { // Update organizer meta update_post_meta($organizerId, '_OrganizerPhone', sanitize_text_field($data['organizer_phone'] ?? '')); update_post_meta($organizerId, '_OrganizerWebsite', esc_url_raw($data['organizer_website'] ?? '')); update_post_meta($organizerId, '_OrganizerEmail', sanitize_email($data['organizer_email'] ?? '')); update_post_meta($eventId, '_EventOrganizerID', $organizerId); } } } /** * Check if user can edit event */ public function canUserEditEvent(int $eventId): bool { if (!is_user_logged_in()) { return false; } $user = wp_get_current_user(); $userId = $user->ID; // New event - check if user has the capability to create events if ($eventId === 0) { // Check for The Events Calendar capabilities or trainer role return current_user_can('edit_tribe_events') || current_user_can('publish_tribe_events') || in_array('hvac_trainer', $user->roles) || in_array('hvac_master_trainer', $user->roles) || in_array('administrator', $user->roles); } // Existing event - validate ownership and permissions $event = get_post($eventId); if (!$event || $event->post_type !== 'tribe_events') { return false; } // Check ownership FIRST - owners can always edit their own events if ($event->post_author == $userId) { return true; } // Administrators can edit any event if (in_array('administrator', $user->roles)) { return true; } // Master trainers can edit any event if (in_array('hvac_master_trainer', $user->roles)) { return true; } // Non-owners need special permissions to edit others' events return current_user_can('edit_others_tribe_events') || current_user_can('edit_others_posts'); } /** * Get all venues for dropdown (using generator) * * @return Generator */ public function getVenuesForDropdown(): Generator { $venues = get_posts([ 'post_type' => 'tribe_venue', 'posts_per_page' => 100, // Limit to prevent memory issues 'orderby' => 'title', 'order' => 'ASC', 'author' => get_current_user_id(), ]); foreach ($venues as $venue) { yield $venue->ID => $venue->post_title; } } /** * Get all organizers for dropdown (using generator) * * @return Generator */ public function getOrganizersForDropdown(): Generator { $organizers = get_posts([ 'post_type' => 'tribe_organizer', 'posts_per_page' => 100, // Limit to prevent memory issues 'orderby' => 'title', 'order' => 'ASC', 'author' => get_current_user_id(), ]); foreach ($organizers as $organizer) { yield $organizer->ID => $organizer->post_title; } } /** * Get event categories for checkboxes * * @return Generator */ public function getCategoriesForCheckboxes(): Generator { $categories = get_terms([ 'taxonomy' => 'tribe_events_cat', 'hide_empty' => false, ]); if (!is_wp_error($categories)) { foreach ($categories as $category) { yield $category->term_id => $category->name; } } } } /** * Singleton trait for memory efficiency */ trait HVAC_Singleton_Trait { private static ?self $instance = null; public static function instance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } private function __clone() {} public function __wakeup() { throw new Exception("Cannot unserialize singleton"); } }