]*>.*?<\/\1>/is', '', $html ); $html = preg_replace( '/<(script|object|embed|link|style)\b[^>]*\/?>/i', '', $html ); $html = preg_replace( '/\s+on\w+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $html ); $html = preg_replace( '/(href|src)\s*=\s*["\']?\s*javascript:/i', '$1=""', $html ); $allowed = array( 'img' => array( 'src' => true, 'alt' => true, 'width' => true, 'height' => true, 'class' => true, 'loading' => true, ), 'iframe' => array( 'src' => true, 'width' => true, 'height' => true, 'class' => true, 'title' => true, 'allow' => true, 'allowfullscreen' => true, 'frameborder' => true, ), 'p' => array( 'class' => true, 'id' => true, ), 'div' => array( 'class' => true, 'id' => true, ), 'span' => array( 'class' => true ), 'br' => array(), 'strong' => array(), 'em' => array(), 'a' => array( 'href' => true, 'target' => true, 'rel' => true, 'class' => true, ), ); $html = wp_kses( $html, $allowed ); return agile_filter_feed_html_urls( $html ); } /** * Remove img/iframe/a tags whose URLs fail feed URL policy. * * @param string $html Kses-filtered HTML. * @return string */ function agile_filter_feed_html_urls( $html ) { if ( '' === $html ) { return ''; } $html = preg_replace_callback( '/<(img|iframe)\b[^>]*\bsrc\s*=\s*("|\')([^"\']+)\2[^>]*>/i', static function ( $matches ) { $tag = strtolower( $matches[1] ); $url = $matches[3]; if ( ! agile_is_allowed_feed_media_url( $url, $tag ) ) { return ''; } return $matches[0]; }, $html ); return preg_replace_callback( '/]*\bhref\s*=\s*("|\')([^"\']+)\1[^>]*>.*?<\/a>/is', static function ( $matches ) { if ( ! agile_is_allowed_feed_media_url( $matches[2], 'a' ) ) { return ''; } return $matches[0]; }, $html ); } /** * @param string $url URL from a feed HTML attribute. * @param string $tag img, iframe, or a. * @return bool */ function agile_is_allowed_feed_media_url( $url, $tag ) { $url = trim( (string) $url ); if ( '' === $url ) { return false; } $parsed = wp_parse_url( $url ); if ( empty( $parsed['scheme'] ) || ! in_array( strtolower( $parsed['scheme'] ), array( 'http', 'https' ), true ) ) { return false; } if ( empty( $parsed['host'] ) ) { return false; } $host = strtolower( $parsed['host'] ); if ( 'img' === $tag || 'a' === $tag ) { return true; } if ( 'iframe' !== $tag ) { return false; } $web_url = get_option( 'agile_field_webURL' ); if ( is_string( $web_url ) && '' !== $web_url ) { $web_parsed = wp_parse_url( esc_url_raw( $web_url ) ); if ( ! empty( $web_parsed['host'] ) && strcasecmp( $host, $web_parsed['host'] ) === 0 ) { return true; } } foreach ( array( 'agiletix.com', 'agileticketing.net', 'vimeo.com', 'youtube.com' ) as $suffix ) { if ( $host === $suffix || substr( $host, -strlen( $suffix ) - 1 ) === '.' . $suffix ) { return true; } } if ( in_array( $host, array( 'youtu.be', 'm.youtube.com' ), true ) ) { return true; } return false; } class agile_tix_post_creator { /** * Takes a single showing event and creates a tab containing the following showing specific info */ static function agile_create_showing_tab_content( $showing ) { // $executionTime = new ExecutionTime('agile_create_showing_tab_content'); // $executionTime->start(); $date = $showing['DisplayDate'] ?? ''; $BuyLink = $showing['LegacyPurchaseLink'] ?? ''; // $InfoLink = $showing['InfoLink'] ?? ''; $Venue = $showing['Venue'] ?? ''; $tabContent = '
'; if ( ! empty( $date ) ) { $format_date = wp_date( 'F j, Y g:i A', strtotime( $date ) ); $tabContent .= '

' . $format_date . '

'; } if ( ! empty( $BuyLink ) ) { $tabContent .= 'Buy Tickets
'; } if ( ! empty( $Venue ) ) { $name = $Venue['Name'] ?? ''; $Address1 = $Venue['Address1'] ?? ''; $Address2 = $Venue['Address2'] ?? ''; $City = $Venue['City'] ?? ''; $State = $Venue['State'] ?? ''; $Zip = $Venue['Zip'] ?? ''; $tabContent .= '

' . $name . ' ' . $Address1 . ' ' . $Address2 . ' ' . $City . ' ' . $State . ' ' . $Zip . '

'; } $tabContent .= '
'; // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); return $tabContent; } static function add_image_to_media_library_from_uri( $image_url ) { // agile_tix::agile_tix_log_event("add_image_to_media_library_from_uri begin"); // $executionTime = new ExecutionTime('add_image_to_media_library_from_uri'); // $executionTime->start(); if ( empty( $image_url ) ) { return; // new WP_Error('invalid_url', 'The URL is invalid.'); } $exists = self::check_image_exists_in_media_library( $image_url ); // $validatorExists = json_encode($exists); // $downURLExists = function_exists("download_url"); // agile_tix::agile_tix_log_event("add_image_to_media_library_from_uri flag# 1 - " .$exists . " - " . $downURLExists); if ( $exists == false && function_exists( 'download_url' ) && function_exists( 'media_handle_sideload' ) ) { // agile_tix::agile_tix_log_event("add_image_to_media_library_from_uri flag# 1.5"); $tmp = download_url( $image_url ); if ( is_wp_error( $tmp ) ) { agile_tix::agile_tix_log_event( 'add_image_to_media_library_from_uri download_url failed: ' . $tmp->get_error_message() . ' - ' . $image_url, agile_tix::LOG_VERBOSE ); return false; } // Prepare the file array for media_handle_sideload $file_array = array( 'name' => basename( $image_url ), 'tmp_name' => $tmp, ); // agile_tix::agile_tix_log_event("add_image_to_media_library_from_uri flag# 1.7"); // Upload the image to the media library // Double-check function exists before calling (safety check for Action Scheduler context) if ( ! function_exists( 'media_handle_sideload' ) ) { require_once ABSPATH . 'wp-admin/includes/media.php'; } $attachment_id = media_handle_sideload( $file_array, 0 ); // 0 as the parent ID (media library without a specific post) // agile_tix::agile_tix_log_event("add_image_to_media_library_from_uri flag# 2"); if ( is_wp_error( $attachment_id ) ) { agile_tix::agile_tix_log_event( 'add_image_to_media_library_from_uri media_handle_sideload failed: ' . $attachment_id->get_error_message() . ' - ' . $image_url, agile_tix::LOG_VERBOSE ); wp_delete_file( $file_array['tmp_name'] ); return $attachment_id; } $attachment_url = wp_get_attachment_url( $attachment_id ); // agile_tix::agile_tix_log_event("add_image_to_media_library_from_uri flag# 3"); // add the files hash to its meta data so we can more quickly query against it to avoid saving duplicates // $image_hash = $image_url; // mb_convert_encoding($file_path, 'UTF-8', mb_detect_encoding($file_path, 'UTF-8, ISO-8859-1, ISO-8859-15', true)) update_post_meta( $attachment_id, 'ATS_image_URL', $image_url ); // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); // agile_tix::agile_tix_log_event("add_image_to_media_library_from_uri end exists"); return array( 'attachment_id' => $attachment_id, 'url' => $attachment_url, ); } else { // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); // agile_tix::agile_tix_log_event("add_image_to_media_library_from_uri end else"); return $exists;// if $exists isnt false, its the img URL } } static function check_image_exists_in_media_library( $image_URL ) { global $wpdb; // Query the database to check for the file name in the media library $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'ATS_image_URL' AND meta_value = %s LIMIT 1", $image_URL ) ); if ( $post_id != null ) { $attachment_url = wp_get_attachment_url( $post_id ); return array( 'attachment_id' => $post_id, 'url' => $attachment_url, ); } return false; // No duplicate found } static function agile_create_showing_tab_content_showslist( $showing ) { // $executionTime = new ExecutionTime('agile_create_showing_tab_content_showslist'); // $executionTime->start(); $date = $showing['DisplayDate'] ?? ''; $Venue = $showing['Venue'] ?? ''; $LegacyPurchaseLink = $showing['LegacyPurchaseLink'] ?? ''; $tabContent = ''; $tabContent .= '
'; // class="tabcontent"> if ( ! empty( $date ) ) { $format_date = wp_date( 'F j, g:i A', strtotime( $date ) ); $tabContent .= '

' . $format_date . '

'; } if ( ! empty( $LegacyPurchaseLink ) ) { $tabContent .= 'Buy Tickets
'; } $tabContent .= '
'; // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); return $tabContent; } /** * Event IDs for posts synced from the main feed (no ATS_ComingSoonDate meta). * * @param string $post_type Configured post type. * @return string[] Event IDs as strings. */ static function agile_get_main_feed_event_ids( $post_type ) { $ids = array(); $query = new WP_Query( array( 'post_type' => $post_type, 'posts_per_page' => -1, 'fields' => 'ids', 'meta_query' => array( 'relation' => 'AND', array( 'key' => 'ATS_EventID', 'value' => '', 'compare' => '!=', ), array( 'key' => 'ATS_ComingSoonDate', 'compare' => 'NOT EXISTS', ), ), ) ); if ( empty( $query->posts ) ) { return $ids; } foreach ( $query->posts as $post_id ) { $event_id = get_post_meta( $post_id, 'ATS_EventID', true ); if ( '' !== $event_id && null !== $event_id ) { $ids[] = (string) $event_id; } } return $ids; } /** * Release the in-flight refresh lock. */ public static function agile_release_refresh_lock() { delete_option( 'agile_refresh_lock' ); } /** * Whether an existing refresh lock is stale (older than 30 minutes and no AS work). * * @param mixed $existing Lock option value. * @return bool */ public static function agile_refresh_lock_is_stale( $existing ) { $as_count = function_exists( 'agile_tix_count_group_actions' ) ? agile_tix_count_group_actions() : -1; if ( $as_count > 0 ) { return false; } if ( $as_count < 0 ) { return false; } $started = 0; if ( is_array( $existing ) && isset( $existing['started'] ) ) { $started = (int) $existing['started']; } if ( $started <= 0 ) { return true; } return ( time() - $started ) > ( 30 * MINUTE_IN_SECONDS ); } /** * Atomically acquire the refresh lock. add_option fails if the option already exists. * * @param string $refresh_token Current refresh token. * @return bool */ public static function agile_try_acquire_refresh_lock( $refresh_token ) { $lock_data = array( 'token' => $refresh_token, 'started' => time(), ); if ( add_option( 'agile_refresh_lock', $lock_data, '', 'no' ) ) { return true; } $existing = get_option( 'agile_refresh_lock' ); if ( ! self::agile_refresh_lock_is_stale( $existing ) ) { return false; } $existing_token = is_array( $existing ) && isset( $existing['token'] ) ? $existing['token'] : 'n/a'; $existing_age = is_array( $existing ) && isset( $existing['started'] ) ? ( time() - (int) $existing['started'] ) : -1; agile_tix::agile_tix_log_event( 'REFRESH LOCK stale; replacing' . ' | prior_token=' . $existing_token . ' | age_sec=' . $existing_age . ' | AS=' . agile_tix_describe_pending_actions() ); delete_option( 'agile_refresh_lock' ); return (bool) add_option( 'agile_refresh_lock', $lock_data, '', 'no' ); } static function ats_universal_refresh_showsList( $deepRefresh = false ) { $refresh_token = uniqid( 'refresh_', true ); $prior_context = get_option( 'agile_processing_context' ); $prior_total = (int) get_option( 'agile_total_chunks', 0 ); $prior_done = (int) get_option( 'agile_processed_chunks', 0 ); $prior_status = get_option( 'agile_field_utility_update_status', 'unknown' ); if ( ! self::agile_try_acquire_refresh_lock( $refresh_token ) ) { $existing_lock = get_option( 'agile_refresh_lock' ); $lock_token = is_array( $existing_lock ) && isset( $existing_lock['token'] ) ? $existing_lock['token'] : 'n/a'; agile_tix::agile_tix_log_event( 'REFRESH SKIPPED: already in progress' . ' | skipped_token=' . $refresh_token . ' | lock_token=' . $lock_token . ' | prior_context=' . ( $prior_context ? 'yes' : 'no' ) . ' | prior_chunks=' . $prior_done . '/' . $prior_total . ' | prior_status=' . $prior_status . ' | AS=' . agile_tix_describe_pending_actions() ); return array( 'started' => false, 'reason' => 'in_progress', ); } agile_tix::agile_tix_log_event( '=== REFRESH START === token=' . $refresh_token . ' deepRefresh=' . ( $deepRefresh ? 'true' : 'false' ) . ' | lock=acquired' . ' | leftover_context=' . ( $prior_context ? 'YES' : 'no' ) . ' prior_chunks=' . $prior_done . '/' . $prior_total . ' prior_status=' . $prior_status . ' | AS=' . agile_tix_describe_pending_actions() ); if ( $prior_context ) { $prior_is_cs = ! empty( $prior_context['isComingSoonFeed'] ); $prior_total_e = isset( $prior_context['total_events'] ) ? (int) $prior_context['total_events'] : -1; $prior_start = isset( $prior_context['startTime'] ) ? $prior_context['startTime'] : 'n/a'; agile_tix::agile_tix_log_event( 'REFRESH: leftover agile_processing_context will be replaced (lock acquired)' . ' | wasComingSoon=' . ( $prior_is_cs ? 'true' : 'false' ) . ' | prior_total_events=' . $prior_total_e . ' | prior_startTime=' . $prior_start . ' | incomplete_chunks=' . ( $prior_total > 0 && $prior_done < $prior_total ? 'YES' : 'no' ) ); } $startTime = microtime( true ); update_option( 'agile_field_utility_last_post_scheduled_start', current_time( 'timestamp' ) ); agile_tix_Options::update_utility_status( 'Scheduled' ); // Reset counters for chunked processing update_option( 'agile_field_utility_last_post_count_created', 0 ); update_option( 'agile_field_utility_last_post_count_updated', 0 ); update_option( 'agile_field_utility_last_post_count_deleated', 0 ); // Content manipulation settings $commentStat = get_option( 'agile_field_comment_status' ) ?? 'closed'; $postStat = get_option( 'agile_field_post_status' ) ?? 'publish'; $autoCull = get_option( 'agile_field_auto_cull' ) ?? 1; $themeType = get_option( 'agile_field_config_integration_type' ) ?? 'none'; $transient = agile_tix_data_fetcher::fetch_agile_data(); // Error handling for fetch data if ( gettype( $transient ) == 'string' && str_contains( $transient, 'Error' ) ) { agile_tix::agile_tix_log_event( 'REFRESH FAIL token=' . $refresh_token . ' fetch_agile_data error: ' . $transient ); self::agile_release_refresh_lock(); agile_tix_Options::update_utility_status( 'Failed' ); return array( 'started' => false, 'reason' => 'failed', ); } // Fetch coming soon feed early if enabled (for event ID comparison) $comingSoonData = null; $comingSoonEventIds = array(); $useComingSoon = get_option( 'agile_field_use_coming_soon_feed' ); agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' useComingSoon=' . ( $useComingSoon ? 'true' : 'false' ) . ' themeType=' . $themeType . ' autoCull=' . $autoCull ); if ( $useComingSoon ) { $comingSoonDataRaw = agile_tix_data_fetcher::fetch_agile_data( 'coming_soon' ); // Handle different response types if ( is_string( $comingSoonDataRaw ) ) { // String response indicates an error agile_tix::agile_tix_log_event( 'Coming Soon Feed: Fetch failed or returned error: ' . $comingSoonDataRaw ); $comingSoonData = null; } elseif ( is_array( $comingSoonDataRaw ) ) { // Valid array response - handle empty feeds (missing ArrayOfShows is normal) if ( ! isset( $comingSoonDataRaw['ArrayOfShows'] ) ) { // Empty feed - create empty ArrayOfShows array for consistency $comingSoonDataRaw['ArrayOfShows'] = array(); } $comingSoonData = $comingSoonDataRaw; // Store the data (may have empty ArrayOfShows) $totalComingSoonEvents = count( $comingSoonData['ArrayOfShows'] ); // Create event ID map for comparison (only events with ComingSoonDate) foreach ( $comingSoonData['ArrayOfShows'] as $comingSoonEvent ) { if ( isset( $comingSoonEvent['ComingSoonDate'] ) && isset( $comingSoonEvent['ID'] ) ) { $comingSoonEventIds[ $comingSoonEvent['ID'] ] = $comingSoonEvent; } } $eventsWithComingSoonDate = is_array( $comingSoonEventIds ) ? count( $comingSoonEventIds ) : 0; agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' comingSoon feed shows=' . $totalComingSoonEvents . ' withComingSoonDate=' . $eventsWithComingSoonDate ); } else { // Unexpected data type agile_tix::agile_tix_log_event( 'Coming Soon Feed: Unexpected data type: ' . gettype( $comingSoonDataRaw ) ); $comingSoonData = null; } } // Determine post type using the shared function $postType = agile_tix_post_type::get_configured_post_type(); if ( empty( $postType ) ) { agile_tix::agile_tix_log_event( 'REFRESH FAIL token=' . $refresh_token . ' no post type configured (themeType=' . $themeType . ')' ); self::agile_release_refresh_lock(); agile_tix_Options::update_utility_status( 'Failed' ); return array( 'started' => false, 'reason' => 'failed', ); } $args = array( 'post_type' => $postType, 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => 'ATS_EventID', 'value' => '', 'compare' => '!=', ), ), ); $event_ids = array(); $main_feed_event_ids_only = array(); // Separate list for main feed only (excludes coming soon posts) $query = new WP_Query( $args ); if ( $query->have_posts() ) { while ( $query->have_posts() ) { $query->the_post(); $event_id = get_post_meta( get_the_ID(), 'ATS_EventID', true ); if ( ! empty( $event_id ) ) { $event_ids[ $event_id ] = get_the_ID(); // Only include in main_feed_event_ids if this post does NOT have ComingSoonDate // This ensures coming soon posts are not incorrectly treated as main feed posts $has_coming_soon_date = get_post_meta( get_the_ID(), 'ATS_ComingSoonDate', true ); if ( empty( $has_coming_soon_date ) ) { $main_feed_event_ids_only[] = $event_id; } } } wp_reset_postdata(); } $events = array(); if ( array_key_exists( 'ArrayOfShows', $transient ) ) { $events = $transient['ArrayOfShows']; } else { agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' WARNING: main feed response missing ArrayOfShows key; keys=' . ( is_array( $transient ) ? implode( ',', array_keys( $transient ) ) : gettype( $transient ) ) ); } agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' main feed ArrayOfShows=' . count( $events ) . ' | WP posts with ATS_EventID=' . count( $event_ids ) . ' | main_feed_only_posts=' . count( $main_feed_event_ids_only ) . ' | postType=' . $postType ); // Store original events for cleanup purposes $original_events = $events; // Filter events to only include those that need updates or are new $filter_result = self::agile_filter_events_for_refresh( $events, $event_ids, $deepRefresh ); $events = $filter_result['filtered_events']; $processed_event_ids = $filter_result['processed_event_ids']; $new_ids_for_log = array(); foreach ( $events as $fe ) { $fid = isset( $fe['ID'] ) ? (string) $fe['ID'] : ''; if ( '' === $fid ) { continue; } if ( ! isset( $event_ids[ $fid ] ) && ! isset( $event_ids[ (int) $fid ] ) ) { $new_ids_for_log[] = $fid; } } agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' FILTER RESULT: to_process=' . count( $events ) . ' skipped_unchanged=' . count( $processed_event_ids ) . ' presumed_new=' . count( $new_ids_for_log ) . ( empty( $new_ids_for_log ) ? '' : ' new_ids=[' . implode( ', ', $new_ids_for_log ) . ']' ) ); // Store processing context for chunks $processing_context = array( 'deepRefresh' => $deepRefresh, 'commentStat' => $commentStat, 'postStat' => $postStat, 'themeType' => $themeType, 'postType' => $postType, 'event_ids' => $event_ids, 'processed_event_ids' => $processed_event_ids, 'autoCull' => $autoCull, 'startTime' => $startTime, 'total_events' => count( $events ), 'events' => $original_events, 'comingSoonEventIds' => $comingSoonEventIds, 'comingSoonData' => $comingSoonData, 'useComingSoon' => $useComingSoon, 'main_feed_event_ids' => $main_feed_event_ids_only, // Store main feed event IDs only (excludes coming soon posts) 'refresh_token' => $refresh_token, ); $comingSoonEventIdsCount = is_array( $comingSoonEventIds ) ? count( $comingSoonEventIds ) : 0; $comingSoonDataType = gettype( $comingSoonData ); agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' WRITING context (overwrites prior if any)' . ' | useComingSoon=' . ( $useComingSoon ? 'true' : 'false' ) . ' | comingSoonEventIds=' . $comingSoonEventIdsCount . ' | comingSoonDataType=' . $comingSoonDataType ); // Store context temporarily update_option( 'agile_processing_context', $processing_context ); $chunksize = get_option( 'agile_chunk_size' ) ?? 10; $chunksize = (int) $chunksize; if ( $chunksize <= 0 ) { // incase the user put something weird in the input $chunksize = 10; } $chunks = array_chunk( $events, $chunksize ); $total_chunks = count( $chunks ); // Store chunk tracking update_option( 'agile_total_chunks', $total_chunks ); update_option( 'agile_processed_chunks', 0 ); agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' scheduling ' . $total_chunks . ' chunk(s) chunksize=' . $chunksize ); // Schedule each chunk $scheduled_ok = 0; foreach ( $chunks as $index => $chunk ) { // Store chunk data in options table with unique key $chunk_key = 'agile_chunk_data_' . $index . '_' . uniqid() . '_' . time(); $chunk_event_ids = array(); foreach ( $chunk as $ce ) { if ( isset( $ce['ID'] ) ) { $chunk_event_ids[] = (string) $ce['ID']; } } update_option( $chunk_key, $chunk, false ); // false = don't autoload $action_id = agile_tix_schedule_single_action( time(), // Due immediately — staggered delays stall on DISABLE_WP_CRON / failed AS loopback 'agile_process_events_batch', array( 'chunk_key' => $chunk_key, // Pass the key instead of data 'chunk_index' => $index, 'is_final_chunk' => ( $index === $total_chunks - 1 ), ), 'agile_events_processing' ); if ( $action_id ) { ++$scheduled_ok; agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' scheduled chunk ' . $index . ' action_id=' . $action_id . ' chunk_key=' . $chunk_key . ' events=[' . implode( ', ', $chunk_event_ids ) . ']' . ' run_at_offset_sec=0' ); } else { agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . " Failed to schedule chunk $index key=$chunk_key" ); // Clean up the stored chunk data if scheduling failed delete_option( $chunk_key ); } } // If no events to process, handle cleanup immediately if ( empty( $events ) ) { agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' EMPTY filtered set -> INLINE finalize (context_arg=array)' . ' | AS after schedule=' . agile_tix_describe_pending_actions() ); self::agile_finalize_processing( $processing_context ); } else { agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' EXIT waiting on AS batches scheduled_ok=' . $scheduled_ok . '/' . $total_chunks . ' | AS=' . agile_tix_describe_pending_actions() ); agile_tix_kick_queue_after_schedule( 'AgileTix Refresh ' . $refresh_token ); agile_tix::agile_tix_log_event( 'REFRESH token=' . $refresh_token . ' AFTER queue kick | AS=' . agile_tix_describe_pending_actions() . ' | status=' . get_option( 'agile_field_utility_update_status', 'unknown' ) ); } return array( 'started' => true, ); } public function agile_handle_events_batch( $chunk_key, $chunk_index, $is_final_chunk ) { // agile_tix::agile_tix_log_event("Args received - chunk_key: $chunk_key, chunk_index: $chunk_index, is_final_chunk: " . ($is_final_chunk ? 'true' : 'false')); // agile_tix::agile_tix_log_event("agile_handle_events_batch start"); // agile_tix::agile_tix_log_event("Args received: " . print_r($args, true)); // Set active timestamp and status when processing actually begins $current_timestamp = current_time( 'timestamp' ); // Set total processing start time only on first chunk (never reset) if ( $chunk_index === 0 ) { update_option( 'agile_field_utility_total_processing_start', $current_timestamp ); } // Set chunk start time (resets on each chunk) update_option( 'agile_field_utility_last_post_update_start', $current_timestamp ); agile_tix_Options::update_utility_status( 'Active' ); // Retrieve processing context $context = get_option( 'agile_processing_context' ); if ( ! $context ) { agile_tix::agile_tix_log_event( 'Batch Handler: ABANDON chunk — processing context MISSING' . ' | chunk_index=' . $chunk_index . ' | chunk_key=' . $chunk_key . ' | is_final_chunk=' . ( $is_final_chunk ? 'true' : 'false' ) . ' | chunk_option_exists=' . ( false !== get_option( $chunk_key, false ) ? 'yes' : 'no' ) . ' | counters=' . (int) get_option( 'agile_processed_chunks', 0 ) . '/' . (int) get_option( 'agile_total_chunks', 0 ) . ' | AS=' . agile_tix_describe_pending_actions() . ' | ASSUMPTION: overlapping refresh or finalize deleted shared context before this batch ran' ); return; } $deepRefresh = $context['deepRefresh']; $commentStat = $context['commentStat']; $postStat = $context['postStat']; $themeType = $context['themeType']; $event_ids = $context['event_ids']; $processed_event_ids = $context['processed_event_ids'] ?? array(); $isComingSoonFeed = $context['isComingSoonFeed'] ?? false; $context_token = $context['refresh_token'] ?? 'none'; // Only enrich with coming soon data if processing main feed $comingSoonEventIds = ( $isComingSoonFeed ) ? array() : ( $context['comingSoonEventIds'] ?? array() ); // Retrieve chunk data $chunk = get_option( $chunk_key ); $chunkCount = is_array( $chunk ) ? count( $chunk ) : 0; $chunk_ids = array(); if ( is_array( $chunk ) ) { foreach ( $chunk as $ce ) { if ( isset( $ce['ID'] ) ) { $chunk_ids[] = (string) $ce['ID']; } } } agile_tix::agile_tix_log_event( 'Batch Handler: Processing chunk ' . $chunk_index . ' | refresh_token=' . $context_token . ' | isComingSoonFeed=' . ( $isComingSoonFeed ? 'true' : 'false' ) . ' | is_final_chunk=' . ( $is_final_chunk ? 'true' : 'false' ) . ' | events_in_chunk=' . $chunkCount . ' | event_ids=[' . implode( ', ', $chunk_ids ) . ']' . ' | counters_before=' . (int) get_option( 'agile_processed_chunks', 0 ) . '/' . (int) get_option( 'agile_total_chunks', 0 ) ); // agile_tix::agile_tix_log_event('Processing context deepRefresh ' . $chunk_index); // agile_tix::agile_tix_log_event('Processing context commentStat ' . $commentStat); // agile_tix::agile_tix_log_event('Processing context postStat ' . $postStat); // agile_tix::agile_tix_log_event('Processing context themeType ' . $themeType); // agile_tix::agile_tix_log_event('Processing context event_ids ' . $event_ids); // Performance optimizations for this chunk wp_suspend_cache_invalidation( true ); wp_defer_term_counting( true ); wp_defer_comment_counting( true ); global $wpdb; $wpdb->query( 'SET autocommit = 0;' ); if ( ! $chunk || ! is_array( $chunk ) ) { agile_tix::agile_tix_log_event( "Batch Handler: chunk data missing or invalid for key {$chunk_key} (type: " . gettype( $chunk ) . ')' ); return; } $chunk_created = 0; $chunk_updated = 0; $chunkCount = is_array( $chunk ) ? count( $chunk ) : 0; // agile_tix::agile_tix_log_event("Batch Handler: Processing chunk with $chunkCount events (chunk_key: $chunk_key)"); foreach ( $chunk as $event ) { $eventID = $event['ID'] ?? 'NO_ID'; $eventName = $event['Name'] ?? 'NO_NAME'; $hasComingSoonDate = isset( $event['ComingSoonDate'] ) ? 'true' : 'false'; // if ($isComingSoonFeed) { // agile_tix::agile_tix_log_event("Batch Handler (Coming Soon): Processing event $eventID ($eventName) - ComingSoonDate = $hasComingSoonDate"); // } $event_id_key = (string) ( $event['ID'] ?? '' ); $postID = $event_ids[ $event_id_key ] ?? 0; if ( 0 === $postID && '' !== $event_id_key ) { $event_meta_query = array( array( 'key' => 'ATS_EventID', 'value' => $event_id_key, 'compare' => '=', ), ); if ( $isComingSoonFeed ) { $event_meta_query[] = array( 'key' => 'ATS_ComingSoonDate', 'compare' => 'EXISTS', ); } else { $event_meta_query[] = array( 'key' => 'ATS_ComingSoonDate', 'compare' => 'NOT EXISTS', ); } $existing_posts = get_posts( array( 'post_type' => agile_tix_post_type::get_configured_post_type(), 'posts_per_page' => 1, 'orderby' => 'ID', 'order' => 'ASC', 'meta_query' => $event_meta_query, ) ); if ( ! empty( $existing_posts ) ) { $postID = (int) $existing_posts[0]->ID; } } $isNewEvent = ( $postID == 0 ); agile_tix::agile_tix_log_event( 'Batch Handler: event ' . $eventID . ' (' . $eventName . ')' . ' isNewEvent=' . ( $isNewEvent ? 'true' : 'false' ) . ' postID=' . $postID . ' ComingSoonDate=' . $hasComingSoonDate, agile_tix::LOG_VERBOSE ); // if ($isComingSoonFeed) { // agile_tix::agile_tix_log_event("Batch Handler (Coming Soon): Event $eventID - isNewEvent = " . ($isNewEvent ? 'true' : 'false') . ", postID = $postID"); // } $comingSoonEventData = null; $has_main_feed_showings = isset( $event['CurrentShowings'] ) && is_array( $event['CurrentShowings'] ) && ! empty( $event['CurrentShowings'] ); if ( ! $has_main_feed_showings ) { if ( isset( $comingSoonEventIds[ $event_id_key ] ) ) { $comingSoonEventData = $comingSoonEventIds[ $event_id_key ]; } elseif ( isset( $comingSoonEventIds[ $event['ID'] ] ) ) { $comingSoonEventData = $comingSoonEventIds[ $event['ID'] ]; } } // Remove processed event from event_ids for final cleanup // Only remove if this event was actually processed (not skipped) if ( ! $isNewEvent && ! isset( $processed_event_ids[ $event_id_key ] ) ) { unset( $event_ids[ $event_id_key ] ); } if ( ! $deepRefresh && ! $isNewEvent ) { $modDate = get_post_meta( $postID, 'ATS_ModifiedDate', true ); $lastModified = $modDate != '' ? $modDate : false; $firstEndDate = get_post_meta( $postID, 'ATS_firstEndDate', true ); $isComingSoonEvent = isset( $event['ComingSoonDate'] ) && ( ! isset( $event['CurrentShowings'] ) || empty( $event['CurrentShowings'] ) ); // For coming soon events, skip firstIsExpired check (they don't have showings/end dates) if ( $isComingSoonEvent ) { $firstIsExpired = false; // Coming soon events are never "expired" based on showings } else { // Add 8 hour wiggle to match filter function logic (covers timezones from LA to NY) $firstDate_timestamp = strtotime( $firstEndDate ); if ( $firstDate_timestamp !== false ) { $firstDate_timestamp = $firstDate_timestamp + ( 8 * 60 * 60 ); } $current_timestamp = current_time( 'timestamp' ); $firstIsExpired = ( $firstDate_timestamp !== false && $firstDate_timestamp < $current_timestamp ); } if ( $firstIsExpired && ! $isComingSoonEvent ) { agile_tix::agile_tix_log_event( 'firstIsExpired == true, eventid : ' . $event['ID'] . ' postid : ' . $postID . ' firstDate_timestamp : ' . ( $firstDate_timestamp ?? 'false' ) . ' current_timestamp : ' . $current_timestamp, agile_tix::LOG_VERBOSE ); } $lastShowings = get_post_meta( $postID, 'ATS_showingIDs', true ); // Ensure lastShowings is an array for proper comparison if ( ! is_array( $lastShowings ) ) { $lastShowings = empty( $lastShowings ) ? array() : ( is_string( $lastShowings ) ? unserialize( $lastShowings ) : array( $lastShowings ) ); if ( ! is_array( $lastShowings ) ) { $lastShowings = array(); } } $currentShowings = array(); if ( isset( $event['CurrentShowings'] ) && is_array( $event['CurrentShowings'] ) ) { foreach ( $event['CurrentShowings'] as $showing ) { array_push( $currentShowings, $showing['ID'] ); } } // Normalize arrays for comparison (sort and ensure same types) sort( $lastShowings ); sort( $currentShowings ); $lastShowings = array_map( 'strval', $lastShowings ); $currentShowings = array_map( 'strval', $currentShowings ); $showings_match = ( $lastShowings === $currentShowings ); $current_most_recent = self::agile_get_most_recent_lastUpdate( $event ); $dates_match = ( $lastModified != false && $lastModified == $current_most_recent ); // Always log when a post is NOT being skipped (to identify problematic posts) // if (!($showings_match && !$firstIsExpired && $dates_match)) { // $eventName = isset($event['Name']) ? $event['Name'] : 'NO_NAME'; // $skip_reasons = array(); // // if (!$showings_match) { // $skip_reasons[] = "showings mismatch"; // } // if ($firstIsExpired) { // $skip_reasons[] = "first showing expired"; // } // if (!$dates_match) { // $skip_reasons[] = "dates mismatch"; // } // // $reason_str = !empty($skip_reasons) ? implode(', ', $skip_reasons) : 'unknown'; // // agile_tix::agile_tix_log_event("Batch Handler: Event {$event['ID']} ({$eventName}) - Post ID {$postID} NOT SKIPPED - Reason: $reason_str"); // agile_tix::agile_tix_log_event("Batch Handler: Event {$event['ID']} - Details: showings_match=" . ($showings_match ? 'true' : 'false') . // ", firstIsExpired=" . ($firstIsExpired ? 'true' : 'false') . // ", dates_match=" . ($dates_match ? 'true' : 'false')); // agile_tix::agile_tix_log_event("Batch Handler: Event {$event['ID']} - Date comparison: stored_date='$lastModified' vs current_date='$current_most_recent'"); // agile_tix::agile_tix_log_event("Batch Handler: Event {$event['ID']} - Showings comparison: lastShowings=[" . implode(', ', $lastShowings) . "] vs currentShowings=[" . implode(', ', $currentShowings) . "]"); // // if ($isComingSoonEvent) { // agile_tix::agile_tix_log_event("Batch Handler: Event {$event['ID']} - Is coming soon event (firstIsExpired check skipped)"); // } else { // $firstEndDateDisplay = !empty($firstEndDate) ? $firstEndDate : 'EMPTY'; // $firstDateTimestampDisplay = isset($firstDate_timestamp) && $firstDate_timestamp !== false ? date('Y-m-d H:i:s', $firstDate_timestamp) : 'INVALID'; // $currentTimestampDisplay = date('Y-m-d H:i:s', $current_timestamp); // agile_tix::agile_tix_log_event("Batch Handler: Event {$event['ID']} - Expiration check: firstEndDate='$firstEndDateDisplay', firstDate_timestamp='$firstDateTimestampDisplay', current_timestamp='$currentTimestampDisplay'"); // } // } if ( $showings_match && ! $firstIsExpired && $dates_match ) { agile_tix::agile_tix_log_event( 'Batch Handler: SKIP update event ' . ( $event['ID'] ?? '' ) . ' postID=' . $postID . ' (showings+dates match, not expired) — already filtered in but re-checked', agile_tix::LOG_VERBOSE ); continue; } agile_tix::agile_tix_log_event( 'Batch Handler: Event ' . $event['ID'] . ' (' . $eventName . ') - Post ID ' . $postID . ' will update | showings_match=' . ( $showings_match ? 'true' : 'false' ) . ' firstIsExpired=' . ( $firstIsExpired ? 'true' : 'false' ) . ' dates_match=' . ( $dates_match ? 'true' : 'false' ) . " | stored_date='$lastModified' vs current_date='$current_most_recent'", agile_tix::LOG_VERBOSE ); } // agile_tix::agile_tix_log_event("agile_handle_events_batch themeType" . $themeType); // if ($isComingSoonFeed) { // agile_tix::agile_tix_log_event("Batch Handler (Coming Soon): Creating/updating post for event $eventID using themeType = $themeType"); // } // Validate themeType - if empty or null, default to 'none' if ( empty( $themeType ) ) { agile_tix::agile_tix_log_event( "Batch Handler: themeType is empty, defaulting to 'none' for event $eventID" ); $themeType = 'none'; } switch ( $themeType ) { case 'none': self::agile_post_creator( $event, $commentStat, $postStat, $isNewEvent, $postID, $comingSoonEventData ); // if ($isComingSoonFeed) { // agile_tix::agile_tix_log_event("Batch Handler (Coming Soon): Called agile_post_creator for event $eventID"); // } break; case 'event_champ': agile_tix_event_champ_integrate::agile_eventChamp_post_creator( $event, $commentStat, $postStat, $isNewEvent, $postID, $comingSoonEventData ); break; case 'custom': agile_tix_custom_support::agile_user_custom_post_creator( $event, $commentStat, $postStat, $isNewEvent, $postID, $comingSoonEventData ); break; case 'amy_movie': agile_tix_amy_movie_integrate::agile_amy_movie_post_creator( $event, $commentStat, $postStat, $isNewEvent, $postID, $comingSoonEventData ); break; default: agile_tix::agile_tix_log_event( "Batch Handler: ERROR - Unexpected themeType '$themeType' for event $eventID, defaulting to 'none'" ); // Instead of failing, default to 'none' to allow processing to continue self::agile_post_creator( $event, $commentStat, $postStat, $isNewEvent, $postID, $comingSoonEventData ); break; } if ( $isNewEvent ) { ++$chunk_created; } else { ++$chunk_updated; } } // Restore performance settings for this chunk wp_suspend_cache_invalidation( false ); wp_defer_term_counting( false ); wp_defer_comment_counting( false ); $wpdb->query( 'SET autocommit = 1;' ); $wpdb->query( 'COMMIT;' ); // Update counters $total_created = get_option( 'agile_field_utility_last_post_count_created', 0 ) + $chunk_created; $total_updated = get_option( 'agile_field_utility_last_post_count_updated', 0 ) + $chunk_updated; update_option( 'agile_field_utility_last_post_count_created', $total_created ); update_option( 'agile_field_utility_last_post_count_updated', $total_updated ); // Update processed chunks counter $processed_chunks = get_option( 'agile_processed_chunks', 0 ) + 1; update_option( 'agile_processed_chunks', $processed_chunks ); // Update remaining event_ids in context for cleanup $context['event_ids'] = $event_ids; update_option( 'agile_processing_context', $context ); agile_tix::agile_tix_log_event( 'Batch Processing: chunk ' . $chunk_index . ' done' . ' | refresh_token=' . ( $context['refresh_token'] ?? 'none' ) . ' | created=' . $chunk_created . ' | updated=' . $chunk_updated . ' | counters=' . $processed_chunks . '/' . (int) get_option( 'agile_total_chunks', 0 ) . ' | isComingSoonFeed=' . ( ! empty( $context['isComingSoonFeed'] ) ? 'true' : 'false' ) ); // error_log("Agile Tix: Processed chunk {$chunk_index} - {$chunk_created} created, {$chunk_updated} updated"); if ( $chunk_created >= 0 && $chunk_updated >= 0 ) { // Processing completed successfully delete_option( $chunk_key ); // agile_tix::agile_tix_log_event("Successfully processed and cleaned up chunk: $chunk_key"); } // If this is the final chunk, handle cleanup if ( $is_final_chunk ) { $scheduled_time = time(); // Immediate — +20s delay never drains when AS loopback/WP-Cron are dead $scheduled_time_str = gmdate( 'Y-m-d H:i:s', $scheduled_time ); agile_tix::agile_tix_log_event( 'Final Chunk: scheduling finalize immediately' . ' | refresh_token=' . ( $context['refresh_token'] ?? 'none' ) . ' | scheduled_utc=' . $scheduled_time_str . ' | isComingSoonFeed=' . ( $isComingSoonFeed ? 'true' : 'false' ) . ' | AS=' . agile_tix_describe_pending_actions() ); // agile_tix::agile_tix_log_event( 'Final Chunk: DEBUG - Current time: ' . current_time( 'mysql' ) ); // agile_tix::agile_tix_log_event( "Final Chunk: DEBUG - Scheduled for: $scheduled_time_str (timestamp: $scheduled_time)" ); // agile_tix::agile_tix_log_event( 'Final Chunk: isComingSoonFeed = ' . ( $isComingSoonFeed ? 'true' : 'false' ) ); // if ( $isComingSoonFeed ) { // agile_tix::agile_tix_log_event( 'Final Chunk: DEBUG - This is coming soon feed, finalization will run culling' ); // } $finalActionID = agile_tix_schedule_single_action( $scheduled_time, 'agile_finalize_processing', array(), // Empty args - let the finalizer get context from options 'agile_events_processing' ); agile_tix::agile_tix_log_event( 'Final Chunk: Scheduled finalization action with ID: ' . ( $finalActionID ?: 'FAILED' ) ); // Do not nest QueueRunner::run while already inside a batch; drain on shutdown instead. agile_tix_schedule_queue_kick_on_shutdown(); // agile_tix::agile_tix_log_event("Final Chunk: Scheduled finalization action with ID: " . ($finalActionID ?: 'FAILED')); if ( ! $finalActionID ) { global $wpdb; agile_tix::agile_tix_log_event( 'DB Error scheduling finalizer: ' . $wpdb->last_error ); // Fallback: Try to schedule with longer delay if initial scheduling fails // But only if we're sure this is truly the last chunk $total_chunks = get_option( 'agile_total_chunks', 0 ); $processed_chunks = get_option( 'agile_processed_chunks', 0 ); if ( $processed_chunks >= $total_chunks ) { agile_tix::agile_tix_log_event( 'Scheduling finalizer failed, attempting immediate retry' ); $finalActionID = agile_tix_schedule_single_action( time(), 'agile_finalize_processing', array(), 'agile_events_processing' ); agile_tix_schedule_queue_kick_on_shutdown(); } } else { // agile_tix::agile_tix_log_event("Finalizer scheduled with ID: $finalActionID"); } } } public static function agile_finalize_processing( $context_arg = null ) { agile_tix::agile_tix_log_event( '=== FINALIZATION START ===' . ' context_arg_type=' . gettype( $context_arg ) . ' | note: context_arg is LOGGED ONLY — real context always loaded from option agile_processing_context' . ' | AS=' . agile_tix_describe_pending_actions() ); $context = get_option( 'agile_processing_context' ); if ( ! $context ) { agile_tix::agile_tix_log_event( 'Finalization: ERROR - Processing context not found for finalization' . ' | counters=' . (int) get_option( 'agile_processed_chunks', 0 ) . '/' . (int) get_option( 'agile_total_chunks', 0 ) . ' | ASSUMPTION: prior finalize/refresh already deleted context (overlap)' ); return; } $ctx_token = $context['refresh_token'] ?? 'none'; agile_tix::agile_tix_log_event( 'Finalization: Context retrieved' . ' | refresh_token=' . $ctx_token . ' | isComingSoonFeed=' . ( ! empty( $context['isComingSoonFeed'] ) ? 'true' : 'false' ) . ' | total_events=' . ( isset( $context['total_events'] ) ? $context['total_events'] : 'n/a' ) . ' | keys=' . implode( ', ', array_keys( $context ) ) ); $isComingSoonFeed = isset( $context['isComingSoonFeed'] ) ? $context['isComingSoonFeed'] : false; // Verify all chunks have been processed before finalizing $total_chunks = get_option( 'agile_total_chunks', 0 ); $processed_chunks = get_option( 'agile_processed_chunks', 0 ); if ( $total_chunks > 0 && $processed_chunks < $total_chunks ) { agile_tix::agile_tix_log_event( 'Finalization: PREMATURE — rescheduling immediately' . ' | refresh_token=' . $ctx_token . ' | processed=' . $processed_chunks . '/' . $total_chunks ); // Reschedule if not all chunks are done (immediate so DISABLE_WP_CRON hosts can drain) $premature_id = agile_tix_schedule_single_action( time(), 'agile_finalize_processing', array(), 'agile_events_processing' ); agile_tix::agile_tix_log_event( 'Finalization: premature reschedule action_id=' . ( $premature_id ?: 'FAILED' ) ); agile_tix_schedule_queue_kick_on_shutdown(); return; } $autoCull = $context['autoCull']; $event_ids = $context['event_ids']; $processed_event_ids = $context['processed_event_ids'] ?? array(); $startTime = $context['startTime']; $postDeleted = 0; // agile_tix::agile_tix_log_event("Finalization: autoCull = $autoCull"); if ( $autoCull == 1 ) { $isComingSoonFeed = $context['isComingSoonFeed'] ?? false; agile_tix::agile_tix_log_event( 'Finalization: Auto-cull enabled, isComingSoonFeed = ' . ( $isComingSoonFeed ? 'true' : 'false' ) . ' autoCull=' . $autoCull ); // agile_tix::agile_tix_log_event( 'Finalization: autoCull value = ' . $autoCull ); if ( $isComingSoonFeed ) { // For coming soon feed cleanup, only delete posts that: // 1. Have ComingSoonDate (to identify coming soon posts) // 2. Are not in the current coming soon feed // Note: We don't delete main feed posts even if they have ComingSoonDate agile_tix::agile_tix_log_event( '=== Auto-Cull (Coming Soon): Starting cleanup for coming soon feed ===' ); // agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): DEBUG - Current timestamp: ' . current_time( 'mysql' ) ); $postType = isset( $context['postType'] ) ? $context['postType'] : 'post'; // agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): DEBUG - Post type being queried: ' . $postType ); // Get ALL event IDs from the current coming soon feed response FIRST // This is the authoritative list for culling - if an event ID is not here, delete the post $current_feed_event_ids = isset( $context['all_coming_soon_feed_event_ids'] ) && is_array( $context['all_coming_soon_feed_event_ids'] ) ? $context['all_coming_soon_feed_event_ids'] : array(); // Ensure it's always an array if ( ! is_array( $current_feed_event_ids ) ) { $current_feed_event_ids = array(); } agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): Event IDs in current feed: ' . ( empty( $current_feed_event_ids ) ? 'NONE' : implode( ', ', $current_feed_event_ids ) ), agile_tix::LOG_VERBOSE ); agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): Total event IDs in feed: ' . count( $current_feed_event_ids ) ); // Query for posts with ComingSoonDate (primary query) $all_existing_posts = get_posts( array( 'post_type' => $postType, 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => 'ATS_EventID', 'value' => '', 'compare' => '!=', ), array( 'key' => 'ATS_ComingSoonDate', 'compare' => 'EXISTS', ), ), ) ); $existingPostsCount = is_array( $all_existing_posts ) ? count( $all_existing_posts ) : 0; agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): Found $existingPostsCount existing posts with ComingSoonDate" ); // Log all existing coming soon posts // foreach ( $all_existing_posts as $post ) { // $post_event_id = get_post_meta( $post->ID, 'ATS_EventID', true ); // $coming_soon_date = get_post_meta( $post->ID, 'ATS_ComingSoonDate', true ); // agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): Existing post ID {$post->ID}, Event ID: $post_event_id, ComingSoonDate: $coming_soon_date" ); // } $main_feed_event_ids = self::agile_get_main_feed_event_ids( $postType ); $all_posts_to_check = $all_existing_posts; // Only protect posts that were actually processed/created in this run // This includes events from filtered_events (newly created/updated) and // events from processed_event_ids that are ALSO in the current feed $just_processed_event_ids = array(); if ( isset( $context['filtered_events'] ) && is_array( $context['filtered_events'] ) ) { foreach ( $context['filtered_events'] as $event ) { if ( isset( $event['ID'] ) ) { $just_processed_event_ids[] = (string) $event['ID']; } } } // Only add processed_event_ids that are in the current feed // This protects posts that were just updated (not just existing ones) if ( isset( $context['processed_event_ids'] ) && is_array( $context['processed_event_ids'] ) && is_array( $current_feed_event_ids ) ) { foreach ( $context['processed_event_ids'] as $event_id => $post_id ) { $event_id_str = (string) $event_id; // Only protect if it's in the current feed (was actually processed this run) if ( in_array( $event_id_str, $current_feed_event_ids, true ) ) { $just_processed_event_ids[] = $event_id_str; } } } $just_processed_event_ids = array_unique( $just_processed_event_ids ); $protected_ids_str = empty( $just_processed_event_ids ) ? 'NONE' : implode( ', ', $just_processed_event_ids ); agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): Protecting ' . count( $just_processed_event_ids ) . ' just-processed event IDs: ' . $protected_ids_str ); // First, detect and handle duplicate posts (multiple posts with same event ID) $event_id_to_posts = array(); foreach ( $all_posts_to_check as $post ) { $post_event_id = get_post_meta( $post->ID, 'ATS_EventID', true ); $post_event_id_str = (string) $post_event_id; if ( ! empty( $post_event_id_str ) ) { if ( ! isset( $event_id_to_posts[ $post_event_id_str ] ) ) { $event_id_to_posts[ $post_event_id_str ] = array(); } $event_id_to_posts[ $post_event_id_str ][] = $post; } } // Remove duplicates: for each event ID with multiple posts, keep one and delete the rest $duplicates_deleted = 0; foreach ( $event_id_to_posts as $event_id_str => $posts_with_same_id ) { if ( count( $posts_with_same_id ) > 1 ) { agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): Found ' . count( $posts_with_same_id ) . " duplicate posts for event ID $event_id_str" ); // Determine which post to keep $post_to_keep = null; $posts_to_delete = array(); // First, try to find a post that was just processed if ( isset( $context['processed_event_ids'] ) && is_array( $context['processed_event_ids'] ) ) { foreach ( $context['processed_event_ids'] as $proc_event_id => $proc_post_id ) { if ( (string) $proc_event_id === $event_id_str ) { // Found a processed post for this event ID - find it in our list foreach ( $posts_with_same_id as $post ) { if ( $post->ID == $proc_post_id ) { $post_to_keep = $post; break 2; // Break out of both loops } } } } } // If no processed post found, keep the most recently modified one if ( $post_to_keep === null ) { foreach ( $posts_with_same_id as $post ) { if ( $post_to_keep === null ) { $post_to_keep = $post; } else { // Compare modification dates - keep the most recent $keep_date = strtotime( $post_to_keep->post_modified ); $this_date = strtotime( $post->post_modified ); if ( $this_date > $keep_date ) { $posts_to_delete[] = $post_to_keep; $post_to_keep = $post; } else { $posts_to_delete[] = $post; } } } } else { // We found a processed post, mark all others for deletion foreach ( $posts_with_same_id as $post ) { if ( $post->ID !== $post_to_keep->ID ) { $posts_to_delete[] = $post; } } } // Delete all duplicate posts except the one we're keeping if ( $post_to_keep !== null ) { agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): Keeping post ID {$post_to_keep->ID} for event ID $event_id_str, deleting " . count( $posts_to_delete ) . ' duplicates' ); foreach ( $posts_to_delete as $duplicate_post ) { agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): DELETING duplicate post ID {$duplicate_post->ID} (event ID $event_id_str)" ); $mediaID = get_post_meta( $duplicate_post->ID, 'ATS_MediaLib_Img_ID', true ); if ( ! empty( $mediaID ) && is_numeric( $mediaID ) && $mediaID > 0 ) { wp_delete_post( (int) $mediaID, true ); } wp_delete_post( $duplicate_post->ID, true ); ++$duplicates_deleted; ++$postDeleted; // Remove from all_posts_to_check array so it's not processed again $all_posts_to_check = array_filter( $all_posts_to_check, function ( $p ) use ( $duplicate_post ) { return $p->ID !== $duplicate_post->ID; } ); $all_posts_to_check = array_values( $all_posts_to_check ); // Re-index } } } } if ( $duplicates_deleted > 0 ) { agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): Removed $duplicates_deleted duplicate posts" ); } foreach ( $all_posts_to_check as $post ) { $post_event_id = get_post_meta( $post->ID, 'ATS_EventID', true ); $post_event_id_str = (string) $post_event_id; // Skip if this is a main feed post (has event ID in main feed list) if ( in_array( $post_event_id_str, $main_feed_event_ids, true ) ) { agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): SKIPPING post ID {$post->ID} (event ID $post_event_id_str) - is a main feed post", agile_tix::LOG_VERBOSE ); continue; } // Skip if this post was just processed in this run if ( in_array( $post_event_id_str, $just_processed_event_ids, true ) ) { agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): KEEPING post ID {$post->ID} (event ID $post_event_id_str) - was just processed", agile_tix::LOG_VERBOSE ); continue; } // Check if this event ID is in the current coming soon feed response // Compare against ALL event IDs from the feed, not just filtered ones $isInFeed = is_array( $current_feed_event_ids ) && in_array( $post_event_id_str, $current_feed_event_ids, true ); // If not in coming soon feed, delete the coming soon post // This handles cases where: // 1. Event transitioned from coming soon to now showing (no longer in coming soon feed) // 2. Event was removed from coming soon feed entirely // Note: Even if the event is now in the main feed (now showing), we still delete // the old coming soon post because the main feed will have its own post if ( ! $isInFeed ) { agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): DELETING post ID {$post->ID} (event ID $post_event_id_str) - not in current coming soon feed" ); $mediaID = get_post_meta( $post->ID, 'ATS_MediaLib_Img_ID', true ); if ( ! empty( $mediaID ) && is_numeric( $mediaID ) && $mediaID > 0 ) { wp_delete_post( (int) $mediaID, true ); } wp_delete_post( $post->ID, true ); ++$postDeleted; } else { agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): KEEPING post ID {$post->ID} (event ID $post_event_id_str) - found in feed", agile_tix::LOG_VERBOSE ); } } $total_posts_checked = count( $all_posts_to_check ); agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): ==== CULLING COMPLETE ====' ); agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): Total posts checked: ' . $total_posts_checked ); agile_tix::agile_tix_log_event( "Auto-Cull (Coming Soon): Total posts deleted: $postDeleted" ); agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): Posts kept: ' . ( $total_posts_checked - $postDeleted ) ); agile_tix::agile_tix_log_event( 'Auto-Cull (Coming Soon): ==== END CULLING ====' ); } else { // Main feed cleanup // IMPORTANT: Exclude coming soon posts from main feed culling // Coming soon posts are managed by their own culling logic $all_existing_posts = get_posts( array( 'post_type' => $context['postType'], 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => 'ATS_EventID', 'value' => '', 'compare' => '!=', ), array( 'key' => 'ATS_ComingSoonDate', 'compare' => 'NOT EXISTS', // Exclude coming soon posts ), ), ) ); $existingPostsCount = is_array( $all_existing_posts ) ? count( $all_existing_posts ) : 0; agile_tix::agile_tix_log_event( "Auto-Cull (Main Feed): Found $existingPostsCount existing main feed posts (coming soon posts excluded)", agile_tix::LOG_VERBOSE ); $event_id_to_posts = array(); foreach ( $all_existing_posts as $post ) { $post_event_id_str = (string) get_post_meta( $post->ID, 'ATS_EventID', true ); if ( '' === $post_event_id_str ) { continue; } if ( ! isset( $event_id_to_posts[ $post_event_id_str ] ) ) { $event_id_to_posts[ $post_event_id_str ] = array(); } $event_id_to_posts[ $post_event_id_str ][] = $post; } $duplicate_ids_deleted = array(); foreach ( $event_id_to_posts as $event_id_str => $posts_with_same_id ) { if ( count( $posts_with_same_id ) < 2 ) { continue; } usort( $posts_with_same_id, function ( $a, $b ) { return (int) $a->ID - (int) $b->ID; } ); $post_to_keep = $posts_with_same_id[0]; $posts_to_delete = array_slice( $posts_with_same_id, 1 ); agile_tix::agile_tix_log_event( 'Auto-Cull (Main Feed): Found ' . count( $posts_with_same_id ) . " duplicate posts for event ID $event_id_str, keeping post ID {$post_to_keep->ID}" ); foreach ( $posts_to_delete as $duplicate_post ) { agile_tix::agile_tix_log_event( "Auto-Cull (Main Feed): DELETING duplicate post ID {$duplicate_post->ID} (event ID $event_id_str)" ); $media_id = get_post_meta( $duplicate_post->ID, 'ATS_MediaLib_Img_ID', true ); if ( ! empty( $media_id ) && is_numeric( $media_id ) && $media_id > 0 ) { wp_delete_post( (int) $media_id, true ); } wp_delete_post( $duplicate_post->ID, true ); $duplicate_ids_deleted[] = $duplicate_post->ID; ++$postDeleted; } } if ( ! empty( $duplicate_ids_deleted ) ) { $all_existing_posts = array_values( array_filter( $all_existing_posts, function ( $p ) use ( $duplicate_ids_deleted ) { return ! in_array( $p->ID, $duplicate_ids_deleted, true ); } ) ); } $current_feed_event_ids = array(); foreach ( $context['events'] ?? array() as $event ) { if ( isset( $event['ID'] ) ) { $event_id_str = (string) $event['ID']; if ( ! in_array( $event_id_str, $current_feed_event_ids, true ) ) { $current_feed_event_ids[] = $event_id_str; } } } foreach ( $context['filtered_events'] ?? array() as $event ) { if ( isset( $event['ID'] ) ) { $event_id_str = (string) $event['ID']; if ( ! in_array( $event_id_str, $current_feed_event_ids, true ) ) { $current_feed_event_ids[] = $event_id_str; } } } $processedEventIds = isset( $context['processed_event_ids'] ) && is_array( $context['processed_event_ids'] ) ? $context['processed_event_ids'] : array(); foreach ( $processedEventIds as $event_id => $post_id ) { $event_id_str = (string) $event_id; if ( ! in_array( $event_id_str, $current_feed_event_ids, true ) ) { $current_feed_event_ids[] = $event_id_str; } } // agile_tix::agile_tix_log_event("Auto-Cull (Main Feed): DEBUG - Built current_feed_event_ids from: events=" . count($context['events'] ?? []) . ", filtered_events=" . count($context['filtered_events'] ?? []) . ", processed_event_ids=" . count($processedEventIds) . ", total=" . count($current_feed_event_ids)); $feedEventIdsCount = is_array( $current_feed_event_ids ) ? count( $current_feed_event_ids ) : 0; // agile_tix::agile_tix_log_event("Auto-Cull (Main Feed): Current feed has $feedEventIdsCount event IDs"); $just_processed_event_ids = array(); if ( isset( $context['filtered_events'] ) && is_array( $context['filtered_events'] ) ) { foreach ( $context['filtered_events'] as $event ) { if ( isset( $event['ID'] ) ) { $just_processed_event_ids[] = (string) $event['ID']; } } } if ( isset( $context['processed_event_ids'] ) && is_array( $context['processed_event_ids'] ) ) { foreach ( $context['processed_event_ids'] as $event_id => $post_id ) { $just_processed_event_ids[] = (string) $event_id; } } $just_processed_event_ids = array_unique( $just_processed_event_ids ); agile_tix::agile_tix_log_event( 'Auto-Cull (Main Feed): Protecting ' . count( $just_processed_event_ids ) . ' just-processed event IDs from deletion', agile_tix::LOG_VERBOSE ); // Find posts for events that are no longer in the feed foreach ( $all_existing_posts as $post ) { $post_event_id = get_post_meta( $post->ID, 'ATS_EventID', true ); $post_event_id_str = (string) $post_event_id; // Double-check: Skip if this is a coming soon post (shouldn't happen due to query, but safety check) $has_coming_soon_date = get_post_meta( $post->ID, 'ATS_ComingSoonDate', true ); if ( ! empty( $has_coming_soon_date ) ) { agile_tix::agile_tix_log_event( "Auto-Cull (Main Feed): SKIP post ID {$post->ID} (event ID $post_event_id_str) - has ComingSoonDate (should not be in query)", agile_tix::LOG_VERBOSE ); continue; } if ( in_array( $post_event_id_str, $just_processed_event_ids, true ) ) { agile_tix::agile_tix_log_event( "Auto-Cull (Main Feed): PROTECTING post ID {$post->ID} (event ID $post_event_id_str) - was just processed", agile_tix::LOG_VERBOSE ); continue; } if ( ! in_array( $post_event_id_str, $current_feed_event_ids, true ) ) { agile_tix::agile_tix_log_event( "Auto-Cull (Main Feed): Deleting post ID {$post->ID} (event ID $post_event_id_str) - not in current main feed", agile_tix::LOG_VERBOSE ); $mediaID = get_post_meta( $post->ID, 'ATS_MediaLib_Img_ID', true ); if ( ! empty( $mediaID ) && is_numeric( $mediaID ) && $mediaID > 0 ) { wp_delete_post( (int) $mediaID, true ); } wp_delete_post( $post->ID, true ); ++$postDeleted; } } agile_tix::agile_tix_log_event( 'Auto-Cull (Main Feed): complete' . ' | deleted=' . $postDeleted . ' | duplicates_deleted=' . count( $duplicate_ids_deleted ) . ' | feed_ids=' . count( $current_feed_event_ids ) . ' | existing_posts_checked=' . count( $all_existing_posts ) ); } } // Update final statistics update_option( 'agile_field_utility_last_post_count_deleated', $postDeleted ); // Calculate runtime $endTime = microtime( true ); update_option( 'agile_field_utility_last_known_runtime', ( $endTime - $startTime ) ); update_option( 'agile_field_utility_last_post_update', current_time( 'timestamp' ) ); // Check if coming soon feed needs to be processed (only for main feed, not coming soon feed itself) $isComingSoonFeed = $context['isComingSoonFeed'] ?? false; agile_tix::agile_tix_log_event( 'Finalization: post-cull handoff' . ' | refresh_token=' . ( $context['refresh_token'] ?? 'none' ) . ' | isComingSoonFeed=' . ( $isComingSoonFeed ? 'true' : 'false' ) . ' | posts_deleted_this_finalize=' . $postDeleted . ' | AS=' . agile_tix_describe_pending_actions() ); // If this is coming soon feed finalization, verify posts exist if ( $isComingSoonFeed ) { agile_tix::agile_tix_log_event( '=== Coming Soon Feed Finalization ===' ); // agile_tix::agile_tix_log_event("Coming Soon Feed Finalization: Verifying created posts exist"); $verify_args = array( 'post_type' => $context['postType'], 'posts_per_page' => -1, 'post_status' => 'any', // Check all statuses 'meta_query' => array( array( 'key' => 'ATS_ComingSoonDate', 'compare' => 'EXISTS', ), ), ); $verify_query = new WP_Query( $verify_args ); $found_posts = array(); if ( $verify_query->have_posts() ) { while ( $verify_query->have_posts() ) { $verify_query->the_post(); $post_id = get_the_ID(); $event_id = get_post_meta( $post_id, 'ATS_EventID', true ); $coming_soon_date = get_post_meta( $post_id, 'ATS_ComingSoonDate', true ); $post_status = get_post_status( $post_id ); $post_title = get_the_title( $post_id ); $found_posts[] = array( 'ID' => $post_id, 'Title' => $post_title, 'EventID' => $event_id, 'Status' => $post_status, 'ComingSoonDate' => $coming_soon_date, ); } wp_reset_postdata(); } $found_count = count( $found_posts ); // agile_tix::agile_tix_log_event("Coming Soon Feed Finalization: Found $found_count posts with ComingSoonDate meta"); // foreach ($found_posts as $post_info) { // agile_tix::agile_tix_log_event("Coming Soon Feed Finalization: Post ID {$post_info['ID']} - '{$post_info['Title']}' (EventID: {$post_info['EventID']}, Status: {$post_info['Status']}, ComingSoonDate: {$post_info['ComingSoonDate']})"); // } } if ( ! $isComingSoonFeed ) { // This is main feed finalization - check if coming soon feed needs processing $useComingSoon = $context['useComingSoon'] ?? false; $comingSoonData = $context['comingSoonData'] ?? null; $comingSoonEventIds = $context['comingSoonEventIds'] ?? array(); // Get variables from context for coming soon feed processing $comingSoonPostType = $context['postType']; $comingSoonThemeType = $context['themeType']; $comingSoonCommentStat = $context['commentStat']; $comingSoonPostStat = $context['postStat']; $comingSoonDeepRefresh = $context['deepRefresh']; $comingSoonAutoCull = $context['autoCull']; $comingSoonStartTime = $context['startTime']; // Re-query after main feed processing; the refresh-start snapshot is stale on new installs. $main_feed_source_event_ids = array(); foreach ( $context['events'] ?? array() as $main_event ) { if ( isset( $main_event['ID'] ) ) { $main_feed_source_event_ids[] = (string) $main_event['ID']; } } // Clean up temporary options before processing coming soon (to avoid conflicts) agile_tix::agile_tix_log_event( 'Finalization: DELETING main-feed context before coming-soon handoff' . ' | refresh_token=' . ( $context['refresh_token'] ?? 'none' ) . ' | ASSUMPTION: any still-pending main-feed batch actions will ABANDON after this' . ' | AS=' . agile_tix_describe_pending_actions() ); delete_option( 'agile_processing_context' ); delete_option( 'agile_total_chunks' ); delete_option( 'agile_processed_chunks' ); // Process coming soon feed if enabled and data is available agile_tix::agile_tix_log_event( 'Finalization: coming-soon handoff check' . ' | useComingSoon=' . ( $useComingSoon ? 'true' : 'false' ) . ' | comingSoonDataType=' . gettype( $comingSoonData ) . ' | hasArrayOfShows=' . ( is_array( $comingSoonData ) && isset( $comingSoonData['ArrayOfShows'] ) ? 'yes' : 'no' ) ); if ( $useComingSoon && $comingSoonData && gettype( $comingSoonData ) != 'string' && isset( $comingSoonData['ArrayOfShows'] ) ) { agile_tix::agile_tix_log_event( 'Finalization: Triggering process_coming_soon_feed (' . count( $comingSoonData['ArrayOfShows'] ) . ' shows in payload)' ); agile_tix_Options::update_utility_status( 'Processing Coming Soon Feed' ); self::process_coming_soon_feed( $comingSoonData, $comingSoonEventIds, $comingSoonPostType, $comingSoonThemeType, $comingSoonCommentStat, $comingSoonPostStat, $comingSoonDeepRefresh, $comingSoonAutoCull, $comingSoonStartTime, $main_feed_source_event_ids ); // Don't clean up context yet - coming soon feed will use it agile_tix::agile_tix_log_event( 'Finalization: Coming soon feed processing initiated, exiting main finalize' ); return; // Exit early - coming soon feed will finalize later } elseif ( $useComingSoon && $comingSoonData && gettype( $comingSoonData ) != 'string' && ! isset( $comingSoonData['ArrayOfShows'] ) ) { agile_tix::agile_tix_log_event( 'Main Feed Finalization: Coming soon feed data is missing ArrayOfShows key. Data structure may be invalid.' ); } elseif ( $useComingSoon ) { agile_tix::agile_tix_log_event( 'Finalization: Coming soon enabled but data not usable — skipping to Idle' ); } } // Clean up temporary options (for coming soon feed finalization or main feed without coming soon) agile_tix::agile_tix_log_event( 'Finalization: DELETING context and setting Idle' . ' | refresh_token=' . ( isset( $context['refresh_token'] ) ? $context['refresh_token'] : 'n/a' ) . ' | isComingSoonFeed=' . ( $isComingSoonFeed ? 'true' : 'false' ) . ' | created=' . get_option( 'agile_field_utility_last_post_count_created', 0 ) . ' | updated=' . get_option( 'agile_field_utility_last_post_count_updated', 0 ) . ' | deleted=' . get_option( 'agile_field_utility_last_post_count_deleated', 0 ) . ' | AS=' . agile_tix_describe_pending_actions() ); delete_option( 'agile_processing_context' ); delete_option( 'agile_total_chunks' ); delete_option( 'agile_processed_chunks' ); self::agile_release_refresh_lock(); // Set status to Idle agile_tix_Options::update_utility_status( 'Idle' ); agile_tix::agile_tix_log_event( '=== FINALIZATION COMPLETE (Idle) ===' ); // error_log('Agile Tix: Processing completed - ' . $postDeleted . ' posts deleted'); } /** * Process coming soon feed events * Only processes events that don't exist in main feed */ public static function process_coming_soon_feed( $comingSoonData, $comingSoonEventIds, $postType, $themeType, $commentStat, $postStat, $deepRefresh, $autoCull, $startTime, $main_feed_source_event_ids = array() ) { agile_tix::agile_tix_log_event( '=== Coming Soon Feed Processing START ===' . ' postType=' . $postType . ' themeType=' . $themeType . ' deepRefresh=' . ( $deepRefresh ? 'true' : 'false' ) . ' | AS=' . agile_tix_describe_pending_actions() ); if ( ! isset( $comingSoonData['ArrayOfShows'] ) ) { agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: ERROR - ArrayOfShows not found in comingSoonData' ); return; } $totalEventsInFeed = ( isset( $comingSoonData['ArrayOfShows'] ) && is_array( $comingSoonData['ArrayOfShows'] ) ) ? count( $comingSoonData['ArrayOfShows'] ) : 0; agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: Total events in feed = ' . $totalEventsInFeed ); // Main feed IDs must reflect posts created during this refresh, not the refresh-start snapshot. $main_feed_event_ids_list = self::agile_get_main_feed_event_ids( $postType ); $main_feed_event_ids = array(); foreach ( $main_feed_event_ids_list as $event_id ) { $main_feed_event_ids[ (string) $event_id ] = true; } $main_feed_source_ids = array(); foreach ( $main_feed_source_event_ids as $event_id ) { $main_feed_source_ids[ (string) $event_id ] = true; } $mainFeedCount = is_array( $main_feed_event_ids ) ? count( $main_feed_event_ids ) : 0; $storedCount = is_array( $main_feed_event_ids_from_context ) ? count( $main_feed_event_ids_from_context ) : 0; // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: Using $storedCount stored main feed event IDs (no database query needed)"); // First, collect ALL event IDs from the coming soon feed response for culling comparison $all_coming_soon_feed_event_ids = array(); foreach ( $comingSoonData['ArrayOfShows'] as $event ) { if ( isset( $event['ID'] ) ) { $all_coming_soon_feed_event_ids[] = (string) $event['ID']; } } agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: Collected ' . count( $all_coming_soon_feed_event_ids ) . ' event IDs from feed: ' . implode( ', ', $all_coming_soon_feed_event_ids ) ); // Filter coming soon events: must have ComingSoonDate and not exist as a main feed post // Check database for existing posts with this event ID (not just the current refresh list) $comingSoonEvents = array(); $skippedNoComingSoonDate = 0; $skippedInMainFeed = 0; foreach ( $comingSoonData['ArrayOfShows'] as $event ) { $eventID = $event['ID'] ?? 'NO_ID'; $eventName = $event['Name'] ?? 'NO_NAME'; // Only process events with ComingSoonDate if ( ! isset( $event['ComingSoonDate'] ) ) { ++$skippedNoComingSoonDate; agile_tix::agile_tix_log_event( "Coming Soon Feed Processing: Skipping event $eventID ($eventName) - no ComingSoonDate field", agile_tix::LOG_VERBOSE ); continue; } $event_id_key = (string) $eventID; if ( isset( $main_feed_source_ids[ $event_id_key ] ) || isset( $main_feed_event_ids[ $event_id_key ] ) ) { ++$skippedInMainFeed; continue; } $existing_post = get_posts( array( 'post_type' => $postType, 'posts_per_page' => 1, 'meta_query' => array( array( 'key' => 'ATS_EventID', 'value' => (string) $eventID, 'compare' => '=', ), ), ) ); if ( ! empty( $existing_post ) ) { $found_post_id = $existing_post[0]->ID; $has_coming_soon_date = get_post_meta( $found_post_id, 'ATS_ComingSoonDate', true ); $showing_ids = get_post_meta( $found_post_id, 'ATS_showingIDs', true ); $has_showings = is_array( $showing_ids ) ? ! empty( $showing_ids ) : ! empty( $showing_ids ); if ( empty( $has_coming_soon_date ) || $has_showings ) { ++$skippedInMainFeed; agile_tix::agile_tix_log_event( "Coming Soon Feed Processing: Skipping event $eventID ($eventName) - already exists as main feed post (post ID: $found_post_id)", agile_tix::LOG_VERBOSE ); continue; } } $comingSoonEvents[] = $event; } $comingSoonEventsCount = is_array( $comingSoonEvents ) ? count( $comingSoonEvents ) : 0; agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: Filtered results' . ' | to_process=' . $comingSoonEventsCount . ' | skipped_noComingSoonDate=' . $skippedNoComingSoonDate . ' | skipped_inMainFeed=' . $skippedInMainFeed ); // Get existing coming soon posts (exclude those that are in main feed) // Note: We need to do this even if $comingSoonEvents is empty, so culling can run to delete posts that fell off the feed $coming_soon_event_ids = array(); $coming_soon_args = array( 'post_type' => $postType, 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => 'ATS_EventID', 'value' => '', 'compare' => '!=', ), array( 'key' => 'ATS_ComingSoonDate', 'compare' => 'EXISTS', ), ), ); $coming_soon_query = new WP_Query( $coming_soon_args ); if ( $coming_soon_query->have_posts() ) { while ( $coming_soon_query->have_posts() ) { $coming_soon_query->the_post(); $event_id = get_post_meta( get_the_ID(), 'ATS_EventID', true ); if ( ! empty( $event_id ) ) { // Only include if not in main feed (to avoid treating enriched main feed posts as coming soon posts) if ( ! isset( $main_feed_event_ids[ (string) $event_id ] ) ) { $coming_soon_event_ids[ (string) $event_id ] = get_the_ID(); } } } wp_reset_postdata(); } $existingComingSoonCount = is_array( $coming_soon_event_ids ) ? count( $coming_soon_event_ids ) : 0; // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: Found $existingComingSoonCount existing coming soon posts"); // Filter events that need updates // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: Filtering events for refresh (deepRefresh = " . ($deepRefresh ? 'true' : 'false') . ")"); $filter_result = self::agile_filter_events_for_refresh( $comingSoonEvents, $coming_soon_event_ids, $deepRefresh ); $filtered_coming_soon_events = isset( $filter_result['filtered_events'] ) && is_array( $filter_result['filtered_events'] ) ? $filter_result['filtered_events'] : array(); $processed_coming_soon_event_ids = isset( $filter_result['processed_event_ids'] ) && is_array( $filter_result['processed_event_ids'] ) ? $filter_result['processed_event_ids'] : array(); $filteredCount = count( $filtered_coming_soon_events ); agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: After refresh filter - ' . $filteredCount . ' events need processing' ); // Store processing context for coming soon feed // Store both filtered events (for processing) and all events (for culling) $coming_soon_context = array( 'deepRefresh' => $deepRefresh, 'commentStat' => $commentStat, 'postStat' => $postStat, 'themeType' => $themeType, 'postType' => $postType, 'event_ids' => $coming_soon_event_ids, 'processed_event_ids' => $processed_coming_soon_event_ids, 'autoCull' => $autoCull, 'startTime' => $startTime, 'total_events' => $filteredCount, 'events' => $comingSoonEvents, // Filtered coming soon events (for processing) 'filtered_events' => $filtered_coming_soon_events, // Only events that need processing 'isComingSoonFeed' => true, 'main_feed_event_ids' => $main_feed_event_ids_list, // Store for culling (live main-feed IDs) 'all_coming_soon_feed_event_ids' => $all_coming_soon_feed_event_ids, // ALL event IDs from feed response (for culling comparison) ); update_option( 'agile_processing_context', $coming_soon_context ); agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: Stored context at ' . current_time( 'mysql' ) ); agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: Context isComingSoonFeed = ' . ( $coming_soon_context['isComingSoonFeed'] ? 'true' : 'false' ) ); agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: Context all_coming_soon_feed_event_ids count = ' . ( isset( $coming_soon_context['all_coming_soon_feed_event_ids'] ) ? count( $coming_soon_context['all_coming_soon_feed_event_ids'] ) : 0 ) ); // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: DEBUG - Context['events'] count: " . (isset($coming_soon_context['events']) ? count($coming_soon_context['events']) : 0)); // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: DEBUG - Context['filtered_events'] count: " . (isset($coming_soon_context['filtered_events']) ? count($coming_soon_context['filtered_events']) : 0)); // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: DEBUG - Context['processed_event_ids'] count: " . (isset($coming_soon_context['processed_event_ids']) ? count($coming_soon_context['processed_event_ids']) : 0)); // if (isset($coming_soon_context['events']) && is_array($coming_soon_context['events'])) { // $event_ids_in_context = array(); // foreach ($coming_soon_context['events'] as $event) { // if (isset($event['ID'])) { // $event_ids_in_context[] = (string)$event['ID']; // } // } // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: DEBUG - Event IDs in context['events']: " . implode(', ', $event_ids_in_context)); // } // Process in chunks $chunksize = get_option( 'agile_chunk_size' ) ?? 10; $chunksize = (int) $chunksize; if ( $chunksize <= 0 ) { $chunksize = 10; } $chunks = is_array( $filtered_coming_soon_events ) ? array_chunk( $filtered_coming_soon_events, $chunksize ) : array(); $total_chunks = is_array( $chunks ) ? count( $chunks ) : 0; // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: Chunking $filteredCount events into $total_chunks chunks (chunk size = $chunksize)"); update_option( 'agile_total_chunks', $total_chunks ); update_option( 'agile_processed_chunks', 0 ); // If there are no events to process, still schedule finalization for culling // This ensures posts that fell off the feed get deleted if ( $total_chunks === 0 ) { agile_tix::agile_tix_log_event( 'Coming Soon Feed Processing: no chunks — scheduling finalize for culling only' ); $scheduled_time = time(); agile_tix_schedule_single_action( $scheduled_time, 'agile_finalize_processing', array(), 'agile_events_processing' ); agile_tix_kick_queue_after_schedule( 'AgileTix ComingSoon Cull-Only' ); return; // No events to process, but culling will run } // Schedule each chunk $scheduledChunks = 0; foreach ( $chunks as $index => $chunk ) { $chunk_key = 'agile_chunk_data_' . $index . '_' . uniqid() . '_' . time(); update_option( $chunk_key, $chunk, false ); $chunkEventCount = is_array( $chunk ) ? count( $chunk ) : 0; // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: Scheduling chunk $index with $chunkEventCount events (chunk_key: $chunk_key)"); $action_id = agile_tix_schedule_single_action( time(), 'agile_process_events_batch', array( 'chunk_key' => $chunk_key, 'chunk_index' => $index, 'is_final_chunk' => ( $index === $total_chunks - 1 ), ), 'agile_events_processing' ); if ( ! $action_id ) { agile_tix::agile_tix_log_event( "Coming Soon Feed Processing: ERROR - Failed to schedule chunk $index" ); delete_option( $chunk_key ); } else { ++$scheduledChunks; // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: Successfully scheduled chunk $index (action_id: $action_id)"); } } // agile_tix::agile_tix_log_event("Coming Soon Feed Processing: Scheduled $scheduledChunks of $total_chunks chunks"); if ( $scheduledChunks > 0 ) { agile_tix::agile_tix_log_event( "Coming Soon Feed Processing: Scheduled $scheduledChunks of $total_chunks chunks — kicking AS queue" ); agile_tix_kick_queue_after_schedule( 'AgileTix ComingSoon' ); } } /** * Creates a single post of the agile post type * * @param mixed $event * @param string $commentStat (closed or open) * @param string $postStat (draft or publish) * @param bool $isNewEvent * @param int $postID */ static function agile_post_creator( $event, $commentStat, $postStat, $isNewEvent, $postID, $comingSoonEventData = null ) { $eventID = $event['ID'] ?? 'NO_ID'; $eventName = $event['Name'] ?? 'NO_NAME'; // agile_tix::agile_tix_log_event("Post Creator: FUNCTION ENTERED for event $eventID ($eventName) - isNewEvent: " . ($isNewEvent ? 'true' : 'false') . ", postID: $postID"); // agile_tix::agile_tix_log_event("agile_post_creator run"); // $executionTime = new ExecutionTime('agile_post_creator content collection'); // $executionTime->start(); $thumbnailID = ''; $EventImage = $event['EventImage'] ?? ''; // agile_tix::agile_tix_log_event("Post Creator: Event $eventID - Processing thumbnail, EventImage: " . ($EventImage ?: 'empty')); if ( ! empty( $EventImage ) ) { $mediaLibImg = self::add_image_to_media_library_from_uri( $EventImage ); if ( $mediaLibImg && ! is_wp_error( $mediaLibImg ) && $mediaLibImg && array_key_exists( 'url', $mediaLibImg ) && array_key_exists( 'attachment_id', $mediaLibImg ) ) { $thumbnailID = $mediaLibImg['attachment_id']; } } // agile_tix::agile_tix_log_event("Post Creator: Event $eventID - Thumbnail processing complete, thumbnailID: " . ($thumbnailID ?: 'empty')); // agile_tix::agile_tix_log_event("agile_post_creator post event img search"); // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); // $executionTime = new ExecutionTime('agile_post_creator $post_data binding'); // $executionTime->start(); // Initialize tabContainer - coming soon events don't have CurrentShowings $tabContainer = ''; $hasCurrentShowings = array_key_exists( 'CurrentShowings', $event ); // agile_tix::agile_tix_log_event("Post Creator: Event $eventID - Checking CurrentShowings, exists: " . ($hasCurrentShowings ? 'true' : 'false')); if ( $hasCurrentShowings ) { // Group showtimes by date $groupedShowings = array(); $showingIDs = array(); foreach ( $event['CurrentShowings'] as $show ) { $showingIDs[] = $show['ID']; $displayDate = $show['DisplayDate'] ?? ''; if ( $displayDate ) { $dateOnly = wp_date( 'F j, Y', strtotime( $displayDate ) ); $groupedShowings[ $dateOnly ][] = $show; } } if ( empty( $groupedShowings ) ) { $tabContainer = ''; } else { $tabContainer = '
'; $isFirst = true; foreach ( $groupedShowings as $date => $shows ) { $tabId = 'tab_' . md5( $date ); $activeClass = $isFirst ? ' active' : ''; // Use shorter date format for tabs $shortDate = wp_date( 'M j', strtotime( $date ) ); if ( wp_date( 'Y', strtotime( $date ) ) !== wp_date( 'Y' ) ) { $shortDate .= ', ' . wp_date( 'Y', strtotime( $date ) ); } $button_html = ''; // Allow data-tab-id attribute $allowed_html = array( 'button' => array( 'class' => true, 'data-tab-id' => true, ), ); $tabContainer .= wp_kses( $button_html, $allowed_html ); $isFirst = false; } $tabContainer .= '
'; // Tab contents $tabContainer .= '
'; $isFirst = true; foreach ( $groupedShowings as $date => $shows ) { $tabId = 'tab_' . md5( $date ); $style = $isFirst ? ' style="display:block;"' : ''; $tabContainer .= '
'; $tabContainer .= '
'; foreach ( $shows as $show ) { $tabContainer .= self::agile_create_showing_tab_content_showslist( $show ); } $tabContainer .= '
'; $isFirst = false; } $tabContainer .= '
'; $tabContainer .= ''; } } // agile_tix::agile_tix_log_event("Post Creator: Event $eventID - Tab container processing complete, tabContainer length: " . strlen($tabContainer)); // agile_tix::agile_tix_log_event("pre postdata "); // agile_tix::agile_tix_log_event("Post Creator: Event $eventID - About to create post_data array"); // Get the configured post type (respects theme settings) $configured_post_type = agile_tix_post_type::get_configured_post_type(); if ( empty( $configured_post_type ) ) { $configured_post_type = 'agile_post'; // Fallback to default } // agile_tix::agile_tix_log_event("Post Creator: Event $eventID - Using configured post type: $configured_post_type"); $post_data = array( 'ID' => $postID, 'post_title' => $event['Name'], 'post_content' => self::agile_create_universal_content_body( $event ) . $tabContainer . '
' . $event['CorpOrgName'] . '
', 'post_status' => $postStat, 'post_type' => $configured_post_type, 'post_author' => 1, // get_user_by('id', 1)->user_id, // we want to use meta instead of tax_input for important data, if the user doesn't have perms to manage taxonomy, the process fails 'comment_status' => $commentStat, /* 'meta_input' => array( //meta data "ATS_ModifiedDate" => agile_tix_post_creator::agile_get_most_recent_lastUpdate($event) ?? '', "ATS_EventID" => $event['ID'] ?? '', "ATS_ExternalEventID" => $event['ExternalEventID'] ?? '', "ATS_CorpOrgID" => $event['CorpOrgID'] ?? '', "ATS_CorpOrgName" => $event['CorpOrgName'] ?? '', "ATS_OrgID" => $event['OrgID'] ?? '', "ATS_Name" => $event['Name'] ?? '', "ATS_Distributor" => $event['Distributor'] ?? '', "ATS_ExtraHTML" => $event['ExtraHTML'] ?? '', "ATS_DisplayColor" => $event['DisplayColor'] ?? '', //"ATS_EventImage" => $event['EventImage'] ?? '', //"ATS_EventThumbImage" => $event['EventThumbImage'] ?? '', //"ATS_Event_Img_Hash" => (empty($event['EventImage'])) ? '' : $event['EventImage'],//This line causes issues with irregular chars "ATS_MediaLib_Img_ID" => $thumbnailID ?? '', "ATS_firstEndDate" => $event['CurrentShowings'][0]['EndDate'] ?? '', "ATS_showingIDs" => $showingIDs ?? [], "ATS_HideFeatImg" => get_option('agile_field_render_carousel') ?? 0 ),*/ 'meta_input' => self::agile_create_universal_meta_fields( $event, $thumbnailID, $comingSoonEventData ), ); // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); // agile_tix::agile_tix_log_event("post postdata "); // $executionTime = new ExecutionTime('agile_post_creator genre parsing'); // $executionTime->start(); $genreValues = array(); if ( ! empty( $event['CustomProperties'] ) ) { // parse genre for taxonomy $names = array_column( $event['CustomProperties'], 'Name' ); $values = array_column( $event['CustomProperties'], 'Value' ); $indices = array_keys( $names, 'Genre' ); $genreValues = array_map( function ( $index ) use ( $values ) { return $values[ $index ]; }, $indices ); } // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); // $executionTime = new ExecutionTime('agile_post_creator post update/insert'); // $executionTime->start(); // agile_tix::agile_tix_log_event('agile_post_creator pre kses filter'); // Temporarily allow SVG elements for post content add_filter( 'wp_kses_allowed_html', 'agile_allow_svg_in_posts', 10, 2 ); // agile_tix::agile_tix_log_event('agile_post_creator post kses filter'); $eventID = $event['ID'] ?? 'NO_ID'; $eventName = $event['Name'] ?? 'NO_NAME'; $hasComingSoonDate = isset( $event['ComingSoonDate'] ) ? 'true' : 'false'; // Log post data details before creation $postStatus = $post_data['post_status'] ?? 'NOT SET'; $postType = $post_data['post_type'] ?? 'NOT SET'; $hasMetaInput = isset( $post_data['meta_input'] ) ? 'true' : 'false'; $comingSoonInMeta = isset( $post_data['meta_input']['ATS_ComingSoonDate'] ) ? $post_data['meta_input']['ATS_ComingSoonDate'] : 'NOT IN META'; // agile_tix::agile_tix_log_event("Post Creator: Pre-creation check for event $eventID - Status: $postStatus, Type: $postType, HasMetaInput: $hasMetaInput, ComingSoonDate in meta: $comingSoonInMeta"); if ( $isNewEvent ) { // insert a new post // agile_tix::agile_tix_log_event("Post Creator: Creating NEW post for event $eventID ($eventName) - ComingSoonDate = $hasComingSoonDate, postType = $postType, postStatus = $postStatus"); $postID = wp_insert_post( $post_data ); if ( is_wp_error( $postID ) ) { agile_tix::agile_tix_log_event( "Post Creator: ERROR creating post for event $eventID - " . $postID->get_error_message() ); } else { // agile_tix::agile_tix_log_event("Post Creator: Successfully created post ID $postID for event $eventID ($eventName)"); // agile_tix::agile_tix_log_event("Post Creator: DEBUG - Post creation timestamp: " . current_time('mysql')); // $savedEventID = get_post_meta($postID, 'ATS_EventID', true); // agile_tix::agile_tix_log_event("Post Creator: DEBUG - Saved EventID meta: $savedEventID"); // $savedComingSoonDate = get_post_meta($postID, 'ATS_ComingSoonDate', true); // agile_tix::agile_tix_log_event("Post Creator: DEBUG - Saved ComingSoonDate meta: " . ($savedComingSoonDate ?: 'NOT SET')); // $savedEventID = get_post_meta($postID, 'ATS_EventID', true); // $postStatus = get_post_status($postID); // $savedPostType = get_post_type($postID); // $postObject = get_post($postID); // $postObjectPostType = $postObject ? $postObject->post_type : 'NOT FOUND'; // agile_tix::agile_tix_log_event("Post Creator: Post $postID verification - Status: $postStatus, Type (get_post_type): $savedPostType, Type (post object): $postObjectPostType, EventID: $savedEventID, ComingSoonDate: " . ($savedComingSoonDate ?: 'NOT SET')); // $test_query = new WP_Query(array( // 'post_type' => $savedPostType, // 'p' => $postID, // 'post_status' => 'any', // )); // $found_in_query = $test_query->have_posts() ? 'YES' : 'NO'; // agile_tix::agile_tix_log_event("Post Creator: Post $postID query test - Found in WP_Query: $found_in_query"); } // agile_tix::agile_tix_log_event("agile_post_creator wp_insert_post postID" . $postID); } else { // update an existing post // agile_tix::agile_tix_log_event("Post Creator: UPDATING existing post ID $postID for event $eventID ($eventName) - ComingSoonDate = $hasComingSoonDate"); $postID = wp_update_post( $post_data ); if ( is_wp_error( $postID ) ) { agile_tix::agile_tix_log_event( "Post Creator: ERROR updating post $postID for event $eventID - " . $postID->get_error_message() ); } else { // agile_tix::agile_tix_log_event("Post Creator: Successfully updated post ID $postID for event $eventID ($eventName)"); // Verify meta was saved // $savedComingSoonDate = get_post_meta($postID, 'ATS_ComingSoonDate', true); // $savedEventID = get_post_meta($postID, 'ATS_EventID', true); // $postStatus = get_post_status($postID); // agile_tix::agile_tix_log_event("Post Creator: Post $postID verification - Status: $postStatus, EventID: $savedEventID, ComingSoonDate: " . ($savedComingSoonDate ?: 'NOT SET')); } // agile_tix::agile_tix_log_event("agile_post_creator wp_update_postpostID" . $postID); } if ( $postID && ! is_wp_error( $postID ) ) { $has_showings = isset( $event['CurrentShowings'] ) && is_array( $event['CurrentShowings'] ) && ! empty( $event['CurrentShowings'] ); if ( $has_showings ) { delete_post_meta( $postID, 'ATS_ComingSoonDate' ); } } // agile_tix::agile_tix_log_event('agile_post_creator pre kses filter removal'); // Remove the SVG filter after post creation remove_filter( 'wp_kses_allowed_html', 'agile_allow_svg_in_posts', 10 ); // agile_tix::agile_tix_log_event('agile_post_creator post kses filter removal'); // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); // $executionTime = new ExecutionTime('agile_post_creator genre & thumbnail stuff'); // $executionTime->start(); if ( $thumbnailID != '' ) { set_post_thumbnail( $postID, $thumbnailID ); } wp_set_object_terms( $postID, $genreValues, 'agile_genre' ); // $executionTime->end(); // agile_tix::agile_tix_log_event($executionTime); } function wpse_kses_allowed_html( $allowed, $context ) { // agile_tix::agile_tix_log_event($executionTime); if ( $context != 'post' ) { // agile_tix::agile_tix_log_event("'post' !== context"); return $allowed; } $allowed['button']['onclick'] = true; return $allowed; } static function agile_group_custom_properties( $data ) { $grouped = array(); foreach ( $data as $property ) { $group = $property['Group']; $name = $property['Name']; if ( ! isset( $grouped[ $group ] ) ) { $grouped[ $group ] = array(); } if ( ! isset( $grouped[ $group ][ $name ] ) ) { $grouped[ $group ][ $name ] = array(); } $grouped[ $group ][ $name ][] = $property; } return $grouped; } /** * A function intended to parse content and user defined content options without styling for any post type */ static function agile_create_universal_content_body( $show ) { try { $content = ''; // media content $addMedia = $show['AdditionalMedia'] ?? array(); $mediaCar = self::generateMediaCarousel( $addMedia ); $content .= $mediaCar; // text content $InfoLink = $show['InfoLink'] ?? ''; $Duration = $show['Duration'] ?? ''; $Distributor = $show['Distributor'] ?? ''; $ShortDescription = $show['ShortDescription'] ?? ''; $FullDescription = $show['FullDescription'] ?? ''; $Type = $show['Type'] ?? ''; $ShortDescriptive1 = $show['ShortDescriptive1'] ?? ''; $ShortDescriptive2 = $show['ShortDescriptive2'] ?? ''; $ExtraHTML = $show['ExtraHTML'] ?? ''; $CustomProperties = agile_tix_amy_movie_integrate::groupCustomProperties( $show ); // agile_tix::agile_tix_log_event('$CustomProperties: ' . json_encode($CustomProperties)); $LocationDiv = ''; if ( isset( $show['CurrentShowings'] ) && ! empty( $show['CurrentShowings'] ) && isset( $show['CurrentShowings'][0] ) ) { if ( isset( $show['CurrentShowings'][0]['Venue'] ) && $show['CurrentShowings'][0]['Venue'] ) { $venue = $show['CurrentShowings'][0]['Venue'] ?? null; $name = $venue['Name'] ?? ''; $addressParts = array_filter( array( $venue['Address1'] ?? '', $venue['Address2'] ?? '', ( $venue['City'] ?? '' ) . ', ' . ( $venue['State'] ?? '' ) . ' ' . ( $venue['Zip'] ?? '' ), ) ); $address = implode( '
', $addressParts ); $cleanAddress = implode( ', ', $addressParts ); $LocationDiv .= '
Location: ' . $name . '
' . $address . '
'; $LocationDiv .= ''; $LocationDiv .= '
'; } } // agile_tix::agile_tix_log_event('Post creator - location html: ' . $LocationDiv); // Build content strictly from layout_json order and templates $settings = get_option( 'agile_options_content_settings' ); $layout = array(); if ( is_array( $settings ) && isset( $settings['layout_json'] ) ) { $decoded = json_decode( $settings['layout_json'], true ); if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) { $layout = $decoded; } } if ( ! empty( $layout ) ) { foreach ( $layout as $block ) { // agile_tix::agile_tix_log_event("Template Mapping - block: " . json_encode($block)); $type = isset( $block['type'] ) ? $block['type'] : ''; $fieldKey = isset( $block['field'] ) ? $block['field'] : ''; $template = isset( $block['text'] ) ? $block['text'] : ''; if ( $type === 'text' ) { if ( ! empty( trim( $template ) ) ) { $content .= '

' . esc_html( $template ) . '

'; } continue; } if ( $type === 'field' ) { switch ( $fieldKey ) { case 'agile_field_content_distributor': // agile_tix::agile_tix_log_event("Template Mapping - distributor: $Distributor, $template "); if ( ! empty( $Distributor ) && ! empty( trim( $template ) ) ) { // agile_tix::agile_tix_log_event("Template Mapping - distributor: $Distributor, $template "); $content .= '

' . str_replace( '{distributor}', $Distributor, $template ) . '

'; } break; case 'agile_field_content_duration': if ( ! empty( $Duration ) && ! empty( trim( $template ) ) ) { $content .= '

' . str_replace( '{duration}', $Duration, $template ) . '

'; } break; case 'agile_field_content_infolink': // agile_tix::agile_tix_log_event('Post creator case agile_field_content_infoLink ' . InfoLink . " " . $template); if ( $InfoLink && ! empty( trim( $InfoLink ) ) && ! empty( trim( $template ) ) ) { $content .= '' . esc_html( $template ) . '
'; } break; case 'agile_field_content_short_description': if ( ! empty( $ShortDescription ) && ! empty( trim( $template ) ) ) { $content .= '

' . str_replace( '{short description}', $ShortDescription, $template ) . '

'; } break; case 'agile_field_content_full_description': if ( ! empty( $FullDescription ) && ! empty( trim( $template ) ) ) { $content .= '

' . str_replace( '{full description}', $FullDescription, $template ) . '

'; } break; case 'agile_field_content_renderlocation': if ( ! empty( $LocationDiv ) ) { // textless field; ignore template and render widget $content .= $LocationDiv; } break; case 'agile_field_content_rating': // textless field; get Vote Count and Vote Average from CustomProperties $voteAverageValues = self::getCustomPropertyValues( $show, 'Vote Average' ); $voteCountValues = self::getCustomPropertyValues( $show, 'Vote Count' ); if ( ! empty( $voteAverageValues ) && ! empty( $voteCountValues ) ) { $voteAverage = floatval( $voteAverageValues[0] ); $voteCount = intval( $voteCountValues[0] ); // Round to nearest first decimal place $roundedAverage = round( $voteAverage, 1 ); // Get TMDB SVG URL $tmdb_svg_url = plugin_dir_url( __DIR__ ) . 'assets/ExternalAssets/tmdbShort.svg'; // Build rating display HTML $ratingDiv = '
'; $ratingDiv .= '
'; // Create 5 stars, filled based on rating (each star = 2 points out of 10) $starsValue = $roundedAverage / 2; // Convert to 5-star scale $fullStars = floor( $starsValue ); $hasHalfStar = ( $starsValue - $fullStars ) >= 0.5; for ( $i = 0; $i < 5; $i++ ) { if ( $i < $fullStars ) { $ratingDiv .= '★'; } elseif ( $i == $fullStars && $hasHalfStar ) { $ratingDiv .= '★'; } else { $ratingDiv .= '★'; } } $ratingDiv .= '
'; $ratingDiv .= '
'; $ratingDiv .= '' . esc_html( number_format( $roundedAverage, 1 ) ) . ''; $ratingDiv .= '/10'; $ratingDiv .= '(' . esc_html( number_format( $voteCount ) ) . ' votes)'; $ratingDiv .= '
'; $ratingDiv .= ''; $ratingDiv .= '
'; $content .= $ratingDiv; } break; case 'agile_field_content_type': if ( ! empty( $Type ) && ! empty( trim( $template ) ) ) { $content .= '

' . str_replace( '{type}', $Type, $template ) . '

'; } break; case 'agile_field_content_short_descriptive1': if ( ! empty( $ShortDescriptive1 ) && ! empty( trim( $template ) ) ) { $content .= '

' . str_replace( '{short descriptive1}', $ShortDescriptive1, $template ) . '

'; } break; case 'agile_field_content_short_descriptive2': if ( ! empty( $ShortDescriptive2 ) && ! empty( trim( $template ) ) ) { $content .= '

' . str_replace( '{short descriptive2}', $ShortDescriptive2, $template ) . '

'; } break; case 'agile_field_content_extra_html': if ( ! empty( $ExtraHTML ) && ! empty( trim( $template ) ) ) { // ExtraHTML may contain HTML, so we need to handle it differently // Replace the token in the template, then sanitize the result $renderedContent = str_replace( '{extra html}', $ExtraHTML, $template ); $sanitized_extra_html = agile_sanitize_feed_html( $renderedContent ); if ( '' !== $sanitized_extra_html ) { $content .= '
' . $sanitized_extra_html . '
'; } } break; } } if ( $type === 'custom-property' ) { // agile_tix::agile_tix_log_event("Cust Prop Template Mapping - is custom-property : " . json_encode($block)); $propertyName = isset( $block['propertyName'] ) ? trim( $block['propertyName'] ) : ''; $template = isset( $block['template'] ) ? $block['template'] : ''; if ( ! empty( $propertyName ) && ! empty( trim( $template ) ) ) { // Safely get custom property values from the event // agile_tix::agile_tix_log_event("Cust Prop Template Mapping - template : " . $template); $customPropertyValues = self::getCustomPropertyValues( $show, $propertyName ); // agile_tix::agile_tix_log_event("Cust Prop Template Mapping - customPropertyValues : " . json_encode($customPropertyValues)); if ( ! empty( $customPropertyValues ) ) { // Handle multiple values by joining them $valuesString = implode( ', ', $customPropertyValues ); // Replace the placeholder in the template $placeholder = '{' . $propertyName . '}'; $renderedContent = str_replace( $placeholder, $valuesString, $template ); $content .= '

' . esc_html( $renderedContent ) . '

'; } } } } } return $content; } catch ( Exception $e ) { agile_tix::agile_tix_log_event( $e->getMessage() ); } } /** * Safely extract custom property values from event data * Handles multiple values for the same property name */ static function getCustomPropertyValues( $event, $propertyName ) { $values = array(); try { // Check if CustomProperties exists and is an array if ( isset( $event['CustomProperties'] ) && is_array( $event['CustomProperties'] ) ) { // agile_tix::agile_tix_log_event('getCustomPropertyValues - Found CustomProperties array with ' . count($event['CustomProperties']) . ' items'); // agile_tix::agile_tix_log_event('getCustomPropertyValues - Looking for property: "' . $propertyName . '"'); foreach ( $event['CustomProperties'] as $index => $property ) { // agile_tix::agile_tix_log_event('getCustomPropertyValues - Property ' . $index . ': ' . json_encode($property)); if ( is_array( $property ) && isset( $property['Name'] ) && isset( $property['Value'] ) ) { $propName = trim( $property['Name'] ); $searchName = trim( $propertyName ); // Try exact match first if ( $propName === $searchName ) { $value = trim( $property['Value'] ); if ( ! empty( $value ) ) { $values[] = $value; // agile_tix::agile_tix_log_event('getCustomPropertyValues - Found exact match: ' . $value); } } // Try case-insensitive match elseif ( strtolower( $propName ) === strtolower( $searchName ) ) { $value = trim( $property['Value'] ); if ( ! empty( $value ) ) { $values[] = $value; // agile_tix::agile_tix_log_event('getCustomPropertyValues - Found case-insensitive match: ' . $value); } } } } } else { // agile_tix::agile_tix_log_event('getCustomPropertyValues - No CustomProperties found in event data'); } } catch ( Exception $e ) { // Log error but don't break the post creation agile_tix::agile_tix_log_event( 'Error extracting custom property "' . $propertyName . '": ' . $e->getMessage() ); } // agile_tix::agile_tix_log_event('getCustomPropertyValues - Returning ' . count($values) . ' values: ' . json_encode($values)); return $values; } static function agile_create_universal_meta_fields( $event, $thumbnailID, $comingSoonEventData = null ) { $metainput = array(); try { $showingIDs = array(); $showTimes = array(); // Handle CurrentShowings - coming soon events never have CurrentShowings if ( isset( $event['CurrentShowings'] ) && is_array( $event['CurrentShowings'] ) ) { foreach ( $event['CurrentShowings'] as $showing ) { array_push( $showingIDs, $showing['ID'] ); array_push( $showTimes, array( $showing['DisplayDate'] => $showing['LegacyPurchaseLink'] ) ); } } $metainput['ATS_showingIDs'] = $showingIDs ?? array(); $metainput['ATS_ModifiedDate'] = self::agile_get_most_recent_lastUpdate( $event ) ?? ''; $metainput['ATS_EventID'] = $event['ID'] ?? ''; // Coming soon events might not have CurrentShowings, so check before accessing $metainput['ATS_firstEndDate'] = ( isset( $event['CurrentShowings'] ) && ! empty( $event['CurrentShowings'] ) && isset( $event['CurrentShowings'][0]['EndDate'] ) ) ? $event['CurrentShowings'][0]['EndDate'] : ''; $metainput['ATS_HideFeatImg'] = get_option( 'agile_field_render_carousel' ) ?? 0; $metainput['ATS_MediaLib_Img_ID'] = $thumbnailID ?? ''; $metainput['OrgID'] = $event['OrgID'] ?? ''; $metainput['CorpOrgID'] = $event['CorpOrgID'] ?? ''; $metainput['ATS_Type'] = $event['Type'] ?? ''; $metainput['ATS_ShowTimes'] = $showTimes ?? array(); // Add ComingSoonDate only for coming-soon-only events (no active showings on main feed). $has_showings = isset( $event['CurrentShowings'] ) && is_array( $event['CurrentShowings'] ) && ! empty( $event['CurrentShowings'] ); if ( ! $has_showings ) { if ( isset( $event['ComingSoonDate'] ) ) { $metainput['ATS_ComingSoonDate'] = $event['ComingSoonDate']; } elseif ( $comingSoonEventData && isset( $comingSoonEventData['ComingSoonDate'] ) ) { $metainput['ATS_ComingSoonDate'] = $comingSoonEventData['ComingSoonDate']; } } } catch ( Exception $e ) { agile_tix::agile_tix_log_event( 'agile_create_universal_meta_fields Exception: ' + $e ); } return $metainput; } /** * Ingests a single show containing an array of zero or more showings * * Returns the highest LastUpdated value as a string * * This produces the value we assign to ATS_ModifiedDate so we can quickly check against the entire show to see if any show or showing has been updated */ static function agile_get_most_recent_lastUpdate( $data ) { if ( ! $data ) { agile_tix::agile_tix_log_event( 'agile_get_most_recent_lastUpdate !data' ); return null; } // $name = $data['Name'] ?? "Name missing"; // agile_tix::agile_tix_log_event("agile_get_most_recent_lastUpdate $name"); // Start with the parent show's ModifiedDate $latestDate = strtotime( $data['ModifiedDate'] ); // agile_tix::agile_tix_log_event("agile_get_most_recent_lastUpdate stored latestDate - $latestDate"); // Check each showing inside CurrentShowings if ( ! empty( $data['CurrentShowings'] ) ) { foreach ( $data['CurrentShowings'] as $showing ) { $showingDate = strtotime( $showing['ModifiedDate'] ); // agile_tix::agile_tix_log_event("agile_get_most_recent_lastUpdate showingDate - $showingDate"); if ( $showingDate > $latestDate ) { $latestDate = $showingDate; // agile_tix::agile_tix_log_event("agile_get_most_recent_lastUpdate newer date found - $latestDate"); } } } // agile_tix::agile_tix_log_event("agile_get_most_recent_lastUpdate loop done"); // Return the latest date in the original format return wp_date( 'Y-m-d\TH:i:s', $latestDate ); } static function generateMediaCarousel( $additionalMedia, $carouselId = null ) { $shouldRender = get_option( 'agile_field_render_carousel' ) ?? false; $car = ''; if ( $shouldRender && ! empty( $additionalMedia ) ) { // Generate unique ID if not provided $carouselId = $carouselId ?: 'carousel-' . uniqid(); // Get auto-scroll settings from WordPress options $autoScroll = get_option( 'agile_field_carousel_auto_scroll' ) ?? false; $autoScrollInterval = get_option( 'agile_field_carousel_auto_scroll_interval' ) ?? 5000; // Default 5 seconds? $pauseOnHover = get_option( 'agile_field_carousel_pause_on_hover' ) ?? true; $sizeScale = get_option( 'agile_field_carousel_size' ) ?? 'auto'; // consider letting these be option informed vars instead of consts $carouselHeight = 'auto'; switch ( $sizeScale ) { case 'auto': $carouselHeight = 'auto'; break; case 'xsmall': $carouselHeight = '10vh'; break; case 'small': $carouselHeight = '20vh'; break; case 'medium': $carouselHeight = '30vh'; break; case 'large': $carouselHeight = '40vh'; break; case 'xlarge': $carouselHeight = '50vh'; break; case 'xxlarge': $carouselHeight = '60vh'; break; case 'xxxlarge': $carouselHeight = '70vh'; break; } $carouselWidth = '100%'; // $carouselAspectRatio = get_option('agile_field_carousel_aspect_ratio') ?? '16:9'; // Build data attributes for JavaScript $dataAttrs = "data-carousel-id='{$carouselId}'"; if ( $autoScroll ) { $dataAttrs .= " data-auto-scroll='true'"; $dataAttrs .= " data-auto-scroll-interval='{$autoScrollInterval}'"; $dataAttrs .= " data-pause-on-hover='" . ( $pauseOnHover ? 'true' : 'false' ) . "'"; } // Create inline styles for fixed dimensions $containerStyle = "width: {$carouselWidth}; height: {$carouselHeight}; "; // aspect-ratio: {$carouselAspectRatio}; $car .= "'; // close the container } return $car; } function agile_increase_concurrent_batches( $concurrent_batches ) { $batches = get_option( 'agile_field_actSched_concurrentBatches' ) ?? 1; // agile_tix::agile_tix_log_event("agile_increase_concurrent_batches run - batches = $batches"); if ( $batches != 1 && is_int( $batches ) ) { // agile_tix::agile_tix_log_event("agile_increase_concurrent_batches concurrent batches set to: " . $batches); return $batches; } return $concurrent_batches; } function agile_increase_batch_size( $batch_size ) { $size = get_option( 'agile_field_actSched_batch_size' ) ?? 25; // agile_tix::agile_tix_log_event("agile_increase_batch_size run - size = $size"); if ( $size != 25 && is_int( $size ) ) { // agile_tix::agile_tix_log_event("agile_increase_batch_size batch size set to: " . $size); return $size; } return $batch_size; } /** * Filters events to only include those that need updates or are new * * @param array $events Array of events from the API * @param array $event_ids Mapping of event IDs to post IDs * @param bool $deepRefresh Whether this is a deep refresh (process all events) * @return array Array with 'filtered_events' and 'processed_event_ids' keys */ static function agile_filter_events_for_refresh( $events, $event_ids, $deepRefresh ) { $normalized_event_ids = array(); foreach ( $event_ids as $event_id => $post_id ) { $normalized_event_ids[ (string) $event_id ] = $post_id; } $event_ids = $normalized_event_ids; agile_tix::agile_tix_log_event( 'Filter: start' . ' | feed_events=' . count( $events ) . ' | known_wp_event_ids=' . count( $event_ids ) . ' | deepRefresh=' . ( $deepRefresh ? 'true' : 'false' ) ); if ( $deepRefresh ) { agile_tix::agile_tix_log_event( 'Filter: deepRefresh — returning ALL ' . count( $events ) . ' events' ); return array( 'filtered_events' => $events, 'processed_event_ids' => $event_ids, ); } $filtered_events = array(); $processed_event_ids = array(); // Track which events will be processed $total_events = count( $events ); $new_ids = array(); $update_ids = array(); $skip_ids = array(); // Batch fetch all post meta for existing posts $post_ids = array_values( $event_ids ); $meta_data = array(); foreach ( $post_ids as $post_id ) { $meta_data[ $post_id ] = array( 'ATS_ModifiedDate' => get_post_meta( $post_id, 'ATS_ModifiedDate', true ), 'ATS_firstEndDate' => get_post_meta( $post_id, 'ATS_firstEndDate', true ), 'ATS_showingIDs' => get_post_meta( $post_id, 'ATS_showingIDs', true ), ); } foreach ( $events as $event ) { $event_id_key = (string) ( $event['ID'] ?? '' ); $postID = $event_ids[ $event_id_key ] ?? 0; $isNewEvent = ( $postID == 0 ); // Always include new events if ( $isNewEvent ) { $filtered_events[] = $event; $new_ids[] = $event_id_key; agile_tix::agile_tix_log_event( 'Filter: NEW event id=' . $event_id_key . ' name=' . ( $event['Name'] ?? '' ) . ' — will schedule for create' ); continue; } // Check if existing event needs update $meta = $meta_data[ $postID ] ?? array(); $lastModified = $meta['ATS_ModifiedDate'] ?? false; $firstEndDate = $meta['ATS_firstEndDate'] ?? ''; $lastShowings = $meta['ATS_showingIDs'] ?? array(); // Check if this is a coming soon event (has ComingSoonDate but no CurrentShowings) $isComingSoonEvent = isset( $event['ComingSoonDate'] ) && ( ! isset( $event['CurrentShowings'] ) || empty( $event['CurrentShowings'] ) ); // For coming soon events, skip firstIsExpired check (they don't have showings/end dates) if ( $isComingSoonEvent ) { $firstIsExpired = false; // Coming soon events are never "expired" based on showings } else { $firstDate_timestamp = strtotime( $firstEndDate ) + ( 8 * 60 * 60 ); // Add 8 wiggle hours to cover timezones from LA to NY $current_timestamp = current_time( 'timestamp' ); $firstIsExpired = ( $firstDate_timestamp !== false && $firstDate_timestamp < $current_timestamp ); } $currentShowings = array(); // Coming soon events never have CurrentShowings if ( isset( $event['CurrentShowings'] ) && is_array( $event['CurrentShowings'] ) ) { foreach ( $event['CurrentShowings'] as $showing ) { $currentShowings[] = $showing['ID']; } } // Skip if no changes detected $current_most_recent = self::agile_get_most_recent_lastUpdate( $event ); $showings_match = ( $lastShowings == $currentShowings ); $dates_match = ( $lastModified != false && $lastModified == $current_most_recent ); if ( $showings_match && ! $firstIsExpired && $dates_match ) { // Event doesn't need update, but we need to track it as "processed" to avoid deletion $processed_event_ids[ $event_id_key ] = $postID; $skip_ids[] = $event_id_key; continue; } // Event needs update $filtered_events[] = $event; $update_ids[] = $event_id_key; $reasons = array(); if ( ! $showings_match ) { $reasons[] = 'showings'; } if ( $firstIsExpired ) { $reasons[] = 'expired'; } if ( ! $dates_match ) { $reasons[] = 'modifiedDate'; } agile_tix::agile_tix_log_event( 'Filter: UPDATE event id=' . $event_id_key . ' postID=' . $postID . ' reasons=' . implode( '+', $reasons ) . ' storedMod=' . ( $lastModified ? $lastModified : 'empty' ) . ' feedMod=' . $current_most_recent ); } agile_tix::agile_tix_log_event( 'Filter: summary' . ' | total=' . $total_events . ' | new=' . count( $new_ids ) . ' | update=' . count( $update_ids ) . ' | skip_unchanged=' . count( $skip_ids ) . ( empty( $new_ids ) ? '' : ' | new_ids=[' . implode( ', ', $new_ids ) . ']' ) . ( empty( $update_ids ) ? '' : ' | update_ids=[' . implode( ', ', $update_ids ) . ']' ) ); return array( 'filtered_events' => $filtered_events, 'processed_event_ids' => $processed_event_ids, ); } } // Function to allow SVG elements in post content function agile_allow_svg_in_posts( $allowed, $context ) { if ( $context === 'post' ) { $allowed['svg'] = array( 'version' => true, 'id' => true, 'xmlns' => true, 'xmlns:xlink' => true, 'width' => true, 'height' => true, 'viewBox' => true, 'enable-background' => true, 'xml:space' => true, ); $allowed['path'] = array( 'fill' => true, 'd' => true, ); $allowed['a'] = array( 'href' => true, 'class' => true, 'data-address' => true, ); } return $allowed; }