api = new AI_SEO_Article_Generator_API(); // Hook into WordPress cron add_action('ai_seo_article_generator_process_queue', array($this, 'process_queued_jobs')); // Schedule cron job if not already scheduled if (!wp_next_scheduled('ai_seo_article_generator_process_queue')) { $scheduled = wp_schedule_event(time(), 'ai_seo_article_generator_every_minute', 'ai_seo_article_generator_process_queue'); $this->debug_log('Scheduled background processing cron job: ' . ($scheduled ? 'SUCCESS' : 'FAILED')); } else { $next_run = wp_next_scheduled('ai_seo_article_generator_process_queue'); $this->debug_log('Background processing cron already scheduled. Next run: ' . gmdate('Y-m-d H:i:s', $next_run)); } // Add debug info on admin pages if (is_admin()) { add_action('admin_notices', array($this, 'cron_debug_notice')); } } /** * Show cron debug information in admin */ public function cron_debug_notice() { // Only show on AI SEO plugin pages and to administrators // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only operation if (!isset($_GET['page']) || strpos(sanitize_text_field(wp_unslash($_GET['page'])), 'ai-seo-article-generator') === false) { return; } if (!current_user_can('manage_options')) { return; } global $wpdb; $table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts'; // Check queued jobs $queued_count = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query "SELECT COUNT(*) FROM `$table_name` WHERE background_status = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared 'queued' )); // Check processing jobs $processing_count = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query "SELECT COUNT(*) FROM `$table_name` WHERE background_status = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared 'processing' )); // Check if cron is scheduled $next_run = wp_next_scheduled('ai_seo_article_generator_process_queue'); $cron_status = $next_run ? gmdate('Y-m-d H:i:s', $next_run) : 'NOT SCHEDULED'; // Check if WP Cron is disabled $wp_cron_disabled = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON; echo '
'; echo '

AI SEO Background Processing Debug:

'; echo ''; echo '
'; } /** * Process all queued article generation jobs */ public function process_queued_jobs() { global $wpdb; $table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts'; $this->debug_log('=== Background processor started ==='); $this->debug_log('Current time: ' . gmdate('Y-m-d H:i:s')); // Prevent multiple cron jobs from running simultaneously $lock_option = 'ai_seo_article_generator_processing_lock'; $lock_timeout = 900; // 15 minutes $current_time = time(); $lock_time = get_option($lock_option, 0); // Check if another process is already running if ($lock_time && ($current_time - $lock_time) < $lock_timeout) { $this->debug_log('Another background processor is already running (lock time: ' . gmdate('Y-m-d H:i:s', $lock_time) . '), skipping...'); return; } // Set lock update_option($lock_option, $current_time); try { // Get all queued jobs, but process only one at a time to prevent server overload $queued_jobs = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query "SELECT * FROM `$table_name` WHERE background_status = %s ORDER BY created_at ASC LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared 'queued' )); if (empty($queued_jobs)) { // Also check for stuck processing jobs (older than 30 minutes) $stuck_jobs = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query "SELECT * FROM `$table_name` WHERE background_status = %s AND updated_at < %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared 'processing', gmdate('Y-m-d H:i:s', strtotime('-30 minutes')) )); if (!empty($stuck_jobs)) { $this->debug_log('Found stuck processing job, resetting to queued: ' . $stuck_jobs[0]->id); $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update $table_name, array('background_status' => 'queued'), array('id' => $stuck_jobs[0]->id), array('%s'), array('%d') ); } delete_option($lock_option); return; } $this->debug_log('Found ' . count($queued_jobs) . ' queued jobs to process'); // Process only one job at a time $job = $queued_jobs[0]; $this->process_single_job($job); } catch (Exception $e) { $this->debug_log('Error in process_queued_jobs: ' . $e->getMessage()); } finally { // Always release the lock delete_option($lock_option); } } /** * Process a single article generation job */ private function process_single_job($job) { global $wpdb; $table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts'; $this->debug_log('Processing job ID: ' . $job->id . ' (Keyword: ' . $job->main_keyword . ', Target: ' . $job->target_words . ' words)'); // Increase memory limit for background processing if (function_exists('wp_raise_memory_limit')) { wp_raise_memory_limit('admin'); } // Mark job as processing with current timestamp $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update $table_name, array( 'background_status' => 'processing', 'updated_at' => current_time('mysql') ), array('id' => $job->id), array('%s', '%s'), array('%d') ); try { // Parse structure data $structure = null; if (!empty($job->structure_data)) { $structure = json_decode($job->structure_data, true); } // If no structure exists, generate it first if (!$structure) { $this->debug_log('No structure found, generating structure for job ID: ' . $job->id); $structure_result = $this->api->generate_article_structure( $job->main_keyword, $job->sub_keywords, $job->target_words ); if (!$structure_result['success']) { throw new Exception('Failed to generate structure: ' . $structure_result['message']); } $structure = $structure_result['structure']; // Save structure back to job $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update $table_name, array('structure_data' => json_encode($structure)), array('id' => $job->id), array('%s'), array('%d') ); } // Generate article content $this->debug_log('Generating content for job ID: ' . $job->id); $content_result = $this->api->generate_article_content( $structure, $job->main_keyword, $job->sub_keywords, $job->target_words ); if (!$content_result['success']) { throw new Exception('Failed to generate content: ' . $content_result['message']); } // Calculate word count $word_count = $this->calculate_word_count($content_result['content']); // Update job with completed content $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update $table_name, array( 'content' => $content_result['content'], 'word_count' => $word_count, 'background_status' => 'completed', 'status' => 'ready' ), array('id' => $job->id), array('%s', '%d', '%s', '%s'), array('%d') ); $this->debug_log('Job ID ' . $job->id . ' completed successfully'); // Send email notification $this->send_completion_email($job, true); } catch (Exception $e) { $error_message = $e->getMessage(); $this->debug_log('Job ID ' . $job->id . ' failed: ' . $error_message); // Check if this is a retry-able error $retry_count = get_option('ai_seo_retry_count_job_' . $job->id, 0); $max_retries = 3; $is_retryable_error = $this->is_retryable_error($error_message); if ($is_retryable_error && $retry_count < $max_retries) { // Increment retry count and requeue update_option('ai_seo_retry_count_job_' . $job->id, $retry_count + 1); $this->debug_log('Retrying job ID ' . $job->id . ' (attempt ' . ($retry_count + 1) . '/' . $max_retries . ')'); $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update $table_name, array( 'background_status' => 'queued', 'updated_at' => current_time('mysql') ), array('id' => $job->id), array('%s', '%s'), array('%d') ); } else { // Mark job as failed $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update $table_name, array( 'background_status' => 'failed', 'updated_at' => current_time('mysql') ), array('id' => $job->id), array('%s', '%s'), array('%d') ); // Clean up retry count delete_option('ai_seo_retry_count_job_' . $job->id); // Send failure email notification $this->send_completion_email($job, false, $error_message . ($retry_count > 0 ? ' (after ' . $retry_count . ' retries)' : '')); } } } /** * Send email notification when job is complete */ private function send_completion_email($job, $success = true, $error_message = '') { // Get user email - try to get the user who created the job $user_email = get_option('admin_email'); // Fallback to admin email // Try to get the current user's email if available $current_user = wp_get_current_user(); if ($current_user && $current_user->user_email) { $user_email = $current_user->user_email; } $site_name = get_bloginfo('name'); $site_url = home_url(); if ($success) { /* translators: %s: main keyword */ $subject = sprintf(__('Article Generation Complete - %s', 'ai-seo-article-generator'), $job->main_keyword); $message = sprintf( /* translators: %1$s: main keyword, %2$d: target words, %3$d: word count, %4$s: admin URL, %5$s: article title, %6$s: site name */ __('Your article generation has been completed successfully! Article Details: - Keyword: %1$s - Target Words: %2$d - Word Count: %3$d - Status: Ready for editing You can now edit and publish your article by clicking the link below: %4$s Article Title: %5$s Best regards, %6$s Team', 'ai-seo-article-generator'), $job->main_keyword, $job->target_words, $job->word_count, admin_url('admin.php?page=ai-seo-article-generator-new&draft_id=' . $job->id), $job->title ?: $job->main_keyword, $site_name ); } else { /* translators: %s: main keyword */ $subject = sprintf(__('Article Generation Failed - %s', 'ai-seo-article-generator'), $job->main_keyword); $message = sprintf( /* translators: %1$s: main keyword, %2$d: target words, %3$s: error message, %4$s: admin URL, %5$s: site name */ __('Unfortunately, your article generation has failed. Article Details: - Keyword: %1$s - Target Words: %2$d - Error: %3$s Please try generating the article again or contact support if the problem persists. You can retry by visiting: %4$s Best regards, %5$s Team', 'ai-seo-article-generator'), $job->main_keyword, $job->target_words, $error_message, admin_url('admin.php?page=ai-seo-article-generator-new'), $site_name ); } // Set headers for HTML email $headers = array( 'Content-Type: text/html; charset=UTF-8', 'From: ' . $site_name . ' ' ); // Convert message to HTML $html_message = ''; $html_message .= '
'; $html_message .= '

' . $subject . '

'; $html_message .= '
'; $html_message .= nl2br(esc_html($message)); $html_message .= '
'; $html_message .= '
'; // Send email $sent = wp_mail($user_email, $subject, $html_message, $headers); if ($sent) { $this->debug_log('Email notification sent to: ' . $user_email); } else { $this->debug_log('Failed to send email notification to: ' . $user_email); } } /** * Calculate word count for content */ private function calculate_word_count($content) { if (empty($content)) { return 0; } // Remove HTML tags and decode entities $text = wp_strip_all_tags($content); $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); // Remove extra whitespace and normalize $text = preg_replace('/\s+/', ' ', trim($text)); if (empty($text)) { return 0; } // For Hebrew text, count words by spaces and punctuation if (preg_match('/[\x{0590}-\x{05FF}]/u', $text)) { // Hebrew text detected - use character-based estimation $char_count = mb_strlen($text, 'UTF-8'); $estimated_words = max(1, round($char_count / 4.5)); // Also count actual spaces for mixed content $space_count = substr_count($text, ' '); // Use the higher count for better accuracy return max($estimated_words, $space_count + 1); } // For English/Latin text, use standard word counting $words = str_word_count($text, 0, 'אבגדהוזחטיכלמנסעפצקרשתךםןףץ'); return max(1, $words); } /** * Queue a new article generation job */ public static function queue_article_generation($main_keyword, $sub_keywords, $target_words, $structure_data = null, $article_language = 'auto') { global $wpdb; $table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts'; $data = array( 'title' => $main_keyword, 'main_keyword' => $main_keyword, 'sub_keywords' => $sub_keywords, 'target_words' => $target_words, 'article_language' => $article_language, 'structure_data' => $structure_data ? json_encode($structure_data) : '', 'content' => '', 'status' => 'draft', 'background_status' => 'queued', 'word_count' => 0 ); $result = $wpdb->insert($table_name, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table insert if ($result === false) { return array('success' => false, 'message' => __('Failed to queue article generation', 'ai-seo-article-generator')); } $job_id = $wpdb->insert_id; // Log the queued job if (get_option('ai_seo_article_generator_debug_logging', 0)) { $this->debug_log('Queued background job ID ' . $job_id . ' for keyword: ' . $main_keyword); } return array( 'success' => true, 'job_id' => $job_id, 'message' => __('Article generation has been queued. You will receive an email when it\'s ready.', 'ai-seo-article-generator') ); } /** * Get job status */ public static function get_job_status($job_id) { global $wpdb; $table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts'; $job = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query "SELECT id, title, main_keyword, background_status, word_count, created_at, updated_at FROM `$table_name` WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared $job_id )); if (!$job) { return array('success' => false, 'message' => __('Job not found', 'ai-seo-article-generator')); } return array( 'success' => true, 'job' => array( 'id' => $job->id, 'title' => $job->title, 'keyword' => $job->main_keyword, 'status' => $job->background_status, 'word_count' => $job->word_count, 'created_at' => $job->created_at, 'updated_at' => $job->updated_at ) ); } /** * Check if an error is retryable */ private function is_retryable_error($error_message) { $retryable_patterns = array( 'timeout', 'connection', 'network', '503', '502', '504', 'temporarily unavailable', 'rate limit', 'too many requests' ); $error_lower = strtolower($error_message); foreach ($retryable_patterns as $pattern) { if (strpos($error_lower, $pattern) !== false) { return true; } } return false; } /** * Manual trigger for testing - can be called via URL */ public static function manual_trigger() { if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $processor = new self(); $processor->debug_log('=== Manual trigger initiated ==='); $processor->process_queued_jobs(); wp_die('Manual background processing triggered. Check debug logs.'); } /** * Debug logging helper function */ private function debug_log($message, $data = null) { if (get_option('ai_seo_article_generator_debug_logging', 0)) { $log_message = 'AI SEO Article Generator Background Processor: ' . $message; if ($data !== null && is_array($data)) { $log_message .= ' - ' . wp_json_encode($data); } elseif ($data !== null) { $log_message .= ' - ' . $data; } if (function_exists('error_log')) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging error_log($log_message); } } } }