plugin = ai_seo_article_generator();
add_action('admin_menu', array($this, 'add_admin_menu'));
// Add debug admin notice for language detection
add_action('admin_notices', array($this, 'debug_language_notice'));
add_action('wp_ajax_ai_seo_article_generator_test_connection', array($this, 'ajax_test_connection'));
add_action('wp_ajax_ai_seo_article_generator_generate_structure', array($this, 'ajax_generate_structure'));
add_action('wp_ajax_ai_seo_article_generator_generate_content', array($this, 'ajax_generate_content'));
add_action('wp_ajax_ai_seo_article_generator_queue_background_generation', array($this, 'ajax_queue_background_generation'));
add_action('wp_ajax_ai_seo_article_generator_check_job_status', array($this, 'ajax_check_job_status'));
add_action('wp_ajax_ai_seo_article_generator_retry_failed_job', array($this, 'ajax_retry_failed_job'));
add_action('wp_ajax_ai_seo_article_generator_enhance_section', array($this, 'ajax_enhance_section'));
add_action('wp_ajax_ai_seo_article_generator_save_draft', array($this, 'ajax_save_draft'));
add_action('wp_ajax_ai_seo_article_generator_load_draft', array($this, 'ajax_load_draft'));
add_action('wp_ajax_ai_seo_article_generator_delete_draft', array($this, 'ajax_delete_draft'));
add_action('wp_ajax_ai_seo_article_generator_create_tables', array($this, 'ajax_create_tables'));
add_action('wp_ajax_ai_seo_article_generator_save_structure', array($this, 'ajax_save_structure'));
add_action('wp_ajax_ai_seo_article_generator_load_structure', array($this, 'ajax_load_structure'));
add_action('wp_ajax_ai_seo_article_generator_delete_structure', array($this, 'ajax_delete_structure'));
add_action('wp_ajax_ai_seo_article_generator_debug_structures', array($this, 'ajax_debug_structures'));
add_action('wp_ajax_ai_seo_article_generator_publish_article', array($this, 'ajax_publish_article'));
add_action('wp_ajax_ai_seo_article_generator_manual_trigger_background', array($this, 'ajax_manual_trigger_background'));
add_action('admin_init', array($this, 'handle_settings'));
}
/**
* Get language direction helper
*/
private function get_text_direction() {
return $this->plugin->get_text_direction();
}
/**
* Get text alignment helper
*/
private function get_text_alignment() {
return $this->plugin->get_text_alignment();
}
/**
* Check if Hebrew locale
*/
private function is_hebrew_locale() {
return $this->plugin->is_hebrew_locale();
}
/**
* Debug admin notice for language detection
*/
public function debug_language_notice() {
// Only show on AI SEO plugin pages
// 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;
}
$locale = get_locale();
$is_hebrew = $this->is_hebrew_locale();
$direction = $this->get_text_direction();
$alignment = $this->get_text_alignment();
echo '
';
echo '
AI SEO Article Generator - Language Debug:
';
echo '
WordPress Locale: ' . esc_html($locale) . '
';
echo '
Hebrew Detected: ' . ($is_hebrew ? 'YES' : 'NO') . '
';
echo '
Text Direction: ' . esc_html($direction) . '
';
echo '
Text Alignment: ' . esc_html($alignment) . '
';
echo '
Translation Test: ' . esc_html(__('AI SEO Article Generator', 'ai-seo-article-generator')) . '
';
echo '
';
}
/**
* 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 Admin: ' . $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);
}
}
}
public function add_admin_menu() {
add_menu_page(
__('AI SEO Article Generator', 'ai-seo-article-generator'),
__('AI SEO Generator', 'ai-seo-article-generator'),
'manage_options',
'ai-seo-article-generator',
array($this, 'admin_page'),
'dashicons-edit-large',
30
);
add_submenu_page(
'ai-seo-article-generator',
__('AI SEO Generator Settings', 'ai-seo-article-generator'),
__('Settings', 'ai-seo-article-generator'),
'manage_options',
'ai-seo-article-generator-settings',
array($this, 'settings_page')
);
add_submenu_page(
'ai-seo-article-generator',
__('Create New Article', 'ai-seo-article-generator'),
__('New Article', 'ai-seo-article-generator'),
'manage_options',
'ai-seo-article-generator-new',
array($this, 'new_article_page')
);
add_submenu_page(
'ai-seo-article-generator',
__('Article Drafts', 'ai-seo-article-generator'),
__('Drafts', 'ai-seo-article-generator'),
'manage_options',
'ai-seo-article-generator-drafts',
array($this, 'drafts_page')
);
add_submenu_page(
'ai-seo-article-generator',
__('Structure Library', 'ai-seo-article-generator'),
__('Saved Structures', 'ai-seo-article-generator'),
'manage_options',
'ai-seo-article-generator-structures',
array($this, 'structures_page')
);
add_submenu_page(
'ai-seo-article-generator',
__('Background Jobs', 'ai-seo-article-generator'),
__('Background Jobs', 'ai-seo-article-generator'),
'manage_options',
'ai-seo-article-generator-jobs',
array($this, 'background_jobs_page')
);
}
public function admin_page() {
$api_connected = get_option('ai_seo_article_generator_api_connected', false);
?>
🤖
⚙️
prefix . 'ai_seo_article_generator_drafts';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query for status display
$queued_count = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM `$table_name` WHERE background_status = %s", 'queued'));
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query for status display
$processing_count = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM `$table_name` WHERE background_status = %s", 'processing'));
?>
save_settings();
}
$api_key = get_option('ai_seo_article_generator_api_key', '');
$api_connected = get_option('ai_seo_article_generator_api_connected', false);
?>
prefix . 'ai_seo_article_generator_drafts';
// Get all background jobs (not 'none' status)
$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 DESC LIMIT 50", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared
'none'
));
?>
⚙️
|
|
|
|
|
|
|
|
|
| id); ?> |
main_keyword); ?> |
target_words); ?> |
get_background_status_label($job->background_status)); ?>
|
word_count > 0 ? esc_html(number_format($job->word_count)) : '-'; ?> |
created_at))); ?> |
updated_at))); ?> |
background_status === 'completed' && !empty($job->content)): ?>
background_status === 'processing'): ?>
🔄
background_status === 'queued'): ?>
⏳
background_status === 'failed'): ?>
|
__('Queued', 'ai-seo-article-generator'),
'processing' => __('Processing', 'ai-seo-article-generator'),
'completed' => __('Completed', 'ai-seo-article-generator'),
'failed' => __('Failed', 'ai-seo-article-generator'),
'none' => __('None', 'ai-seo-article-generator')
);
return isset($labels[$status]) ? $labels[$status] : $status;
}
public function drafts_page() {
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts';
$drafts = $wpdb->get_results("SELECT * FROM `{$wpdb->prefix}ai_seo_article_generator_drafts` ORDER BY updated_at DESC"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query
?>
📝
|
|
|
|
|
|
|
| title ?: __('No title', 'ai-seo-article-generator')); ?> |
main_keyword); ?> |
word_count) ? intval($draft->word_count) : 0;
if ($word_count == 0 && !empty($draft->content)) {
// Calculate word count for existing drafts without it
$word_count = $this->calculate_word_count($draft->content);
}
echo esc_html(number_format($word_count) . ' ' . __('words', 'ai-seo-article-generator'));
?> |
get_status_label($draft->status)); ?> |
updated_at))); ?> |
|
' . esc_html(__('Settings saved successfully!', 'ai-seo-article-generator')) . '
';
}
public function ajax_test_connection() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$api = new AI_SEO_Article_Generator_API();
$result = $api->test_connection();
wp_send_json($result);
}
public function ajax_generate_structure() {
$this->debug_log('🚀 AI SEO Article Generator: Starting structure generation AJAX handler');
try {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
$this->debug_log('✅ AI SEO Article Generator: Nonce verification passed');
if (!current_user_can('manage_options')) {
$this->debug_log('❌ AI SEO Article Generator: Insufficient permissions');
wp_send_json(array('success' => false, 'message' => __('Insufficient permissions', 'ai-seo-article-generator')));
return;
}
$main_keyword = isset($_POST['main_keyword']) ? sanitize_text_field(wp_unslash($_POST['main_keyword'])) : '';
$sub_keywords = isset($_POST['sub_keywords']) ? sanitize_textarea_field(wp_unslash($_POST['sub_keywords'])) : '';
$target_words = isset($_POST['target_words']) ? intval($_POST['target_words']) : 0;
$article_language = isset($_POST['article_language']) ? sanitize_text_field(wp_unslash($_POST['article_language'])) : 'auto';
$this->debug_log("📝 AI SEO Article Generator: Structure generation params - Keyword: $main_keyword, Sub-keywords: $sub_keywords, Target words: $target_words");
if (empty($main_keyword)) {
$this->debug_log('❌ AI SEO Article Generator: Main keyword is empty');
wp_send_json(array('success' => false, 'message' => __('Main keyword is required', 'ai-seo-article-generator')));
return;
}
$api = new AI_SEO_Article_Generator_API();
$this->debug_log('🔧 AI SEO Article Generator: API instance created, calling generate_article_structure');
$result = $api->generate_article_structure($main_keyword, $sub_keywords, $target_words, $article_language);
$this->debug_log('📊 AI SEO Article Generator: API result', $result);
wp_send_json($result);
} catch (Exception $e) {
$this->debug_log('💥 AI SEO Article Generator: Exception in ajax_generate_structure: ' . $e->getMessage());
wp_send_json(array('success' => false, 'message' => __('Internal error:', 'ai-seo-article-generator') . ' ' . $e->getMessage()));
}
}
public function ajax_generate_content() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
$structure = isset($_POST['structure']) ? json_decode(sanitize_textarea_field(wp_unslash($_POST['structure'])), true) : array();
$main_keyword = isset($_POST['main_keyword']) ? sanitize_text_field(wp_unslash($_POST['main_keyword'])) : '';
$sub_keywords = isset($_POST['sub_keywords']) ? sanitize_textarea_field(wp_unslash($_POST['sub_keywords'])) : '';
$target_words = isset($_POST['target_words']) ? intval($_POST['target_words']) : 0;
$article_language = isset($_POST['article_language']) ? sanitize_text_field(wp_unslash($_POST['article_language'])) : 'auto';
$api = new AI_SEO_Article_Generator_API();
$result = $api->generate_article_content($structure, $main_keyword, $sub_keywords, $target_words, $article_language);
wp_send_json($result);
}
public function ajax_enhance_section() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_seo_content()
$section_content = isset($_POST['section_content']) ? $this->sanitize_seo_content(wp_unslash($_POST['section_content'])) : '';
$main_keyword = isset($_POST['main_keyword']) ? sanitize_text_field(wp_unslash($_POST['main_keyword'])) : '';
$enhancement_type = isset($_POST['enhancement_type']) ? sanitize_text_field(wp_unslash($_POST['enhancement_type'])) : '';
$api = new AI_SEO_Article_Generator_API();
$result = $api->enhance_section($section_content, $main_keyword, $enhancement_type);
wp_send_json($result);
}
public function ajax_save_draft() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts';
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_seo_content()
$content = isset($_POST['content']) ? $this->sanitize_seo_content(wp_unslash($_POST['content'])) : '';
$data = array(
'title' => isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '',
'main_keyword' => isset($_POST['main_keyword']) ? sanitize_text_field(wp_unslash($_POST['main_keyword'])) : '',
'sub_keywords' => isset($_POST['sub_keywords']) ? sanitize_textarea_field(wp_unslash($_POST['sub_keywords'])) : '',
'target_words' => isset($_POST['target_words']) ? intval($_POST['target_words']) : 0,
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_seo_content()
'structure_data' => isset($_POST['structure_data']) ? $this->sanitize_seo_content(wp_unslash($_POST['structure_data'])) : '',
'content' => $content,
'status' => isset($_POST['status']) ? sanitize_text_field(wp_unslash($_POST['status'])) : 'draft',
'word_count' => $this->calculate_word_count($content),
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_seo_content()
'outbound_links' => isset($_POST['outbound_links']) ? $this->sanitize_seo_content(wp_unslash($_POST['outbound_links'])) : ''
);
if (isset($_POST['draft_id']) && !empty($_POST['draft_id'])) {
$wpdb->update($table_name, $data, array('id' => intval($_POST['draft_id']))); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update
$draft_id = isset($_POST['draft_id']) ? intval($_POST['draft_id']) : 0;
} else {
$wpdb->insert($table_name, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table insert
$draft_id = $wpdb->insert_id;
}
wp_send_json(array('success' => true, 'draft_id' => $draft_id));
}
private function get_status_label($status) {
$labels = array(
'draft' => __('Draft', 'ai-seo-article-generator'),
'structure' => __('Structure', 'ai-seo-article-generator'),
'content' => __('Content', 'ai-seo-article-generator'),
'ready' => __('Ready to Publish', 'ai-seo-article-generator')
);
return isset($labels[$status]) ? $labels[$status] : $status;
}
public function ajax_load_draft() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$draft_id = isset($_POST['draft_id']) ? intval($_POST['draft_id']) : 0;
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query
$draft = $wpdb->get_row($wpdb->prepare("SELECT * FROM `$table_name` WHERE id = %d", $draft_id));
if (!$draft) {
wp_send_json(array('success' => false, 'message' => __('Draft not found', 'ai-seo-article-generator')));
return;
}
// Parse structure data if it exists
$structure_data = null;
if (!empty($draft->structure_data)) {
$structure_data = json_decode($draft->structure_data, true);
}
// Parse outbound links data if it exists
$outbound_links = array();
if (!empty($draft->outbound_links)) {
$parsed_links = json_decode($draft->outbound_links, true);
if ($parsed_links) {
$outbound_links = $parsed_links;
}
}
wp_send_json(array(
'success' => true,
'draft' => array(
'id' => $draft->id,
'title' => $draft->title,
'main_keyword' => $draft->main_keyword,
'sub_keywords' => $draft->sub_keywords,
'target_words' => $draft->target_words,
'structure_data' => $structure_data,
'content' => $draft->content,
'status' => $draft->status,
'outbound_links' => $outbound_links,
'created_at' => $draft->created_at,
'updated_at' => $draft->updated_at
)
));
}
public function ajax_delete_draft() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$draft_id = isset($_POST['draft_id']) ? intval($_POST['draft_id']) : 0;
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts';
$result = $wpdb->delete($table_name, array('id' => $draft_id), array('%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table delete
if ($result === false) {
wp_send_json(array('success' => false, 'message' => __('Error deleting draft', 'ai-seo-article-generator')));
return;
}
wp_send_json(array('success' => true, 'message' => __('Draft deleted successfully', 'ai-seo-article-generator')));
}
public function ajax_create_tables() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
// Call the main plugin's create_tables method
$plugin = ai_seo_article_generator();
$plugin->create_tables();
// Check if tables were created successfully
global $wpdb;
$article_drafts_table = $wpdb->prefix . 'ai_seo_article_generator_drafts';
$article_structures_table = $wpdb->prefix . 'ai_seo_article_generator_structures';
$saved_structures_table = $wpdb->prefix . 'ai_seo_article_generator_saved_structures';
$drafts_exist = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $article_drafts_table)) == $article_drafts_table; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table existence check
$structures_exist = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $article_structures_table)) == $article_structures_table; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table existence check
$saved_structures_exist = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $saved_structures_table)) == $saved_structures_table; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table existence check
if ($drafts_exist && $structures_exist && $saved_structures_exist) {
update_option('ai_seo_article_generator_tables_created', true);
wp_send_json(array('success' => true, 'message' => __('Tables created successfully!', 'ai-seo-article-generator')));
} else {
$missing_tables = array();
if (!$drafts_exist) $missing_tables[] = 'drafts';
if (!$structures_exist) $missing_tables[] = 'structures';
if (!$saved_structures_exist) $missing_tables[] = 'saved_structures';
$error_message = __('Error creating tables:', 'ai-seo-article-generator') . ' ' . implode(', ', $missing_tables);
wp_send_json(array('success' => false, 'message' => $error_message));
}
}
public function handle_settings() {
if (isset($_GET['page']) && sanitize_text_field(wp_unslash($_GET['page'])) === 'ai-seo-article-generator-settings' && isset($_POST['submit']) && isset($_POST['ai_seo_article_generator_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ai_seo_article_generator_nonce'])), 'ai_seo_article_generator_settings')) {
$this->save_settings();
}
}
public function structures_page() {
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_saved_structures';
$structures = $wpdb->get_results("SELECT * FROM `{$wpdb->prefix}ai_seo_article_generator_saved_structures` ORDER BY created_at DESC"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query
?>
📚
|
|
|
|
|
|
|
name); ?>
description): ?>
description); ?>
|
main_keyword); ?> |
target_words . ' ' . __('words', 'ai-seo-article-generator')); ?> |
usage_count); ?> |
created_at))); ?> |
|
debug_log('🔍 AI SEO Article Generator JSON Validation: Starting validation for: ' . substr($raw_json, 0, 100) . '...');
$this->debug_log('🔍 AI SEO Article Generator JSON Validation: Input encoding: ' . mb_detect_encoding($raw_json));
$this->debug_log('🔍 AI SEO Article Generator JSON Validation: Input length: ' . strlen($raw_json));
// Check if input is empty
if (empty($raw_json)) {
$this->debug_log('❌ AI SEO Article Generator JSON Validation: Empty JSON input');
return false;
}
// Clean and prepare JSON for decoding
$cleaned_json = trim($raw_json);
// Check for double-encoding (escaped quotes in JSON)
if (strpos($cleaned_json, '\\"') !== false) {
$this->debug_log('⚠️ AI SEO Article Generator JSON Validation: Detected double-encoded JSON, attempting to fix...');
// Try to decode as a JSON string first (if it was JSON-encoded twice)
$test_decode = json_decode($cleaned_json, true);
if (json_last_error() === JSON_ERROR_NONE && is_string($test_decode)) {
$this->debug_log('✅ AI SEO Article Generator JSON Validation: Successfully unwrapped double-encoded JSON');
$cleaned_json = $test_decode;
} else {
// Fallback: manually remove escaping
$this->debug_log('⚠️ AI SEO Article Generator JSON Validation: Manual escape removal...');
$cleaned_json = str_replace('\\"', '"', $cleaned_json);
$cleaned_json = str_replace('\\\\', '\\', $cleaned_json);
}
$this->debug_log('🔍 AI SEO Article Generator JSON Validation: After double-encoding fix: ' . substr($cleaned_json, 0, 200) . '...');
}
// Log a sample of the actual JSON being processed
$this->debug_log('🔍 AI SEO Article Generator JSON Validation: First 500 chars: ' . substr($cleaned_json, 0, 500));
$this->debug_log('🔍 AI SEO Article Generator JSON Validation: Last 100 chars: ' . substr($cleaned_json, -100));
// Decode JSON to validate structure
$decoded = json_decode($cleaned_json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->debug_log('❌ AI SEO Article Generator JSON Validation: JSON decode error: ' . json_last_error_msg());
$this->debug_log('❌ AI SEO Article Generator JSON Validation: JSON error code: ' . json_last_error());
$this->debug_log('❌ AI SEO Article Generator JSON Validation: Problematic JSON sample: ' . substr($cleaned_json, 0, 1000));
return false;
}
// Validate required structure properties
if (!is_array($decoded)) {
$this->debug_log('❌ AI SEO Article Generator JSON Validation: Decoded data is not an array');
return false;
}
// Check for essential structure elements
$required_fields = array('title', 'sections');
foreach ($required_fields as $field) {
if (!isset($decoded[$field])) {
$this->debug_log('❌ AI SEO Article Generator JSON Validation: Missing required field: ' . $field);
return false;
}
}
// Validate sections array
if (!is_array($decoded['sections'])) {
$this->debug_log('❌ AI SEO Article Generator JSON Validation: Sections is not an array');
return false;
}
// Re-encode to ensure clean JSON (this also sanitizes)
$clean_json = json_encode($decoded, JSON_UNESCAPED_UNICODE);
if ($clean_json === false) {
$this->debug_log('❌ AI SEO Article Generator JSON Validation: Failed to re-encode JSON');
return false;
}
$this->debug_log('✅ AI SEO Article Generator JSON Validation: Successfully validated and sanitized JSON');
$this->debug_log('🔍 AI SEO Article Generator JSON Validation: Clean JSON preview: ' . substr($clean_json, 0, 200) . '...');
return $clean_json;
}
public function ajax_save_structure() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
$description = isset($_POST['description']) ? sanitize_textarea_field(wp_unslash($_POST['description'])) : '';
$main_keyword = isset($_POST['main_keyword']) ? sanitize_text_field(wp_unslash($_POST['main_keyword'])) : '';
$sub_keywords = isset($_POST['sub_keywords']) ? sanitize_textarea_field(wp_unslash($_POST['sub_keywords'])) : '';
$target_words = isset($_POST['target_words']) ? intval($_POST['target_words']) : 0;
// Properly handle JSON structure data with WordPress escaping fixes
$raw_structure_data = isset($_POST['structure_data']) ? sanitize_textarea_field(wp_unslash($_POST['structure_data'])) : '';
$this->debug_log('🔍 AI SEO Article Generator Save Structure: Raw POST data received: ' . substr($raw_structure_data, 0, 200) . '...');
$this->debug_log('🔍 AI SEO Article Generator Save Structure: Raw data length: ' . strlen($raw_structure_data));
// Fix WordPress auto-escaping (magic quotes behavior)
if (function_exists('wp_unslash')) {
$unslashed_data = wp_unslash($raw_structure_data);
$this->debug_log('🔍 AI SEO Article Generator Save Structure: After wp_unslash length: ' . strlen($unslashed_data));
} else {
// Fallback for older WordPress versions
$unslashed_data = stripslashes($raw_structure_data);
$this->debug_log('🔍 AI SEO Article Generator Save Structure: After stripslashes length: ' . strlen($unslashed_data));
}
// Fix potential encoding issues with Hebrew text
if (!mb_check_encoding($unslashed_data, 'UTF-8')) {
$this->debug_log('⚠️ AI SEO Article Generator Save Structure: Data encoding issue detected, attempting to fix...');
$unslashed_data = mb_convert_encoding($unslashed_data, 'UTF-8', 'auto');
}
$this->debug_log('🔍 AI SEO Article Generator Save Structure: Final processed data: ' . substr($unslashed_data, 0, 200) . '...');
$this->debug_log('🔍 AI SEO Article Generator Save Structure: Data encoding: ' . mb_detect_encoding($unslashed_data));
$this->debug_log('🔍 AI SEO Article Generator Save Structure: Final data length: ' . strlen($unslashed_data));
// Validate and sanitize JSON using the unslashed data
$structure_data = $this->validate_and_sanitize_structure_json($unslashed_data);
if (empty($name)) {
wp_send_json(array('success' => false, 'message' => __('Structure name is required', 'ai-seo-article-generator')));
return;
}
if ($structure_data === false) {
$this->debug_log('❌ AI SEO Article Generator Save Structure: Invalid JSON structure data');
wp_send_json(array('success' => false, 'message' => __('Structure data is corrupted or missing', 'ai-seo-article-generator')));
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_saved_structures';
$data = array(
'name' => $name,
'description' => $description,
'main_keyword' => $main_keyword,
'sub_keywords' => $sub_keywords,
'target_words' => $target_words,
'structure_data' => $structure_data
);
$this->debug_log('🔍 AI SEO Article Generator Save Structure: Data to be inserted', $data);
$result = $wpdb->insert($table_name, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table insert
if ($result === false) {
$this->debug_log('❌ AI SEO Article Generator Save Structure: Database insert failed. Error: ' . $wpdb->last_error);
wp_send_json(array('success' => false, 'message' => __('Error saving structure to database', 'ai-seo-article-generator')));
return;
}
$structure_id = $wpdb->insert_id;
$this->debug_log('✅ AI SEO Article Generator Save Structure: Successfully saved with ID: ' . $structure_id);
wp_send_json(array('success' => true, 'message' => __('Structure saved successfully!', 'ai-seo-article-generator'), 'structure_id' => $structure_id));
}
public function ajax_load_structure() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$structure_id = isset($_POST['structure_id']) ? intval($_POST['structure_id']) : 0;
$this->debug_log('🔍 AI SEO Article Generator Load Structure: Loading structure ID: ' . $structure_id);
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_saved_structures';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query
$structure = $wpdb->get_row($wpdb->prepare("SELECT * FROM `$table_name` WHERE id = %d", $structure_id));
if (!$structure) {
$this->debug_log('❌ AI SEO Article Generator Load Structure: Structure not found for ID: ' . $structure_id);
wp_send_json(array('success' => false, 'message' => __('Structure not found', 'ai-seo-article-generator')));
return;
}
$this->debug_log('🔍 AI SEO Article Generator Load Structure: Found structure. Raw data length: ' . strlen($structure->structure_data));
$this->debug_log('🔍 AI SEO Article Generator Load Structure: Raw structure_data preview: ' . substr($structure->structure_data, 0, 200) . '...');
// Increment usage count
$wpdb->update($table_name, array('usage_count' => $structure->usage_count + 1), array('id' => $structure_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update
// Parse structure data
$structure_data = null;
if (!empty($structure->structure_data)) {
$structure_data = json_decode($structure->structure_data, true);
$json_error = json_last_error();
if ($json_error !== JSON_ERROR_NONE) {
$this->debug_log('❌ AI SEO Article Generator Load Structure: JSON decode error: ' . json_last_error_msg());
$this->debug_log('❌ AI SEO Article Generator Load Structure: Problematic JSON: ' . $structure->structure_data);
} else {
$this->debug_log('✅ AI SEO Article Generator Load Structure: JSON decoded successfully');
}
} else {
$this->debug_log('❌ AI SEO Article Generator Load Structure: structure_data is empty or null');
}
wp_send_json(array(
'success' => true,
'structure' => array(
'id' => $structure->id,
'name' => $structure->name,
'description' => $structure->description,
'main_keyword' => $structure->main_keyword,
'sub_keywords' => $structure->sub_keywords,
'target_words' => $structure->target_words,
'structure_data' => $structure_data,
'usage_count' => $structure->usage_count + 1,
'created_at' => $structure->created_at
)
));
}
public function ajax_delete_structure() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$structure_id = isset($_POST['structure_id']) ? intval($_POST['structure_id']) : 0;
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_saved_structures';
$result = $wpdb->delete($table_name, array('id' => $structure_id), array('%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table delete
if ($result === false) {
wp_send_json(array('success' => false, 'message' => __('Error deleting structure', 'ai-seo-article-generator')));
return;
}
wp_send_json(array('success' => true, 'message' => __('Structure deleted successfully', 'ai-seo-article-generator')));
}
public function ajax_debug_structures() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_saved_structures';
$structures = $wpdb->get_results("SELECT id, name, structure_data FROM `{$wpdb->prefix}ai_seo_article_generator_saved_structures` ORDER BY id ASC"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query for debug
$this->debug_log('🔍 AI SEO Article Generator Debug: Found ' . count($structures) . ' structures in database');
$debug_info = array();
foreach ($structures as $structure) {
$structure_data_length = strlen($structure->structure_data);
$is_json_valid = false;
$json_error = '';
if (!empty($structure->structure_data)) {
$decoded = json_decode($structure->structure_data, true);
if (json_last_error() === JSON_ERROR_NONE) {
$is_json_valid = true;
} else {
$json_error = json_last_error_msg();
}
}
$debug_info[] = array(
'id' => $structure->id,
'name' => $structure->name,
'data_length' => $structure_data_length,
'is_json_valid' => $is_json_valid,
'json_error' => $json_error,
'data_preview' => substr($structure->structure_data, 0, 100) . '...'
);
$this->debug_log("🔍 Structure ID {$structure->id} ({$structure->name}): Length={$structure_data_length}, Valid JSON={$is_json_valid}, Error={$json_error}");
}
wp_send_json(array(
'success' => true,
'structures_count' => count($structures),
'debug_info' => $debug_info
));
}
public function ajax_queue_background_generation() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$main_keyword = isset($_POST['main_keyword']) ? sanitize_text_field(wp_unslash($_POST['main_keyword'])) : '';
$sub_keywords = isset($_POST['sub_keywords']) ? sanitize_textarea_field(wp_unslash($_POST['sub_keywords'])) : '';
$target_words = isset($_POST['target_words']) ? intval($_POST['target_words']) : 1000;
$article_language = isset($_POST['article_language']) ? sanitize_text_field(wp_unslash($_POST['article_language'])) : 'auto';
$structure_data = isset($_POST['structure_data']) ? sanitize_textarea_field(wp_unslash($_POST['structure_data'])) : '';
if (empty($main_keyword)) {
wp_send_json(array('success' => false, 'message' => __('Main keyword is required', 'ai-seo-article-generator')));
return;
}
// Load background processor
require_once AI_SEO_ARTICLE_GENERATOR_PLUGIN_DIR . 'includes/class-ai-seo-article-generator-background-processor.php';
$result = AI_SEO_Article_Generator_Background_Processor::queue_article_generation(
$main_keyword,
$sub_keywords,
$target_words,
$structure_data ? json_decode($structure_data, true) : null,
$article_language
);
wp_send_json($result);
}
public function ajax_check_job_status() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$job_id = isset($_POST['job_id']) ? intval($_POST['job_id']) : 0;
if (!$job_id) {
wp_send_json(array('success' => false, 'message' => __('Job ID is required', 'ai-seo-article-generator')));
return;
}
// Load background processor
require_once AI_SEO_ARTICLE_GENERATOR_PLUGIN_DIR . 'includes/class-ai-seo-article-generator-background-processor.php';
$result = AI_SEO_Article_Generator_Background_Processor::get_job_status($job_id);
wp_send_json($result);
}
public function ajax_retry_failed_job() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
$job_id = isset($_POST['job_id']) ? intval($_POST['job_id']) : 0;
if (!$job_id) {
wp_send_json(array('success' => false, 'message' => __('Job ID is required', 'ai-seo-article-generator')));
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts';
// Check if job exists and is failed
$job = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query
"SELECT * FROM `$table_name` WHERE id = %d AND background_status = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared
$job_id,
'failed'
));
if (!$job) {
wp_send_json(array('success' => false, 'message' => __('Job not found or not in failed status', 'ai-seo-article-generator')));
return;
}
// Reset job to queued status
$result = $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')
);
if ($result === false) {
wp_send_json(array('success' => false, 'message' => __('Failed to retry job', 'ai-seo-article-generator')));
return;
}
// Clear retry count to start fresh
delete_option('ai_seo_retry_count_job_' . $job_id);
wp_send_json(array(
'success' => true,
'message' => __('Job has been queued for retry', 'ai-seo-article-generator')
));
}
public function ajax_manual_trigger_background() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
// Load background processor
require_once AI_SEO_ARTICLE_GENERATOR_PLUGIN_DIR . 'includes/class-ai-seo-article-generator-background-processor.php';
$processor = new AI_SEO_Article_Generator_Background_Processor();
// Enable debug logging temporarily for this test
update_option('ai_seo_article_generator_debug_logging', 1);
// Log the manual trigger
$this->debug_log('Manual background processing trigger initiated by user');
// Process queued jobs
$processor->process_queued_jobs();
wp_send_json(array(
'success' => true,
'message' => __('Manual background processing triggered. Check debug logs and job status.', 'ai-seo-article-generator')
));
}
public function ajax_publish_article() {
check_ajax_referer('ai_seo_article_generator_nonce', 'nonce');
if (!current_user_can('publish_posts')) {
wp_die('Insufficient permissions to publish posts');
}
// Fix WordPress auto-escaping before processing
$title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_seo_content()
$content = isset($_POST['content']) ? $this->sanitize_seo_content(wp_unslash($_POST['content'])) : '';
$main_keyword = isset($_POST['main_keyword']) ? sanitize_text_field(wp_unslash($_POST['main_keyword'])) : '';
$sub_keywords = isset($_POST['sub_keywords']) ? sanitize_textarea_field(wp_unslash($_POST['sub_keywords'])) : '';
$structure_data = isset($_POST['structure_data']) ? sanitize_textarea_field(wp_unslash($_POST['structure_data'])) : '';
$draft_id = isset($_POST['draft_id']) ? intval($_POST['draft_id']) : null;
$article_language = isset($_POST['article_language']) ? sanitize_text_field(wp_unslash($_POST['article_language'])) : 'auto';
// Clean markdown artifacts from content
$content = $this->clean_content_artifacts($content);
$title = $this->clean_content_artifacts($title);
$main_keyword = $this->clean_content_artifacts($main_keyword);
$sub_keywords = $this->clean_content_artifacts($sub_keywords);
// Determine if content is Hebrew based on selected language
$api = new AI_SEO_Article_Generator_API();
$is_hebrew = $api->determine_content_language($article_language);
// Add table of contents to content
require_once AI_SEO_ARTICLE_GENERATOR_PLUGIN_DIR . 'includes/class-ai-seo-article-generator-generator.php';
$generator = new AI_SEO_Article_Generator_Generator();
$content = $generator->insert_table_of_contents($content, 'auto', $is_hebrew);
if (empty($title) || empty($content)) {
wp_send_json(array('success' => false, 'message' => __('Title and content are required', 'ai-seo-article-generator')));
return;
}
// Parse structure data for meta description
$structure = null;
$meta_description = '';
if (!empty($structure_data)) {
$structure = json_decode($structure_data, true);
if ($structure && isset($structure['introduction'])) {
$meta_description = wp_trim_words($structure['introduction'], 25, '...');
}
}
// If no meta description from structure, create one from content
if (empty($meta_description)) {
$meta_description = wp_trim_words(wp_strip_all_tags($content), 25, '...');
}
// Create WordPress post
$post_data = array(
'post_title' => $title,
'post_content' => $content,
'post_status' => 'draft',
'post_type' => 'post',
'meta_input' => array(
// AI SEO Article Generator specific meta
'ai_seo_article_generator_main_keyword' => $main_keyword,
'ai_seo_article_generator_sub_keywords' => $sub_keywords,
'ai_seo_article_generator_structure_data' => $structure_data,
'ai_seo_article_generator_generated' => true,
'ai_seo_article_generator_draft_id' => $draft_id,
'ai_seo_article_generator_is_hebrew' => $is_hebrew,
// SEO Meta Tags
'_yoast_wpseo_metadesc' => $meta_description, // Yoast SEO
'_aioseop_description' => $meta_description, // All in One SEO
'description' => $meta_description, // Generic
'_yoast_wpseo_focuskw' => $main_keyword, // Yoast focus keyword
'_aioseop_keywords' => $main_keyword . ', ' . $sub_keywords, // AIOSEO keywords
// Open Graph meta
'_yoast_wpseo_opengraph-title' => $title,
'_yoast_wpseo_opengraph-description' => $meta_description,
'_yoast_wpseo_opengraph-image' => '', // Can be populated later
// Twitter Card meta
'_yoast_wpseo_twitter-title' => $title,
'_yoast_wpseo_twitter-description' => $meta_description,
// Language and direction
'content_language' => $is_hebrew ? 'he' : 'en',
'text_direction' => $is_hebrew ? 'rtl' : 'ltr'
)
);
$post_id = wp_insert_post($post_data);
if (is_wp_error($post_id)) {
wp_send_json(array('success' => false, 'message' => __('Error creating post:', 'ai-seo-article-generator') . ' ' . $post_id->get_error_message()));
return;
}
if (!$post_id) {
wp_send_json(array('success' => false, 'message' => __('Error creating post', 'ai-seo-article-generator')));
return;
}
// Generate edit URL for the new post
$edit_url = admin_url('post.php?post=' . $post_id . '&action=edit');
// Update draft status if we have a draft_id
if ($draft_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'ai_seo_article_generator_drafts';
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update
$table_name,
array('status' => 'published', 'wp_post_id' => $post_id),
array('id' => $draft_id),
array('%s', '%d'),
array('%d')
);
}
wp_send_json(array(
'success' => true,
'message' => __('Article created successfully as WordPress draft', 'ai-seo-article-generator'),
'post_id' => $post_id,
'edit_url' => $edit_url
));
}
private function detect_hebrew_content($text) {
// Check if text contains Hebrew characters
return preg_match('/[\x{05D0}-\x{05EA}]/u', $text) > 0;
}
private function clean_content_artifacts($text) {
if (!$text) return '';
// Remove markdown code blocks but preserve HTML structure
$cleaned = preg_replace('/```html\s*/', '', $text);
$cleaned = preg_replace('/```\s*/', '', $cleaned);
// Fix escaped apostrophes and quotes but preserve HTML attributes
$cleaned = preg_replace('/\\\\+\'(?![^<]*>)/', "'", $cleaned);
$cleaned = preg_replace('/\\\\+"(?![^<]*>)/', '"', $cleaned);
$cleaned = preg_replace('/\\\\\\\\(?![^<]*>)/', "\\", $cleaned);
// Fix HTML entities
$cleaned = html_entity_decode($cleaned, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Remove any remaining weird escaping but preserve HTML structure
$cleaned = stripslashes($cleaned);
// Normalize whitespace but preserve HTML structure
// Don't collapse whitespace inside HTML tags
$cleaned = preg_replace('/\s+(?![^<]*>)/', ' ', $cleaned);
// Clean up any malformed HTML tags
$cleaned = $this->fix_malformed_html($cleaned);
return trim($cleaned);
}
private function fix_malformed_html($content) {
// Fix unclosed tags and common HTML issues
$fixed = $content;
// Ensure proper heading tag closure
$fixed = preg_replace('/<(h[1-6])([^>]*)>(.*?)(?=$3$1>', $fixed);
// Ensure proper paragraph tag closure
$fixed = preg_replace('/]*)>(.*?)(?=
$2
', $fixed);
// Fix list items
$fixed = preg_replace('/]*)>(.*?)(?=|<\/ol>|$)/s', '$2', $fixed);
// Remove empty paragraphs
$fixed = preg_replace('/]*>\s*<\/p>/', '', $fixed);
// Remove duplicate consecutive tags
$fixed = preg_replace('/(<\/p>\s*
]*>)+/', '
', $fixed);
return $fixed;
}
private function sanitize_seo_content($content) {
// Define allowed HTML tags for SEO content
$allowed_html = wp_kses_allowed_html('post');
// Add essential SEO tags that might be missing from default allowed tags
$seo_tags = array(
'h1' => array(
'id' => array(),
'class' => array(),
'style' => array(),
),
'h2' => array(
'id' => array(),
'class' => array(),
'style' => array(),
),
'h3' => array(
'id' => array(),
'class' => array(),
'style' => array(),
),
'h4' => array(
'id' => array(),
'class' => array(),
'style' => array(),
),
'h5' => array(
'id' => array(),
'class' => array(),
'style' => array(),
),
'h6' => array(
'id' => array(),
'class' => array(),
'style' => array(),
),
'div' => array(
'id' => array(),
'class' => array(),
'style' => array(),
),
'span' => array(
'id' => array(),
'class' => array(),
'style' => array(),
),
'table' => array(
'id' => array(),
'class' => array(),
'style' => array(),
'border' => array(),
'cellpadding' => array(),
'cellspacing' => array(),
),
'thead' => array(),
'tbody' => array(),
'tr' => array(
'id' => array(),
'class' => array(),
),
'th' => array(
'id' => array(),
'class' => array(),
'scope' => array(),
),
'td' => array(
'id' => array(),
'class' => array(),
'colspan' => array(),
'rowspan' => array(),
),
);
// Merge with existing allowed tags
$allowed_html = array_merge($allowed_html, $seo_tags);
// Apply sanitization with our enhanced allowed tags
return wp_kses($content, $allowed_html);
}
/**
* Calculate word count for content
* Handles both Hebrew and English text properly
*/
public 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
// Hebrew doesn't use spaces between all words consistently
if (preg_match('/[\x{0590}-\x{05FF}]/u', $text)) {
// Hebrew text detected - use character-based estimation
// Average Hebrew word is about 4-5 characters
$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);
}
}