options = $options; $this->logger = $logger; $this->processor = $processor; } /** * Updates the internal options array. * * @param array $options New plugin options. */ public function set_options( $options ) { $this->options = $options; } /** * Checks for an overdue cron job on admin page load and triggers a fallback execution. * Helps ensure cron runs on low-traffic sites. */ public function check_and_trigger_overdue_cron_on_admin_load() { // Only run for admins, not during AJAX or actual cron runs. if ( ! current_user_can( 'manage_options' ) || wp_doing_ajax() || wp_doing_cron() ) { return; } // Skip if auto-search is disabled. if ( empty( $this->options['enable_auto_search'] ) || 'disabled' === ( $this->options['auto_search_interval'] ?? 'disabled' ) ) { return; } $timestamp = wp_next_scheduled( AINP_AUTO_FETCH_CRON_HOOK ); // Check if overdue by more than 10 minutes (600 seconds) and not checked recently. if ( $timestamp && $timestamp < ( time() - 600 ) && false === get_transient( 'ainp_fallback_cron_check' ) ) { wp_spawn_cron(); // Trigger the cron spawning mechanism. $this->logger->add_log_entry( 'info', __( 'Overdue auto-search cron detected. Triggering fallback execution.', 'ainp-ai-native-publisher' ), __( 'This typically happens when the WordPress scheduler does not run on time due to lack of site visits.', 'ainp-ai-native-publisher' ) ); // Prevent checking too frequently (once per hour). set_transient( 'ainp_fallback_cron_check', time(), HOUR_IN_SECONDS ); } } /** * Schedules or clears the main auto-search cron job based on plugin settings. */ public function schedule_or_clear_auto_search() { $timestamp = wp_next_scheduled( AINP_AUTO_FETCH_CRON_HOOK ); if ( $timestamp ) { wp_clear_scheduled_hook( AINP_AUTO_FETCH_CRON_HOOK ); } $is_enabled = ! empty( $this->options['enable_auto_search'] ); $interval_key = $this->options['auto_search_interval'] ?? 'disabled'; if ( $is_enabled && 'disabled' !== $interval_key ) { $schedules = wp_get_schedules(); $actual_interval = isset( $schedules[ $interval_key ] ) ? $interval_key : 'hourly'; // Fallback wp_schedule_event( time(), $actual_interval, AINP_AUTO_FETCH_CRON_HOOK ); /* translators: %s: Human-readable cron interval (e.g., "Hourly") */ $this->logger->add_log_entry( 'info', sprintf( __( 'Auto-search scheduled to run %s.', 'ainp-ai-native-publisher' ), $this->get_cron_interval_label( $actual_interval ) ), __( 'The next execution will occur at the time scheduled by WP-Cron.', 'ainp-ai-native-publisher' ) ); } elseif ( $timestamp ) { $this->logger->add_log_entry( 'info', __( 'Auto-search has been disabled. The scheduled event was removed.', 'ainp-ai-native-publisher' ) ); } } /** * Adds custom cron recurrence schedules to WordPress. * Ensures display names are translatable. * * @param array $schedules The existing array of cron schedules. * @return array The modified array of cron schedules. */ public function add_custom_cron_schedules( $schedules ) { $custom_schedules = array( 'every_5_minutes' => array( 'interval' => 5 * MINUTE_IN_SECONDS, 'display' => __( 'Every 5 Minutes', 'ainp-ai-native-publisher' ), // i18n ), 'every_15_minutes' => array( 'interval' => 15 * MINUTE_IN_SECONDS, 'display' => __( 'Every 15 Minutes', 'ainp-ai-native-publisher' ), // i18n ), 'every_30_minutes' => array( 'interval' => 30 * MINUTE_IN_SECONDS, 'display' => __( 'Every 30 Minutes', 'ainp-ai-native-publisher' ), // i18n ), 'every_2_hours' => array( 'interval' => 2 * HOUR_IN_SECONDS, 'display' => __( 'Every 2 Hours', 'ainp-ai-native-publisher' ), // i18n ), 'every_6_hours' => array( 'interval' => 6 * HOUR_IN_SECONDS, 'display' => __( 'Every 6 Hours', 'ainp-ai-native-publisher' ), // i18n ), 'every_12_hours' => array( 'interval' => 12 * HOUR_IN_SECONDS, 'display' => __( 'Every 12 Hours', 'ainp-ai-native-publisher' ), // i18n ), ); // Merge custom schedules, ensuring they don't overwrite existing WP schedules with the same key. return array_merge( $custom_schedules, $schedules ); } /** * Gets the human-readable display label for a given cron interval key. * * @param string $interval_key The key of the interval (e.g., 'hourly'). * @return string The display label (e.g., 'Hourly'). */ public function get_cron_interval_label( $interval_key ) { $schedules = wp_get_schedules(); // Ensure we return the display string if it exists. // NOTE: The 'display' value is usually already translated by the defining plugin via __(). return isset( $schedules[ $interval_key ]['display'] ) ? $schedules[ $interval_key ]['display'] : $interval_key; } /** * The main callback function for the automatic search cron job. * Initializes a batch processing queue. */ public function execute_automatic_search_callback() { $this->logger->add_log_entry( 'info', __( 'CRON: Starting automatic batch search.', 'ainp-ai-native-publisher' ) ); if ( empty( $this->options['enable_auto_search'] ) || 'disabled' === ( $this->options['auto_search_interval'] ?? 'disabled' ) ) { $this->logger->add_log_entry( 'warning', __( 'CRON: Auto-search skipped because it is disabled in settings.', 'ainp-ai-native-publisher' ) ); return; } if ( false !== get_transient( AINP_FEED_QUEUE_TRANSIENT ) ) { $this->logger->add_log_entry( 'warning', __( 'CRON: Auto-search skipped because a search is already in progress.', 'ainp-ai-native-publisher' ) ); return; } $sources = $this->options['sources_config'] ?? array(); if ( empty( $sources ) ) { $this->logger->add_log_entry( 'warning', __( 'CRON: Auto-search skipped because no sources are configured.', 'ainp-ai-native-publisher' ) ); return; } // Initialize Batch Process delete_option( AINP_Status_Logger::CANCEL_FLAG_OPTION ); update_option( AINP_EXECUTION_START_TIME_OPTION, time() ); update_option( AINP_PENDING_IMAGE_TASKS_OPTION, 0 ); $this->processor->set_posts_awaiting_image_current_run( array() ); $search_depth_for_cron = $this->options['auto_search_depth'] ?? '1'; $queue_data = array( 'sources' => $sources, 'search_depth' => $search_depth_for_cron, ); set_transient( AINP_FEED_QUEUE_TRANSIENT, $queue_data, HOUR_IN_SECONDS * 2 ); $total_sources = count( $sources ); $this->logger->update_processing_status( 'processing', 0, $total_sources, __( 'Starting automatic batch search...', 'ainp-ai-native-publisher' ) ); /* translators: %d: Number of sources */ $this->logger->add_log_entry( 'info', sprintf( __( 'CRON: Search queue prepared for %d sources.', 'ainp-ai-native-publisher' ), $total_sources ) ); // Schedule the first batch item immediately. wp_schedule_single_event( time(), AINP_PROCESS_BATCH_HOOK ); } /** * Executes the processing of a single item from the batch queue. * Runs in short bursts, processing one source and rescheduling itself. */ public function execute_batch_item() { $this->logger->add_log_entry( 'info', __( 'Executing item from the batch processing queue.', 'ainp-ai-native-publisher' ) ); // Timeout Guard if ( ! empty( $this->options['enable_execution_timeout_guard'] ) ) { $execution_start_time = (int) get_option( AINP_EXECUTION_START_TIME_OPTION, time() ); $max_execution_time = (int) ini_get( 'max_execution_time' ); // Apenas aplique um limite se max_execution_time for definido (maior que 0). // Um valor 0 significa 'ilimitado', então o "guard" deve ser ignorado. if ( $max_execution_time > 0 ) { $time_limit = ( $max_execution_time > 10 ) ? $max_execution_time - 10 : 50; // 10s safety margin or 50s limit if ( ( time() - $execution_start_time ) >= $time_limit ) { $this->logger->add_log_entry( 'warning', __( 'Server execution time limit is approaching. Pausing and rescheduling continuation.', 'ainp-ai-native-publisher' ), 'Limit: ' . $max_execution_time . 's' ); // Reschedule using constant delay. wp_schedule_single_event( time() + self::BATCH_RESCHEDULE_ON_TIMEOUT_SECONDS, AINP_PROCESS_BATCH_HOOK ); return; } } } $queue_data = get_transient( AINP_FEED_QUEUE_TRANSIENT ); // Queue Validation if ( false === $queue_data || ! is_array( $queue_data ) || ! isset( $queue_data['sources'] ) ) { $this->logger->add_log_entry( 'info', __( 'Processing queue is empty or invalid. Batch processing stopped.', 'ainp-ai-native-publisher' ) ); if ( false !== $queue_data ) { delete_transient( AINP_FEED_QUEUE_TRANSIENT ); } return; } $sources_queue = $queue_data['sources']; $search_depth = $queue_data['search_depth'] ?? 'all'; $status = $this->logger->get_processing_status(); $total_sources = $status['total']; $processed_count = $total_sources - count( $sources_queue ); // End of Queue if ( empty( $sources_queue ) ) { delete_transient( AINP_FEED_QUEUE_TRANSIENT ); if ( ! empty( $this->options['delete_awaiting_image_on_completion'] ) ) { $deleted_count = $this->processor->delete_awaiting_image_posts_from_current_run(); if ( $deleted_count > 0 ) { $this->logger->add_log_entry( 'success', sprintf( /* translators: %d: Number of deleted posts */ _n( '%d "Awaiting Image" post was deleted at the end of the fetch.', '%d "Awaiting Image" posts were deleted at the end of the fetch.', $deleted_count, 'ainp-ai-native-publisher' ), $deleted_count ) ); } } $final_message = __( 'Batch search of sources completed. Waiting for image generation queue if any.', 'ainp-ai-native-publisher' ); $this->logger->update_processing_status( 'completed', $total_sources, $total_sources, $final_message ); $this->logger->add_log_entry( 'success', $final_message ); return; } // Process Next Item $source_to_process = array_shift( $sources_queue ); $queue_data['sources'] = $sources_queue; set_transient( AINP_FEED_QUEUE_TRANSIENT, $queue_data, HOUR_IN_SECONDS * 2 ); $this->logger->update_processing_status( 'processing', $processed_count, $total_sources, /* translators: 1: Current item number, 2: Total items, 3: Source URL */ sprintf( __( 'Processing source %1$d of %2$d: %3$s', 'ainp-ai-native-publisher' ), $processed_count + 1, $total_sources, esc_html( $source_to_process['url'] ) ) ); $this->processor->process_single_source( $source_to_process, $search_depth, false ); // is_manual_url is false // Reschedule if ( ! empty( $sources_queue ) ) { $this->logger->add_log_entry( 'info', __( 'Scheduling next item in the queue.', 'ainp-ai-native-publisher' ) ); // Reschedule using constant pause duration. wp_schedule_single_event( time() + self::BATCH_ITEM_PAUSE_SECONDS, AINP_PROCESS_BATCH_HOOK ); } else { $this->logger->add_log_entry( 'info', __( 'End of the source queue reached. Scheduling final check.', 'ainp-ai-native-publisher' ) ); // Schedule final check slightly later. wp_schedule_single_event( time() + ( self::BATCH_ITEM_PAUSE_SECONDS * 2 ), AINP_PROCESS_BATCH_HOOK ); } } } // End Class AINP_Cron_Handler