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 '- Queued Jobs:
' . esc_html($queued_count) . ' ';
echo '- Processing Jobs:
' . esc_html($processing_count) . ' ';
echo '- Next Cron Run:
' . esc_html($cron_status) . ' ';
echo '- WP Cron Status:
' . ($wp_cron_disabled ? 'DISABLED' : 'ENABLED') . ' ';
if ($wp_cron_disabled) {
echo '- WARNING: WP Cron is disabled. Background processing will not work!
';
}
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);
}
}
}
}