604800, // 7 days in seconds 'display' => __('Once Weekly', 'ai-blog-automator') ); } return $schedules; } /** * Schedule the cron event * * @param string|null $frequency Optional frequency override */ public static function schedule_event($frequency = null) { // Clear any existing scheduled event first self::clear_scheduled_event(); // Get frequency from settings or use override if (null === $frequency) { $frequency = Bluteem_ABA_Settings::get_setting('frequency', 'daily'); } // Validate frequency - use same validation as settings (free: daily, twicedaily, weekly) $allowed_frequencies = array('daily', 'twicedaily', 'weekly'); // Allow Pro to add custom frequencies via filter $allowed_frequencies = apply_filters('bluteem_aba_allowed_frequencies', $allowed_frequencies); // Also check if it's a valid WordPress cron schedule (for any custom schedules registered) $cron_schedules = wp_get_schedules(); if (!in_array($frequency, $allowed_frequencies, true) && !isset($cron_schedules[$frequency])) { $frequency = 'daily'; } // Schedule the event $scheduled = wp_schedule_event(time(), $frequency, 'bluteem_aba_generate_post'); if (false === $scheduled) { if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('AI Blog Automator: Failed to schedule cron event'); } } else { if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log("AI Blog Automator: Cron event scheduled with frequency: $frequency"); } } } /** * Clear scheduled cron event */ public static function clear_scheduled_event() { $timestamp = wp_next_scheduled('bluteem_aba_generate_post'); if ($timestamp) { wp_unschedule_event($timestamp, 'bluteem_aba_generate_post'); if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('AI Blog Automator: Cron event unscheduled'); } } } /** * Reschedule event with new frequency * * @param string $new_frequency New frequency */ public static function reschedule_event($new_frequency) { self::schedule_event($new_frequency); if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log("AI Blog Automator: Cron event rescheduled to: $new_frequency"); } } /** * Execute post generation (called by cron) */ public static function execute_generation() { if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('AI Blog Automator: Cron job started'); } // Generate the post $result = Bluteem_ABA_Generator::generate_post(); if ($result['success']) { if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('AI Blog Automator: Cron job completed successfully - ' . $result['message']); } } else { if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('AI Blog Automator: Cron job failed - ' . $result['message']); } } // Send admin notification email if enabled self::maybe_send_notification($result); } /** * Manual generation triggered by admin */ public static function manual_generation() { // Check permissions if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'ai-blog-automator')); } // Verify nonce - sanitize input since wp_verify_nonce is pluggable if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'bluteem_aba_generate_now')) { wp_die(esc_html__('Security check failed.', 'ai-blog-automator')); } $user_id = get_current_user_id(); $existing = wp_next_scheduled('bluteem_aba_manual_generate_post', array($user_id)); if (!self::is_loopback_healthy()) { // This site cannot reliably trigger WP-Cron (loopback blocked / disabled // cron with no system cron), so a background job would never run. Generate // synchronously instead so the button works everywhere. if ($existing) { // Clear any stale queued job so it cannot also run later as a duplicate. wp_unschedule_event($existing, 'bluteem_aba_manual_generate_post', array($user_id)); } // Fire through the hook so the Pro license guard (and any other listeners) // still apply. The execute_manual_generation() callback stores the result // notice; the license guard stores its own error notice when it blocks. do_action('bluteem_aba_manual_generate_post', $user_id); } elseif ($existing) { // A background job is already queued; nudge cron in case it is waiting. self::spawn_cron(); Bluteem_ABA_Settings::set_admin_notice_for_user( $user_id, Bluteem_ABA_Settings::NOTICE_GENERATION, 'info', __('Post generation is already running in the background. Refresh this page shortly for the result.', 'ai-blog-automator') ); } else { wp_schedule_single_event(time(), 'bluteem_aba_manual_generate_post', array($user_id)); self::spawn_cron(); Bluteem_ABA_Settings::set_admin_notice_for_user( $user_id, Bluteem_ABA_Settings::NOTICE_GENERATION, 'info', __('Post generation started in the background. Refresh this page in a minute for the result, or check Posts and Logs.', 'ai-blog-automator') ); } wp_safe_redirect( add_query_arg( array( 'page' => 'ai-blog-automator', ), admin_url('admin.php') ) ); exit; } /** * Run manual generation in the background (WP-Cron). * * @param int $user_id User who triggered generation. * @return void */ public static function execute_manual_generation($user_id) { $user_id = (int) $user_id; $result = Bluteem_ABA_Generator::generate_post(); $extra = array(); if (!empty($result['post_id'])) { $extra['post_id'] = (int) $result['post_id']; } Bluteem_ABA_Settings::set_admin_notice_for_user( $user_id, Bluteem_ABA_Settings::NOTICE_GENERATION, !empty($result['success']) ? 'success' : 'error', isset($result['message']) ? $result['message'] : __('Generation finished.', 'ai-blog-automator'), $extra ); self::maybe_send_notification($result); } /** * Trigger WP-Cron without waiting for the next page load. * * @return void */ private static function spawn_cron() { if (function_exists('spawn_cron')) { spawn_cron(); return; } $cron_url = add_query_arg( 'doing_wp_cron', sprintf('%.22F', microtime(true)), site_url('wp-cron.php') ); wp_remote_post( $cron_url, array( 'timeout' => 0.01, 'blocking' => false, // Loopback request to our own site; skip SSL verification. 'sslverify' => false, ) ); } /** * Probe whether the site can reach itself (loopback). * * Background generation depends on WP-Cron, which is triggered via a * loopback request to wp-cron.php. If loopback is blocked (common on hosts * that resolve the site to a CDN/proxy or firewall outbound self-requests), * scheduled jobs will not run promptly. Result is cached for an hour. * * @return bool True if loopback appears reachable. */ public static function is_loopback_healthy() { $cached = get_transient('bluteem_aba_loopback_health'); if ('ok' === $cached) { return true; } if ('fail' === $cached) { return false; } $response = wp_remote_head( home_url('/'), array( 'timeout' => 5, 'blocking' => true, // Loopback request to our own site; skip SSL verification. 'sslverify' => false, ) ); $healthy = !is_wp_error($response); if ($healthy) { $code = (int) wp_remote_retrieve_response_code($response); // Gateway/origin timeouts mean a loopback can't complete in time. if (in_array($code, array(502, 503, 504, 521, 522, 523, 524), true)) { $healthy = false; } } set_transient('bluteem_aba_loopback_health', $healthy ? 'ok' : 'fail', HOUR_IN_SECONDS); return $healthy; } /** * Whether WP-Cron is expected to fire automatically on this site. * * @return bool */ public static function is_wp_cron_disabled() { return defined('DISABLE_WP_CRON') && DISABLE_WP_CRON; } /** * Detect a manual generation job that was queued but never ran. * * This is an evidence-based check (the event is still scheduled and overdue), * so it does not produce false alarms on healthy sites. * * @param int $user_id User to check (defaults to current user). * @return string Warning message, or empty string when nothing is stuck. */ public static function get_stuck_job_warning($user_id = 0) { $user_id = $user_id ? (int) $user_id : get_current_user_id(); if ($user_id <= 0) { return ''; } $next = wp_next_scheduled('bluteem_aba_manual_generate_post', array($user_id)); if ($next && $next < (time() - 120)) { return __('A post generation was queued more than 2 minutes ago but has not run yet. This usually means WP-Cron is not firing on your site. Ask your host to confirm a system cron is hitting wp-cron.php (recommended), or that loopback requests are not blocked.', 'ai-blog-automator'); } return ''; } /** * Send email notification to admin * * @param array $result Generation result */ private static function maybe_send_notification($result) { // Check if notifications are enabled (could be a future setting) $send_notifications = apply_filters('bluteem_aba_send_notifications', false); if (!$send_notifications) { return; } $admin_email = get_option('admin_email'); $site_name = get_bloginfo('name'); if ($result['success']) { $subject = sprintf( /* translators: %s: Site name */ __('[%s] New AI-Generated Post Published', 'ai-blog-automator'), $site_name ); $message = sprintf( /* translators: 1: Result message, 2: Admin posts URL */ __('A new blog post has been automatically generated and published on your website. %1$s View all posts: %2$s', 'ai-blog-automator'), $result['message'], admin_url('edit.php') ); } else { $subject = sprintf( /* translators: %s: Site name */ __('[%s] AI Blog Automator Error', 'ai-blog-automator'), $site_name ); $message = sprintf( /* translators: 1: Error message, 2: Settings page URL */ __('An error occurred while trying to generate a blog post: %1$s Please check your settings: %2$s', 'ai-blog-automator'), $result['message'], admin_url('admin.php?page=ai-blog-automator') ); } wp_mail($admin_email, $subject, $message); } /** * Get next scheduled run time * * @return string|null Formatted date string or null if not scheduled */ public static function get_next_run_time() { $timestamp = wp_next_scheduled('bluteem_aba_generate_post'); if (!$timestamp) { return null; } return get_date_from_gmt(gmdate('Y-m-d H:i:s', $timestamp), 'F j, Y g:i a'); } /** * Check if cron is scheduled * * @return bool True if scheduled */ public static function is_scheduled() { return (bool) wp_next_scheduled('bluteem_aba_generate_post'); } /** * Get cron schedule interval in human-readable format * * @return string Schedule description */ public static function get_schedule_description() { $frequency = Bluteem_ABA_Settings::get_setting('frequency', 'daily'); $descriptions = array( 'hourly' => __('Every hour', 'ai-blog-automator'), 'twicedaily' => __('Twice daily', 'ai-blog-automator'), 'daily' => __('Once daily', 'ai-blog-automator'), 'weekly' => __('Once weekly', 'ai-blog-automator') ); return isset($descriptions[$frequency]) ? $descriptions[$frequency] : $frequency; } /** * Get all scheduled AI Blog Automator cron events * * @return array Cron event details */ public static function get_cron_info() { $timestamp = wp_next_scheduled('bluteem_aba_generate_post'); if (!$timestamp) { return array( 'scheduled' => false, 'next_run' => null, 'frequency' => null ); } $crons = _get_cron_array(); $schedule = null; if (isset($crons[$timestamp]['bluteem_aba_generate_post'])) { $event = reset($crons[$timestamp]['bluteem_aba_generate_post']); $schedule = isset($event['schedule']) ? $event['schedule'] : null; } return array( 'scheduled' => true, 'next_run' => $timestamp, 'next_run_formatted' => self::get_next_run_time(), 'frequency' => $schedule, 'frequency_description' => self::get_schedule_description() ); } }