init_hooks(); } /** * Check if testing mode is enabled. * * Testing mode requires both a constant to be set AND admin capability. * To enable testing mode, add this to wp-config.php: * define( 'AEO_BOB_LITE_TESTING_MODE', true ); * * @since 1.0.0 * * @return bool True if testing mode is active. */ public static function is_testing_mode() { $current_user = wp_get_current_user(); // Default: testing mode OFF. $is_testing_mode = false; // Testing mode ON only when constant is set AND user is admin. if ( defined( 'AEO_BOB_LITE_TESTING_MODE' ) && AEO_BOB_LITE_TESTING_MODE ) { if ( current_user_can( 'manage_options' ) ) { $is_testing_mode = true; } } // Allow override via filter. return apply_filters( 'aeo_bob_lite_is_testing_mode', $is_testing_mode, $current_user ); } /** * Check if debug mode is enabled * Debug mode controls console logging and debug output * * @return bool True if debug mode is active */ public static function is_debug_mode() { // Debug mode follows testing mode by default $is_debug_mode = self::is_testing_mode(); // Allow separate override via filter return apply_filters('aeo_bob_lite_enable_debug', $is_debug_mode); } /** * Initialize hooks */ private function init_hooks() { // AJAX handlers for score analysis add_action('wp_ajax_aeo_bob_lite_analyze', array($this, 'ajax_analyze_content')); add_action('wp_ajax_aeo_bob_lite_get_scores', array($this, 'ajax_get_scores')); // v1.5.56: Get existing scores // AJAX handlers for optimization workflow add_action('wp_ajax_aeo_bob_lite_analyze_voice', array($this, 'ajax_analyze_voice')); add_action('wp_ajax_aeo_bob_lite_generate_suggestions', array($this, 'ajax_generate_suggestions')); add_action('wp_ajax_aeo_bob_lite_apply_suggestions', array($this, 'ajax_apply_suggestions')); // Enqueue optimization panel script (Block Editor and Classic Editor compatible) add_action('admin_enqueue_scripts', array($this, 'enqueue_optimization_panel_script')); } /** * AJAX: Analyze content and return scores */ public function ajax_analyze_content() { // Verify nonce and permissions if (!check_ajax_referer('aeo_bob_lite_nonce', 'nonce', false)) { wp_send_json_error(array('message' => esc_html__('Security check failed: invalid nonce. Please refresh the page.', 'aeo-bob-lite'))); return; } if (!isset($_POST['post_id'])) { wp_send_json_error(array('message' => esc_html__('Security check failed: post_id missing', 'aeo-bob-lite'))); return; } $post_id = absint(wp_unslash($_POST['post_id'])); if (!current_user_can('edit_post', $post_id)) { wp_send_json_error(array('message' => esc_html__('Security check failed: insufficient permissions', 'aeo-bob-lite'))); return; } // Get post data $post = get_post($post_id); if (!$post) { wp_send_json_error(array('message' => esc_html__('Post not found', 'aeo-bob-lite'))); return; } // ✅ v1.5.40: Use provided content/title if available (for unsaved changes) // This allows RE-ANALYZE to work on current editor content, not just saved content if (isset($_POST['content']) && !empty($_POST['content'])) { $post->post_content = wp_kses_post(wp_unslash($_POST['content'])); } if (isset($_POST['title']) && !empty($_POST['title'])) { $post->post_title = sanitize_text_field(wp_unslash($_POST['title'])); } try { // Compute scores using the (potentially updated) post object $seo_score = $this->compute_seo_score($post); $aeo_score = $this->compute_aeo_score($post); // Validate scores are numeric if (!is_numeric($seo_score) || !is_numeric($aeo_score)) { wp_send_json_error(array('message' => esc_html__('Analysis error: Invalid scores calculated', 'aeo-bob-lite'))); return; } // Store scores in post meta (Lite version uses post meta, not database table) update_post_meta($post_id, '_aeo_bob_aeo_score', $aeo_score); update_post_meta($post_id, '_aeo_bob_seo_score', $seo_score); update_post_meta($post_id, '_aeo_bob_last_analyzed', current_time('mysql')); // Return scores with hints wp_send_json_success(array( 'scores' => array( 'aeo' => (int)$aeo_score, 'seo' => (int)$seo_score, ), 'hints' => $this->generate_hints_from_scores($post, $aeo_score, $seo_score), )); } catch (Exception $e) { wp_send_json_error(array('message' => esc_html__('Analysis error: ', 'aeo-bob-lite') . $e->getMessage())); } } /** * AJAX handler to get existing scores (for initial page load). * v1.5.56: Fetch scores without re-analyzing */ public function ajax_get_scores() { // Verify nonce and permissions if (!check_ajax_referer('aeo_bob_lite_nonce', 'nonce', false)) { wp_send_json_error(array('message' => esc_html__('Security check failed', 'aeo-bob-lite'))); return; } if (!isset($_POST['post_id'])) { wp_send_json_error(array('message' => esc_html__('Post ID missing', 'aeo-bob-lite'))); return; } $post_id = absint(wp_unslash($_POST['post_id'])); if (!current_user_can('edit_post', $post_id)) { wp_send_json_error(array('message' => esc_html__('Permission denied', 'aeo-bob-lite'))); return; } // Get scores from post meta $seo_score_raw = get_post_meta($post_id, '_aeo_bob_seo_score', true); $aeo_score_raw = get_post_meta($post_id, '_aeo_bob_aeo_score', true); $last_analyzed = get_post_meta($post_id, '_aeo_bob_last_analyzed', true); // Validate and convert $seo_score = ($seo_score_raw !== '' && $seo_score_raw !== false) ? (int)$seo_score_raw : null; $aeo_score = ($aeo_score_raw !== '' && $aeo_score_raw !== false) ? (int)$aeo_score_raw : null; $has_scores = ($seo_score !== null && $aeo_score !== null); wp_send_json_success(array( 'has_scores' => $has_scores, 'seo_score' => $seo_score, 'aeo_score' => $aeo_score, 'updated_at' => $last_analyzed, )); } /** * Generate actionable hints from analysis results */ private function generate_hints($result) { $hints = array(); // AEO Hints if (isset($result['aeo_score']) && $result['aeo_score'] < 75) { $hints[] = array( 'type' => 'aeo', 'icon' => '💡', 'title' => esc_html__('Improve Answer Engine Optimization', 'aeo-bob-lite'), 'description' => esc_html__('Your content could be more AI-friendly. Consider answering questions directly and using clear, structured formatting.', 'aeo-bob-lite'), 'pro' => false, ); // Question detection hint if (isset($result['question_score']) && $result['question_score'] < 50) { $hints[] = array( 'type' => 'aeo', 'icon' => '❓', 'title' => esc_html__('Add Question-Answer Format', 'aeo-bob-lite'), 'description' => esc_html__('AI engines love Q&A format. Try adding "What is...?" or "How to...?" questions followed by clear answers.', 'aeo-bob-lite'), 'pro' => false, ); } // Structure hint if (isset($result['structure_score']) && $result['structure_score'] < 50) { $hints[] = array( 'type' => 'aeo', 'icon' => '📋', 'title' => esc_html__('Improve Content Structure', 'aeo-bob-lite'), 'description' => esc_html__('Use clear headings (H2, H3), short paragraphs, and bullet points to make your content scannable for AI.', 'aeo-bob-lite'), 'pro' => false, ); } } // SEO Hints if (isset($result['seo_score']) && $result['seo_score'] < 75) { $hints[] = array( 'type' => 'seo', 'icon' => '🔍', 'title' => esc_html__('Boost SEO Score', 'aeo-bob-lite'), 'description' => esc_html__('Optimize your title, meta description, and heading structure for better search engine visibility.', 'aeo-bob-lite'), 'pro' => false, ); // Keyword density if (isset($result['keyword_score']) && $result['keyword_score'] < 50) { $hints[] = array( 'type' => 'seo', 'icon' => '🎯', 'title' => esc_html__('Optimize Keyword Usage', 'aeo-bob-lite'), 'description' => esc_html__('Use your focus keyword naturally throughout the content, especially in headings and the first paragraph.', 'aeo-bob-lite'), 'pro' => false, ); } } // Pro feature teaser if (count($hints) > 0) { $hints[] = array( 'type' => 'pro', 'icon' => '⭐', 'title' => esc_html__('Want AI-Powered Suggestions?', 'aeo-bob-lite'), 'description' => esc_html__('Upgrade to AEO Bob Pro to get specific, actionable AI suggestions you can apply with one click.', 'aeo-bob-lite'), 'pro' => true, 'upgrade_url' => admin_url('admin.php?page=aeo-bob-lite-upgrade'), ); } return $hints; } /** * Get overall site statistics (from post meta, not database table) */ public function get_site_stats() { global $wpdb; // Count posts with AEO scores in post meta // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Analytics query for dashboard display $stats = $wpdb->get_row( "SELECT COUNT(DISTINCT pm1.post_id) as analyzed, AVG(CAST(pm1.meta_value AS UNSIGNED)) as avg_aeo, AVG(CAST(pm2.meta_value AS UNSIGNED)) as avg_seo FROM " . $wpdb->postmeta . " pm1 LEFT JOIN " . $wpdb->postmeta . " pm2 ON pm1.post_id = pm2.post_id AND pm2.meta_key = '_aeo_bob_seo_score' WHERE pm1.meta_key = '_aeo_bob_aeo_score' AND pm1.meta_value != ''", ARRAY_A ); // Count only published posts and pages (exclude drafts, trash, etc.) $total_posts = wp_count_posts('post'); $total_pages = wp_count_posts('page'); // Only count publish status $published_posts = isset($total_posts->publish) ? intval($total_posts->publish) : 0; $published_pages = isset($total_pages->publish) ? intval($total_pages->publish) : 0; return array( 'total_posts' => $published_posts + $published_pages, 'analyzed' => intval($stats['analyzed'] ?? 0), 'avg_aeo' => round(floatval($stats['avg_aeo'] ?? 0), 1), 'avg_seo' => round(floatval($stats['avg_seo'] ?? 0), 1), ); } /** * Compute SEO score - Enhanced v2.3.09 algorithm from Pro * Features: Gradient scoring, better meta detection, improved link counting * * @since 1.5.73 - Synced from Pro v2.3.09 */ private function compute_seo_score( $post ) { $score = 0; $content = $post->post_content; $title = $post->post_title; $post_id = $post->ID; // ================================================================= // TITLE OPTIMIZATION (25 points) // ================================================================= $title_length = strlen( $title ); if ( $title_length >= 30 && $title_length <= 60 ) { $score += 15; // Optimal length } elseif ( $title_length >= 25 && $title_length <= 65 ) { $score += 12; } elseif ( $title_length >= 20 && $title_length <= 70 ) { $score += 8; } elseif ( $title_length > 0 ) { $score += 4; } // Title contains power words / question words if ( preg_match( '/\b(how|what|why|when|where|which|who)\b/i', $title ) ) { $score += 5; // Question words (great for featured snippets) } if ( preg_match( '/\b(best|top|guide|tips|secrets|ultimate|complete|essential|proven|easy)\b/i', $title ) ) { $score += 5; // Power words } // ================================================================= // CONTENT LENGTH (25 points) - Gradient scoring // ================================================================= $word_count = str_word_count( wp_strip_all_tags( $content ) ); if ( $word_count >= 2000 ) { $score += 25; // Comprehensive } elseif ( $word_count >= 1500 ) { $score += 23; } elseif ( $word_count >= 1000 ) { $score += 20; } elseif ( $word_count >= 750 ) { $score += 17; } elseif ( $word_count >= 500 ) { $score += 14; } elseif ( $word_count >= 300 ) { $score += 10; } elseif ( $word_count >= 100 ) { $score += 5; } // ================================================================= // CONTENT STRUCTURE (25 points) - Gradient scoring // ================================================================= $headings = preg_match_all( '/]*>/i', $content ); $headings = ( $headings === false ) ? 0 : (int) $headings; if ( $headings >= 6 ) { $score += 25; // Excellent structure } elseif ( $headings >= 5 ) { $score += 23; } elseif ( $headings >= 4 ) { $score += 20; } elseif ( $headings >= 3 ) { $score += 16; } elseif ( $headings >= 2 ) { $score += 12; } elseif ( $headings >= 1 ) { $score += 6; } // ================================================================= // INTERNAL LINKING (15 points) - Gradient + multiple detection methods // ================================================================= $internal_links = 0; // Count absolute internal links (https://example.com/page) $home = untrailingslashit( home_url() ); $absolute_links = preg_match_all( '/]*href="[^"]*' . preg_quote( $home, '/' ) . '(?:[\/?#]|$)[^"]*"[^>]*>/i', $content ); $internal_links += ( $absolute_links === false ) ? 0 : (int) $absolute_links; // Count relative internal links (starting with / but not //) $relative_links = preg_match_all( '/]*href="\/(?!\/)[^"]*"[^>]*>/i', $content ); $internal_links += ( $relative_links === false ) ? 0 : (int) $relative_links; // Count relative links without leading slash (href="page-name") // Exclude external protocols (http, https, mailto, tel, #) $simple_relative = preg_match_all( '/]*href="(?!https?:|mailto:|tel:|#|\/\/)[^"\/][^"]*"[^>]*>/i', $content ); $internal_links += ( $simple_relative === false ) ? 0 : (int) $simple_relative; if ( $internal_links >= 5 ) { $score += 15; } elseif ( $internal_links >= 4 ) { $score += 13; } elseif ( $internal_links >= 3 ) { $score += 11; } elseif ( $internal_links >= 2 ) { $score += 8; } elseif ( $internal_links >= 1 ) { $score += 5; } // ================================================================= // META DESCRIPTION (10 points) - Check multiple sources // ================================================================= $meta_description = ''; // Check multiple meta keys (supports Yoast, AIOSEO, Rank Math) $meta_keys = array( '_aeo_bob_meta_description', '_yoast_wpseo_metadesc', '_aioseo_description', '_rank_math_description', ); foreach ( $meta_keys as $key ) { $meta_val = get_post_meta( $post_id, $key, true ); if ( ! empty( $meta_val ) ) { $meta_description = $meta_val; break; } } if ( ! empty( $meta_description ) ) { $meta_len = strlen( $meta_description ); if ( $meta_len >= 120 && $meta_len <= 160 ) { $score += 10; // Perfect length } elseif ( $meta_len >= 100 && $meta_len <= 180 ) { $score += 7; // Good length } elseif ( $meta_len >= 50 ) { $score += 4; // Has description } } return min( 100, $score ); } /** * Compute AEO score - Enhanced v2.3.09 algorithm from Pro * Features: Gradient FAQ scoring, question heading detection, multiple schema keys * * @since 1.5.73 - Synced from Pro v2.3.09 */ private function compute_aeo_score( $post ) { $score = 0; $content = $post->post_content; $post_id = $post->ID; // ================================================================= // FAQ/Q&A CONTENT (30 points) - GRADIENT scoring with 4 detection methods // ================================================================= $faq_score = 0; // Method 1: Explicit FAQ section heading (10 points) $faq_section_patterns = array( '/]*>.*\b(faq|frequently asked questions)\b.*<\/h[1-6]>/i', '/]*>.*\b(questions? and answers?|q\s*&\s*a)\b.*<\/h[1-6]>/i', ); foreach ( $faq_section_patterns as $pattern ) { if ( preg_match( $pattern, $content ) ) { $faq_score += 10; break; } } // Method 2: Question headings (headings ending with ?) - 2 points each, max 10 $question_headings = preg_match_all( '/]*>[^<]*\?<\/h[2-4]>/i', $content ); $question_headings = ( $question_headings === false ) ? 0 : (int) $question_headings; $faq_score += min( 10, $question_headings * 2 ); // Method 3: Stored FAQ section from AI optimization (5 points) $stored_faq = get_post_meta( $post_id, '_aeo_bob_lite_faq_section', true ); if ( ! empty( $stored_faq ) ) { $faq_data = is_string( $stored_faq ) ? json_decode( $stored_faq, true ) : $stored_faq; if ( is_array( $faq_data ) && count( $faq_data ) >= 3 ) { $faq_score += 5; } } // Method 4: Q: / A: patterns in content (5 points) $qa_patterns = preg_match_all( '/\b(q:|a:)\s/i', $content ); if ( $qa_patterns >= 2 ) { $faq_score += 5; } $score += min( 30, $faq_score ); // ================================================================= // STRUCTURED CONTENT (25 points) - Gradient scoring // ================================================================= $headings = preg_match_all( '/]*>/i', $content ); $headings = ( $headings === false ) ? 0 : (int) $headings; if ( $headings >= 5 ) { $score += 25; // Excellent structure } elseif ( $headings >= 4 ) { $score += 22; } elseif ( $headings >= 3 ) { $score += 18; } elseif ( $headings >= 2 ) { $score += 14; // Good structure } elseif ( $headings >= 1 ) { $score += 8; // Basic structure } // ================================================================= // SCHEMA MARKUP (20 points) - Check multiple meta keys + content scan // ================================================================= $schema_score = 0; $has_faqpage_schema = false; // Check multiple meta keys $schema_keys = array( '_aeo_bob_schema_markup', '_aeo_bob_lite_structured_data', '_aeo_bob_lite_schema', ); foreach ( $schema_keys as $key ) { $schema_data = get_post_meta( $post_id, $key, true ); if ( ! empty( $schema_data ) ) { $schema_score = 15; // Check for FAQPage schema in stored meta $schema_str = is_string( $schema_data ) ? $schema_data : wp_json_encode( $schema_data ); if ( preg_match( '/"@type"\s*:\s*"FAQPage"/i', $schema_str ) ) { $has_faqpage_schema = true; } break; } } // Also check for JSON-LD in content itself (bonus 5 points) if ( preg_match( '/]*type=["\']application\/ld\+json["\'][^>]*>/i', $content ) ) { $schema_score = min( 20, $schema_score + 10 ); } // Check for FAQPage schema specifically (very valuable for AEO) if ( $has_faqpage_schema || preg_match( '/"@type"\s*:\s*"FAQPage"/i', $content ) ) { $schema_score = 20; } $score += $schema_score; // ================================================================= // CONTENT DEPTH & EXPERTISE (15 points) - Gradient scoring // ================================================================= $word_count = str_word_count( wp_strip_all_tags( $content ) ); if ( $word_count >= 2000 ) { $score += 15; // Comprehensive content } elseif ( $word_count >= 1500 ) { $score += 13; } elseif ( $word_count >= 1000 ) { $score += 11; } elseif ( $word_count >= 800 ) { $score += 9; } elseif ( $word_count >= 500 ) { $score += 6; } elseif ( $word_count >= 300 ) { $score += 3; } // ================================================================= // ANSWER-FOCUSED CONTENT (10 points) - More patterns, gradient scoring // ================================================================= $answer_patterns = array( '/\b(how to|what is|what are|why does|why do|when should|where can|which)\b/i', '/\b(step|steps|guide|tutorial|explanation|overview|introduction)\b/i', '/\b(benefits?|advantages?|disadvantages?|pros|cons|comparison)\b/i', '/\b(definition|meaning|example|examples|tips?|advice)\b/i', '/\b(best practices?|recommendations?|solutions?)\b/i', ); $answer_signals = 0; foreach ( $answer_patterns as $pattern ) { if ( preg_match( $pattern, $content ) ) { $answer_signals++; } } if ( $answer_signals >= 4 ) { $score += 10; } elseif ( $answer_signals >= 3 ) { $score += 8; } elseif ( $answer_signals >= 2 ) { $score += 6; } elseif ( $answer_signals >= 1 ) { $score += 3; } return min( 100, $score ); } /** * Generate hints from computed scores */ private function generate_hints_from_scores($post, $aeo_score, $seo_score) { $hints = array(); $content = $post->post_content; $title = $post->post_title; // AEO Hints if ($aeo_score < 75) { $word_count = str_word_count(wp_strip_all_tags($content)); // Check for FAQ if (!preg_match('/faq|frequently asked questions/i', $content)) { $hints[] = array( 'type' => 'aeo', 'icon' => '❓', 'title' => esc_html__('Add FAQ Section', 'aeo-bob-lite'), 'description' => esc_html__('AI engines love Q&A format. Add an FAQ section with common questions and clear answers.', 'aeo-bob-lite'), 'pro' => false, ); } // Check for headings $headings = preg_match_all('/]*>/i', $content); if ($headings < 3) { $hints[] = array( 'type' => 'aeo', 'icon' => '📑', 'title' => esc_html__('Improve Content Structure', 'aeo-bob-lite'), 'description' => esc_html__('Add more headings (H2, H3) to break up your content and make it easier for AI to understand.', 'aeo-bob-lite'), 'pro' => false, ); } // Check content depth if ($word_count < 800) { $hints[] = array( 'type' => 'aeo', 'icon' => '📝', 'title' => esc_html__('Expand Your Content', 'aeo-bob-lite'), 'description' => esc_html__('Add more comprehensive information. Aim for at least 800 words for better AI visibility.', 'aeo-bob-lite'), 'pro' => false, ); } } // SEO Hints if ($seo_score < 75) { $title_length = strlen($title); if ($title_length < 30 || $title_length > 60) { $hints[] = array( 'type' => 'seo', 'icon' => '🎯', 'title' => esc_html__('Optimize Title Length', 'aeo-bob-lite'), 'description' => esc_html__('Keep your title between 30-60 characters for best SEO results.', 'aeo-bob-lite'), 'pro' => false, ); } // Check for internal links $internal_links = preg_match_all('/]*href="[^"]*' . preg_quote(home_url(), '/') . '[^"]*"[^>]*>/i', $content); if ($internal_links < 2) { $hints[] = array( 'type' => 'seo', 'icon' => '🔗', 'title' => esc_html__('Add Internal Links', 'aeo-bob-lite'), 'description' => esc_html__('Link to other relevant pages on your site to improve SEO and user experience.', 'aeo-bob-lite'), 'pro' => false, ); } } // Pro feature teaser if (count($hints) > 0) { $hints[] = array( 'type' => 'pro', 'icon' => '⭐', 'title' => esc_html__('Want AI-Powered Suggestions?', 'aeo-bob-lite'), 'description' => esc_html__('Upgrade to AEO Bob Pro to get specific, actionable AI suggestions you can apply with one click.', 'aeo-bob-lite'), 'pro' => true, 'upgrade_url' => admin_url('admin.php?page=aeo-bob-lite-upgrade'), ); } return $hints; } /** * AJAX: Analyze author voice */ public function ajax_analyze_voice() { // Security checks check_ajax_referer('aeo_bob_lite_nonce', 'nonce', true); if (!isset($_POST['post_id']) || !current_user_can('edit_post', absint(wp_unslash($_POST['post_id'])))) { wp_send_json_error('Permission denied'); } $post_id = absint(wp_unslash($_POST['post_id'])); $post = get_post($post_id); if (!$post) { wp_send_json_error('Post not found'); } // Check if Author Voice class exists if (!class_exists('AEO_Bob_Author_Voice')) { require_once AEO_BOB_LITE_PATH . 'includes/core/class-aeo-bob-author-voice.php'; } try { $author_voice = AEO_Bob_Author_Voice::get_instance(); $profile = $author_voice->update_author_voice_profile($post_id); if (!$profile || empty($profile)) { wp_send_json_error('Failed to analyze author voice'); } // Save profile update_post_meta($post_id, '_aeo_bob_author_voice_profile', $profile); wp_send_json_success(array( 'profile' => $profile, 'message' => 'Author voice analyzed successfully' )); } catch (Exception $e) { wp_send_json_error('Analysis failed: ' . $e->getMessage()); } } /** * AJAX: Generate AI suggestions (limited to 3 content + 3 FAQ) */ public function ajax_generate_suggestions() { // Security checks check_ajax_referer('aeo_bob_lite_nonce', 'nonce', true); if (!isset($_POST['post_id']) || !current_user_can('edit_post', absint(wp_unslash($_POST['post_id'])))) { wp_send_json_error('Permission denied'); } $post_id = absint(wp_unslash($_POST['post_id'])); $post = get_post($post_id); if (!$post) { wp_send_json_error('Post not found'); } // Check API key $api_key = get_option('aeo_bob_lite_openai_key', ''); if (empty($api_key)) { wp_send_json_error('OpenAI API key not configured. Please add your API key in settings.'); } try { // Generate suggestions using optimizer class if (!class_exists('AEO_Bob_Lite_Optimizer')) { wp_send_json_error('Optimizer class not found'); } $optimizer = AEO_Bob_Lite_Optimizer::get_instance(); $suggestions = $optimizer->generate_suggestions($post_id); if (!$suggestions || empty($suggestions)) { wp_send_json_error('Failed to generate suggestions. Please try again.'); } // Store suggestions for later application update_post_meta($post_id, '_aeo_bob_lite_suggestions_cache', $suggestions); wp_send_json_success(array( 'suggestions' => $suggestions, 'message' => 'Suggestions generated successfully' )); } catch (Exception $e) { wp_send_json_error('Failed to generate suggestions: ' . $e->getMessage()); } } /** * AJAX: Apply selected suggestions to content */ public function ajax_apply_suggestions() { // Security checks check_ajax_referer('aeo_bob_lite_nonce', 'nonce', true); if (!isset($_POST['post_id']) || !current_user_can('edit_post', absint(wp_unslash($_POST['post_id'])))) { wp_send_json_error('Permission denied'); } $post_id = absint(wp_unslash($_POST['post_id'])); /* * JSON INPUT HANDLING - Why we decode BEFORE sanitizing: * * 1. JSON is a structured data format - sanitizing the raw string would corrupt it * 2. WordPress sanitize_* functions are for individual values, not JSON structures * 3. Proper approach: decode first, then sanitize each field individually * 4. This follows WordPress.org coding standards for JSON input handling */ // Step 1: Unslash the raw JSON string (WordPress adds slashes to $_POST) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON must be decoded before sanitization; individual fields sanitized below $selected_raw = isset( $_POST['selected'] ) ? wp_unslash( $_POST['selected'] ) : ''; // Step 2: Validate we received input if ( empty( $selected_raw ) || ! is_string( $selected_raw ) ) { wp_send_json_error( array( 'message' => esc_html__( 'No suggestions data provided', 'aeo-bob-lite' ) ) ); return; } // Step 3: Decode JSON string to PHP array $selected_decoded = json_decode( $selected_raw, true ); // Step 4: Validate JSON decode succeeded if ( json_last_error() !== JSON_ERROR_NONE ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid JSON format', 'aeo-bob-lite' ), 'error' => json_last_error_msg(), ) ); return; } // Step 5: Validate decoded result is an array if ( ! is_array( $selected_decoded ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid suggestions format: expected array', 'aeo-bob-lite' ) ) ); return; } // Step 6: Sanitize each item individually (proper WordPress approach) $selected = array(); foreach ( $selected_decoded as $item ) { // Skip non-array items if ( ! is_array( $item ) ) { continue; } $sanitized_item = array(); // Sanitize 'type' field - string, sanitized with sanitize_text_field() if ( isset( $item['type'] ) && is_string( $item['type'] ) ) { $sanitized_item['type'] = sanitize_text_field( $item['type'] ); } // Sanitize 'index' field - integer, sanitized with absint() if ( isset( $item['index'] ) && is_numeric( $item['index'] ) ) { $sanitized_item['index'] = absint( $item['index'] ); } // Only include items that have all required fields after sanitization if ( ! empty( $sanitized_item['type'] ) && isset( $sanitized_item['index'] ) ) { $selected[] = $sanitized_item; } } // Step 7: Validate we have at least one valid sanitized item if ( empty( $selected ) ) { wp_send_json_error( array( 'message' => esc_html__( 'No valid suggestions selected', 'aeo-bob-lite' ) ) ); return; } $post = get_post($post_id); if (!$post) { wp_send_json_error('Post not found'); } // Get cached suggestions $suggestions = get_post_meta($post_id, '_aeo_bob_lite_suggestions_cache', true); if (!$suggestions) { wp_send_json_error('No cached suggestions found. Please generate suggestions again.'); } try { // Apply suggestions using optimizer $optimizer = AEO_Bob_Lite_Optimizer::get_instance(); $result = $optimizer->apply_suggestions($post_id, $selected, $suggestions); if (!$result || (is_array($result) && !$result['success'])) { // Check if this is an Elementor detection error if (is_array($result) && isset($result['error']) && $result['error'] === 'elementor_detected') { wp_send_json_error(array( 'message' => $result['message'], 'upgrade_url' => $result['upgrade_url'] ?? 'https://aeobob.com/pricing/', 'upgrade_text' => $result['upgrade_text'] ?? esc_html__('Upgrade to Pro', 'aeo-bob-lite'), 'elementor_detected' => true )); } wp_send_json_error('Failed to apply suggestions'); } // Clear cache delete_post_meta($post_id, '_aeo_bob_lite_suggestions_cache'); // Return success with updated content $response = array( 'message' => 'Suggestions applied successfully', ); // Include updated content if available if (is_array($result)) { // ✅ v1.5.37: CRITICAL - Pass through elementor_updated flag! if (isset($result['elementor_updated'])) { $response['elementor_updated'] = $result['elementor_updated']; } if (isset($result['content'])) { $response['content'] = $result['content']; } if (isset($result['title_changed']) && $result['title_changed']) { $response['title'] = $result['title']; } if (isset($result['sections_added'])) { $response['sections_added'] = $result['sections_added']; } } wp_send_json_success($response); } catch (Exception $e) { wp_send_json_error('Failed to apply suggestions: ' . $e->getMessage()); } } /** * Enqueue optimization panel script (Block Editor and Classic Editor compatible) * v1.5.47.44: Moved from admin_print_footer_scripts to admin_enqueue_scripts for reliable context */ public function enqueue_optimization_panel_script($hook) { // Only load on post.php and post-new.php (edit screens) if (!in_array($hook, array('post.php', 'post-new.php'))) { return; } // Don't load in Elementor editor - it has its own panel // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Context check only if (isset($_GET['action']) && sanitize_text_field(wp_unslash($_GET['action'])) === 'elementor') { return; } // Get current screen and validate post type $screen = get_current_screen(); if (!$screen || !isset($screen->post_type) || !in_array($screen->post_type, array('post', 'page'), true)) { return; } // Get post ID from global $post (safe - no direct input usage) $post_id = 0; global $post; if ( $post && isset( $post->ID ) ) { $post_id = absint( $post->ID ); } elseif ( function_exists( 'get_the_ID' ) ) { $post_id = absint( get_the_ID() ); } // For new posts, ID might be 0 - that's OK, the panel will still load if (!$post_id) { return; // Skip for brand new posts that haven't been saved yet } // Enqueue the external optimizer panel script. wp_enqueue_script( 'aeo-bob-lite-optimizer-panel', AEO_BOB_LITE_URL . 'assets/js/optimizer-panel.js', array( 'jquery' ), AEO_BOB_LITE_VERSION, true ); // Build data object with localized strings. $data = array( 'postId' => intval( $post_id ), 'nonce' => wp_create_nonce( 'aeo_bob_lite_nonce' ), 'upgradeUrl' => admin_url( 'admin.php?page=aeo-bob-lite-upgrade' ), 'i18n' => array( 'analyzing' => __( 'Analyzing...', 'aeo-bob-lite' ), 'voiceProfileDetected' => __( 'Voice Profile Detected', 'aeo-bob-lite' ), 'style' => __( 'Style', 'aeo-bob-lite' ), 'tone' => __( 'Tone', 'aeo-bob-lite' ), 'readingLevel' => __( 'Reading Level', 'aeo-bob-lite' ), 'voiceAnalyzedSuccess' => __( 'Author voice analyzed successfully!', 'aeo-bob-lite' ), 'reanalyzeVoice' => __( 'Re-analyze Author Voice', 'aeo-bob-lite' ), 'analysisFailed' => __( 'Analysis failed', 'aeo-bob-lite' ), 'networkError' => __( 'Network error. Please try again.', 'aeo-bob-lite' ), 'timeout' => __( 'Request timed out. Please try again.', 'aeo-bob-lite' ), 'generating' => __( 'Generating...', 'aeo-bob-lite' ), 'unableToGenerate' => __( 'Unable to generate suggestions. Please check your settings.', 'aeo-bob-lite' ), 'titleSuggestions' => __( 'Title Suggestions', 'aeo-bob-lite' ), 'chooseOne' => __( 'Choose ONE', 'aeo-bob-lite' ), 'originalTitle' => __( 'Original Title', 'aeo-bob-lite' ), 'contentImprovements' => __( 'Content Improvements', 'aeo-bob-lite' ), 'selectImprovements' => __( 'Select improvements to apply.', 'aeo-bob-lite' ), 'original' => __( 'ORIGINAL', 'aeo-bob-lite' ), 'improved' => __( 'IMPROVED', 'aeo-bob-lite' ), 'newParagraph' => __( 'NEW PARAGRAPH', 'aeo-bob-lite' ), 'faqSuggestions' => __( 'FAQ Suggestions', 'aeo-bob-lite' ), 'faqHint' => __( 'These are NEW Q&A pairs that will be added to your post.', 'aeo-bob-lite' ), 'suggestionsGenerated' => __( 'Suggestions generated! Select the ones you want to apply.', 'aeo-bob-lite' ), 'getNewSuggestions' => __( 'Get New Suggestions', 'aeo-bob-lite' ), 'failedGenerate' => __( 'Failed to generate suggestions', 'aeo-bob-lite' ), 'getSuggestions' => __( 'Get AI Suggestions', 'aeo-bob-lite' ), 'timeoutLong' => __( 'Request timed out (60s). The API might be slow.', 'aeo-bob-lite' ), 'selectAtLeastOne' => __( 'Please select at least one suggestion to apply.', 'aeo-bob-lite' ), 'applyConfirm' => __( 'Apply {count} suggestion(s) to your content?', 'aeo-bob-lite' ), 'applying' => __( 'Applying...', 'aeo-bob-lite' ), 'suggestionsApplied' => __( 'Suggestions Applied Successfully!', 'aeo-bob-lite' ), 'suggestionsAddedCount' => __( '{count} suggestion(s) have been added.', 'aeo-bob-lite' ), 'reloadHint' => __( 'The page will reload to show updated content.', 'aeo-bob-lite' ), 'applySelected' => __( 'Apply Selected Suggestions', 'aeo-bob-lite' ), 'reloading' => __( 'Reloading page...', 'aeo-bob-lite' ), 'elementorDetected' => __( 'Elementor Detected', 'aeo-bob-lite' ), 'elementorMessage' => __( 'This post was created with Elementor.', 'aeo-bob-lite' ), 'elementorTip' => __( 'You can still manually copy and paste the suggestions.', 'aeo-bob-lite' ), 'failedApply' => __( 'Failed to apply suggestions', 'aeo-bob-lite' ), 'analyzingContent' => __( 'Analyzing content...', 'aeo-bob-lite' ), 'scoreImprovement' => __( 'Score Improvement', 'aeo-bob-lite' ), 'seoScore' => __( 'SEO Score', 'aeo-bob-lite' ), 'aeoScore' => __( 'AEO Score', 'aeo-bob-lite' ), 'points' => __( 'points', 'aeo-bob-lite' ), 'noChange' => __( 'No change', 'aeo-bob-lite' ), 'rescored' => __( 'Re-scored!', 'aeo-bob-lite' ), 'rescoreFailed' => __( 'Failed to re-score.', 'aeo-bob-lite' ), 'rescoreContent' => __( 'Re-Score Content', 'aeo-bob-lite' ), ), ); // Localize data to window object. wp_localize_script( 'aeo-bob-lite-optimizer-panel', 'aeoBobLiteOptimizerData', $data ); } }