review_repository = new AIFQ_SR_Review_Repository(); $this->review_normalizer = new AIFQ_SR_Review_Normalizer( fn(array $issue) => $this->issue_confidence($issue), fn(array $issue) => $this->issue_simple_explanation($issue), fn(array $issue) => $this->issue_if_ignored($issue) ); $this->reviewed_findings_service = new AIFQ_SR_Reviewed_Findings_Service( $this->review_repository, fn($severity) => $this->severity_sort_value_for_key($severity), fn(string $key, array $reviewed_state): bool => in_array( $key, AIFQ_SR_Finding_Catalog::automatic_revisit_eligible_keys(), true ) ); $this->site_collector = new AIFQ_SR_Site_Collector(); $this->operational_evaluator = new AIFQ_SR_Operational_Evaluator( fn($key, $category, $title, $severity, $explanation, $recommendation, $context = '') => $this->issue( $key, $category, $title, $severity, $explanation, $recommendation, $context ) ); $this->evaluation_pipeline = new AIFQ_SR_Evaluation_Pipeline( fn(array $data) => $this->run_local_checks($data), fn(array $result, array $data) => $this->apply_findings_filter($result, $data), fn(array $issues, array $suggestions, array $passed) => $this->finalize_scan_items($issues, $suggestions, $passed), fn(array $data) => $this->operational_evaluator->evaluate($data) ); $this->copy_builder = new AIFQ_SR_Copy_Builder( fn($timestamp) => $this->format_scan_date($timestamp), fn($issue) => $this->display_issue_title($issue), fn($issue) => $this->display_issue_recommendation($issue) ); $this->memory_manager = new AIFQ_SR_Memory_Manager(); $this->observed_event_recorder = new AIFQ_SR_Observed_Event_Recorder(); $this->memory_processor = new AIFQ_SR_Memory_Processor(); $this->decision_memory_service = new AIFQ_SR_Decision_Memory_Service(); $this->research_store = new AIFQ_SR_Research_Store(); $this->observed_event_recorder->register_hooks(); add_action('admin_init', [$this, 'ensure_memory_foundation']); add_action('admin_menu', [$this, 'register_admin_menu']); add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); add_action('admin_post_aifq_sr_run_scan', [$this, 'handle_scan']); add_action('admin_post_aifq_sr_mark_reviewed', [$this, 'handle_mark_reviewed']); add_action('admin_post_aifq_sr_save_decision', [$this, 'handle_save_decision']); add_action('admin_post_aifq_sr_unmark_reviewed', [$this, 'handle_unmark_reviewed']); add_action('admin_post_aifq_sr_research_start', [$this, 'handle_research_start']); add_action('admin_post_aifq_sr_research_reminder', [$this, 'handle_research_reminder']); add_action('wp_ajax_aifq_sr_research_event', [$this, 'handle_research_event']); } public static function activate($network_wide = false) { // Memory is deliberately site-scoped. On multisite/network activation // other sites initialize lazily when AIFQ runs in their context. try { $manager = new AIFQ_SR_Memory_Manager(); $manager->ensure_foundation(); AIFQ_SR_Decision_Memory_Schema::ensure_current_site_schema(); } catch (Throwable $e) { // Optional Memory must never make plugin activation fail. } } public function ensure_memory_foundation(bool $force = false): bool { if (!$force && get_transient(self::MEMORY_FOUNDATION_BACKOFF_TRANSIENT)) { return false; } try { $ok = $this->memory_manager->ensure_foundation(); if (!$ok) { set_transient(self::MEMORY_FOUNDATION_BACKOFF_TRANSIENT, 1, HOUR_IN_SECONDS); AIFQ_SR_Research_Store::record_memory_error('foundation_unavailable'); return false; } delete_transient(self::MEMORY_FOUNDATION_BACKOFF_TRANSIENT); return true; } catch (Throwable $e) { // The current Site Review remains fully functional when Memory is // unavailable or cannot be initialized. Back off routine admin // retries, while an explicit Site Review may force one fresh try. set_transient(self::MEMORY_FOUNDATION_BACKOFF_TRANSIENT, 1, HOUR_IN_SECONDS); AIFQ_SR_Research_Store::record_memory_error('foundation_exception', $e->getMessage()); return false; } } public static function deactivate() { delete_transient('aifq_sr_large_images_count'); delete_transient('aifq_sr_autoloaded_options_size'); delete_transient('aifq_sr_post_revisions_count'); delete_transient(self::MEMORY_FOUNDATION_BACKOFF_TRANSIENT); } public static function uninstall() { AIFQ_SR_Memory_Schema::uninstall_all_sites(); AIFQ_SR_Decision_Memory_Schema::uninstall_all_sites(); AIFQ_SR_Review_Repository::uninstall(); AIFQ_SR_Research_Store::uninstall(); delete_transient('aifq_sr_large_images_count'); delete_transient('aifq_sr_autoloaded_options_size'); delete_transient('aifq_sr_post_revisions_count'); } public function enqueue_admin_assets($hook_suffix) { $allowed_hooks = ['toplevel_page_aifq-site-review']; if ($this->research_console_enabled()) { $allowed_hooks[] = 'aifq-site-review_page_aifq-site-review-research'; } if (!in_array($hook_suffix, $allowed_hooks, true)) { return; } // Include the asset file timestamp in the cache key. This keeps iterative // 1.10.x test builds from reusing an older cached admin.css/admin.js. $css_path = plugin_dir_path(__FILE__) . 'assets/admin.css'; $js_path = plugin_dir_path(__FILE__) . 'assets/admin.js'; $css_ver = self::ASSET_VERSION . '-' . (file_exists($css_path) ? (string) filemtime($css_path) : '0'); $js_ver = self::ASSET_VERSION . '-' . (file_exists($js_path) ? (string) filemtime($js_path) : '0'); wp_enqueue_style( 'aifq-sr-admin', plugin_dir_url(__FILE__) . 'assets/admin.css', [], $css_ver ); wp_enqueue_script( 'aifq-sr-admin', plugin_dir_url(__FILE__) . 'assets/admin.js', [], $js_ver, true ); wp_localize_script( 'aifq-sr-admin', 'aifqSrI18n', [ 'scanning' => esc_html__('Scanning...', 'aifq-site-review'), 'copied' => esc_html__('Copied', 'aifq-site-review'), 'moreDetails' => esc_html__('More details →', 'aifq-site-review'), 'lessDetails' => esc_html__('Less details ↑', 'aifq-site-review'), 'hideTechnical' => esc_html__('Hide technical details', 'aifq-site-review'), 'technicalDetails' => esc_html__('Technical details', 'aifq-site-review'), ] ); if ($this->research_console_enabled() && $this->research_store->is_active()) { wp_localize_script( 'aifq-sr-admin', 'aifqSrResearch', [ 'enabled' => true, 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aifq_sr_research_event'), ] ); } } public function register_admin_menu() { add_menu_page( esc_html__('AIFQ Site Review', 'aifq-site-review'), esc_html__('AIFQ Site Review', 'aifq-site-review'), 'manage_options', 'aifq-site-review', [$this, 'render_dashboard'], 'dashicons-shield-alt', 58 ); if ($this->research_console_enabled()) { add_submenu_page( 'aifq-site-review', esc_html__('AIFQ Research Console', 'aifq-site-review'), esc_html__('Research Console', 'aifq-site-review'), 'manage_options', 'aifq-site-review-research', [$this, 'render_research_console'] ); } } private function research_console_enabled(): bool { $enabled = str_contains(self::VERSION, '-dev.'); if (defined('AIFQ_SR_ENABLE_RESEARCH_CONSOLE')) { $enabled = (bool) AIFQ_SR_ENABLE_RESEARCH_CONSOLE; } return (bool) apply_filters('aifq_sr_enable_research_console', $enabled); } private function normalize_scan($scan) { return $this->review_normalizer->normalize_scan($scan); } private function normalize_issue($issue) { return $this->review_normalizer->normalize_issue($issue); } public function render_dashboard() { if (!current_user_can('manage_options')) { wp_die( esc_html__('You do not have permission to access this page.', 'aifq-site-review'), esc_html__('Access denied', 'aifq-site-review'), ['response' => 403] ); } $scan = $this->normalize_scan($this->review_repository->get_last_scan()); $previous_scan = $this->normalize_scan($this->review_repository->get_previous_scan()); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Admin-only view/notice parameters; no state change. $notice = isset($_GET['aifq_sr_notice']) ? sanitize_key(wp_unslash($_GET['aifq_sr_notice'])) : ''; $view = isset($_GET['aifq_sr_view']) ? sanitize_key(wp_unslash($_GET['aifq_sr_view'])) : 'review'; // phpcs:enable WordPress.Security.NonceVerification.Recommended ?>
render_v10_notice($notice); ?>

finalize_scan_items($issues, $suggestions, $passed); $issues = $final_items['issues']; $suggestions = $final_items['suggestions']; $passed = $final_items['passed']; $reviewed_map = $this->get_reviewed_findings(); $issue_split = $this->split_reviewed_issues($issues, $reviewed_map); $suggestion_split = $this->split_reviewed_issues($suggestions, $reviewed_map); $issues = $this->v10_sort_attention_items($issue_split['active']); $suggestions = $suggestion_split['active']; $reviewed_attention = array_map(static function ($item) { if (is_array($item)) { $item['_aifq_presentation'] = AIFQ_SR_Finding_Catalog::PRESENTATION_ATTENTION; } return $item; }, $issue_split['reviewed']); $reviewed_opportunities = array_map(static function ($item) { if (is_array($item)) { $item['_aifq_presentation'] = AIFQ_SR_Finding_Catalog::PRESENTATION_OPPORTUNITY; } return $item; }, $suggestion_split['reviewed']); $reviewed_items = array_merge($reviewed_attention, $reviewed_opportunities); $scan_diff = is_array($previous_scan) ? $this->calculate_scan_diff($scan, $previous_scan) : null; $first_actions = $this->get_first_actions($issues, 3); $first_suggestions = $this->get_first_suggestions($suggestions, 3); $copy_summary = $this->copy_builder->build_copy_summary($scan, $issues, $suggestions, $first_actions, $scan_diff, count($reviewed_items)); $developer_message = $this->copy_builder->build_developer_host_message($scan, $issues, $suggestions, $first_actions); $total_checks = count($issues) + count($reviewed_items) + count($suggestions) + count($passed); ?> render_v10_all_checks_view($issues, $reviewed_items, $suggestions, $passed); ?>

v10_status_headline($issues, count($suggestions), count($reviewed_items))); ?>

v10_status_copy(count($issues), count($suggestions), count($reviewed_items))); ?>

format_last_checked_label((int) $scan['created_at'])); ?>
render_v10_changes($scan_diff); ?>

$issue): ?> render_v10_finding($issue, $index === 0, $index >= 3); ?> 3): ?>
render_v10_worth_reviewing($suggestions); ?>

render_v10_sidebar($reviewed_items, $passed, $context_items, $copy_summary, $developer_message); ?>
['info', __('A review was just completed. Please wait a moment before running it again.', 'aifq-site-review')], 'scan_in_progress' => ['info', __('Another review is already running. Please wait for it to finish and try again.', 'aifq-site-review')], 'save_failed' => ['error', __('The review could not be saved. Please try again.', 'aifq-site-review')], 'reviewed' => ['success', __('Finding marked as reviewed for this site.', 'aifq-site-review')], 'unreviewed' => ['success', __('Finding moved back to active review.', 'aifq-site-review')], 'decision_saved' => ['success', __('Decision saved. AIFQ will keep this context with the finding.', 'aifq-site-review')], 'decision_required' => ['info', __('This finding uses Decision Memory. Record what you decided before marking it as reviewed.', 'aifq-site-review')], 'decision_note_required' => ['error', __('Add a short context note when you choose Other.', 'aifq-site-review')], 'decision_conflict' => ['error', __('This decision changed in another request. Review the current decision and try again.', 'aifq-site-review')], 'decision_unavailable' => ['error', __('Decision Memory is temporarily unavailable. The finding was not marked as reviewed.', 'aifq-site-review')], 'decision_save_failed' => ['error', __('The decision could not be saved. The finding was not marked as reviewed.', 'aifq-site-review')], ]; if (!isset($messages[$notice])) { return; } [$type, $message] = $messages[$notice]; printf('

%2$s

', esc_attr($type), esc_html($message)); } private function v10_status_headline(array $issues, $worth_count, $reviewed_count = 0) { $attention_count = count($issues); $worth_count = (int) $worth_count; $reviewed_count = (int) $reviewed_count; $high_count = 0; foreach ($issues as $issue) { if (is_array($issue) && (($issue['severity'] ?? '') === self::SEV_HIGH)) { $high_count++; } } if ($attention_count === 0 && $worth_count === 0 && $reviewed_count > 0) { return __('No open items need your attention.', 'aifq-site-review'); } if ($attention_count === 0 && $worth_count === 0) { return __('Everything we checked looks stable.', 'aifq-site-review'); } if ($attention_count === 0) { return __('Your site looks stable, with a few things worth reviewing.', 'aifq-site-review'); } if ($high_count === 1) { return __('One important item deserves a closer look.', 'aifq-site-review'); } if ($high_count > 1) { return __('A few important items deserve a closer look.', 'aifq-site-review'); } if ($attention_count <= 5) { return __('A few items deserve a closer look.', 'aifq-site-review'); } return __('Several items deserve a closer look.', 'aifq-site-review'); } private function v10_status_copy($attention_count, $worth_count, $reviewed_count) { $attention_count = (int) $attention_count; $worth_count = (int) $worth_count; $reviewed_count = (int) $reviewed_count; if ($attention_count === 0 && $worth_count === 0) { if ($reviewed_count > 0) { /* translators: %d: number of findings already marked as reviewed. */ return sprintf(_n('No open items need attention. %d finding is already marked as reviewed.', 'No open items need attention. %d findings are already marked as reviewed.', $reviewed_count, 'aifq-site-review'), $reviewed_count); } return __('No items currently need your attention or a separate review.', 'aifq-site-review'); } /* translators: %d: number of items that need attention. */ $attention_text = sprintf(_n('%d item needs your attention.', '%d items need your attention.', $attention_count, 'aifq-site-review'), $attention_count); if ($worth_count > 0) { /* translators: %d: number of additional items worth reviewing. */ $attention_text .= ' ' . sprintf(_n('%d more is worth reviewing when you have time.', '%d more are worth reviewing when you have time.', $worth_count, 'aifq-site-review'), $worth_count); } return $attention_text; } private function render_v10_changes($diff) { ?>
format_scan_date($diff['previous_date']) : ''; $events = []; foreach ($new_items as $item) { $events[] = [ 'type' => 'new', 'label' => __('New', 'aifq-site-review'), 'title' => $this->display_issue_title($item), 'copy' => $this->issue_preview($item), ]; } foreach ($resolved_items as $item) { $events[] = [ 'type' => 'resolved', 'label' => __('Resolved', 'aifq-site-review'), 'title' => $this->display_issue_title($item), 'copy' => __('This finding was present in the previous saved review and is not detected in the current one.', 'aifq-site-review'), ]; } ?>

$event): ?> render_v10_change_item($event['type'], $event['label'], $event['title'], $event['copy'], $index >= 3); ?> 3): ?>
>
decision_current_cache)) { try { $this->decision_current_cache[$key] = $this->decision_memory_service->get_current_decision($key); } catch (Throwable $e) { $this->decision_current_cache[$key] = null; } } return is_array($this->decision_current_cache[$key]) ? $this->decision_current_cache[$key] : null; } private function decision_type_label(string $type): string { $labels = [ 'keep_current_setup' => __('Keep current setup', 'aifq-site-review'), 'handled_elsewhere' => __('Handled elsewhere', 'aifq-site-review'), 'defer_for_now' => __('Defer for now', 'aifq-site-review'), 'no_longer_relevant' => __('No longer relevant', 'aifq-site-review'), 'other' => __('Other', 'aifq-site-review'), ]; return $labels[$type] ?? __('Other', 'aifq-site-review'); } private function decision_actor_label(int $user_id): string { if ($user_id > 0 && function_exists('get_userdata')) { $user = get_userdata($user_id); if ($user && isset($user->display_name) && trim((string) $user->display_name) !== '') { return (string) $user->display_name; } } return __('WordPress administrator', 'aifq-site-review'); } private function decision_meta_label(array $decision): string { $timestamp = (int) ($decision['decided_at'] ?? 0); $actor = $this->decision_actor_label((int) ($decision['decided_by'] ?? 0)); if ($timestamp <= 0) { return sprintf( /* translators: %s: WordPress user display name. */ __('by %s', 'aifq-site-review'), $actor ); } return sprintf( /* translators: 1: decision date, 2: WordPress user display name. */ __('%1$s by %2$s', 'aifq-site-review'), $this->format_scan_date($timestamp), $actor ); } private function render_v12_decision_context(array $decision, bool $previous = false): void { $label = $previous ? __('Previous decision', 'aifq-site-review') : __('Decision', 'aifq-site-review'); $note = trim((string) ($decision['decision_note'] ?? '')); $finding_key = sanitize_key((string) ($decision['finding_key'] ?? '')); ?>
decision_type_label((string) ($decision['decision_type'] ?? ''))); ?> decision_meta_label($decision)); ?>

decision_for_finding($key); $has_current = is_array($current); $picker_id = 'aifq-v12-decision-' . $key; $current_type = $has_current ? sanitize_key((string) ($current['decision_type'] ?? '')) : ''; $current_note = $has_current ? (string) ($current['decision_note'] ?? '') : ''; $labels = [ 'keep_current_setup' => __('Keep current setup', 'aifq-site-review'), 'handled_elsewhere' => __('Handled elsewhere', 'aifq-site-review'), 'defer_for_now' => __('Defer for now', 'aifq-site-review'), 'no_longer_relevant' => __('No longer relevant', 'aifq-site-review'), 'other' => __('Other', 'aifq-site-review'), ]; ?> render_v12_decision_context($current, true); ?>
$label): ?>
> display_issue_title($issue)); ?> issue_preview($issue)); ?>
decision_for_finding($key) === null): ?>

issue_simple_explanation($issue)); ?>

display_issue_recommendation($issue)); ?>

issue_if_ignored($issue); ?>

issue_handler_label($issue)); ?>
render_v12_decision_picker($issue, AIFQ_SR_Finding_Catalog::PRESENTATION_ATTENTION); ?>

$suggestion): ?>
= 3 ? ' hidden' : ''; ?>> display_issue_title($suggestion)); ?>issue_preview($suggestion)); ?>

display_issue_recommendation($suggestion)); ?>

issue_handler_label($suggestion)); ?>

render_v12_decision_picker($suggestion, AIFQ_SR_Finding_Catalog::PRESENTATION_OPPORTUNITY); ?>
3): ?>
v10_check_entry($item, 'attention'); } foreach ($reviewed_issues as $item) { $entries[] = $this->v10_check_entry($item, 'reviewed'); } foreach ($suggestions as $item) { $entries[] = $this->v10_check_entry($item, 'review'); } foreach ($passed as $item) { $entries[] = $this->v10_check_entry($item, 'good'); } $groups = ['Setup' => [], 'Security' => [], 'Reliability' => [], 'Performance' => []]; foreach ($entries as $entry) { $category = isset($groups[$entry['category']]) ? $entry['category'] : 'Setup'; $groups[$category][] = $entry; } ?>
←

$items): ?>

passed_title_to_key((string) ($item['title'] ?? '')); $internal = (string) ($item['category'] ?? ''); $category_id = AIFQ_SR_Finding_Catalog::normalize_category_id($item['category_id'] ?? ''); if ($category_id === '') { $category_id = AIFQ_SR_Finding_Catalog::category_id_for_key($key, $internal); } $title = $state === 'good' ? $this->display_passed_title($item) : $this->display_issue_title($item); $description = $state === 'good' ? $this->display_passed_description($item) : $this->issue_preview($item); $labels = [ 'attention' => __('Needs attention', 'aifq-site-review'), 'review' => __('Worth reviewing', 'aifq-site-review'), 'reviewed' => __('Reviewed', 'aifq-site-review'), 'good' => __('Looks good', 'aifq-site-review'), ]; $icons = ['attention' => '!', 'review' => '·', 'reviewed' => '✓', 'good' => '✓']; return [ 'state' => $state, 'label' => $labels[$state] ?? '', 'icon' => $icons[$state] ?? '·', 'title' => $title, 'description' => $description, 'category' => AIFQ_SR_Finding_Catalog::category_label($category_id), ]; } private function persistent_object_cache_status_label(array $cache): string { $provider_failed = !empty($cache['object_cache_provider_health_supported']) && (($cache['object_cache_provider_health_failed'] ?? null) === true); if ($provider_failed) { return __('Issue detected', 'aifq-site-review'); } return !empty($cache['persistent_object_cache_runtime']) ? __('Active', 'aifq-site-review') : __('Not detected', 'aifq-site-review'); } private function render_v10_sidebar(array $reviewed_items, array $passed, array $context_items, $copy_summary, $developer_message) { $environment = function_exists('wp_get_environment_type') ? wp_get_environment_type() : 'production'; $cache_signals = $this->site_collector->collect_cache_signals(); $object_cache = $this->persistent_object_cache_status_label($cache_signals); $good_preview = $this->get_good_points($passed, 5); ?> 120, 'woo_checkout_page_missing' => 118, 'woo_checkout_page_not_public' => 117, 'development_settings_active' => 116, 'wp_debug_display' => 114, 'savequeries_enabled' => 113, 'debug_on' => 112, 'object_cache_runtime_failure' => 111, 'action_scheduler_backlog' => 110, 'woo_failed_scheduled_actions' => 110, 'woo_background_tasks' => 109, 'woo_scheduled_actions_backlog' => 108, 'core_update' => 106, 'old_wp_version' => 104, 'core_database_update_required' => 103, 'file_editor' => 102, 'woo_database_update_required' => 101, 'upload_not_writable' => 100, 'old_php_updates' => 98, 'default_admin_username' => 96, 'open_registration_privileged_role' => 94, 'pending_admin_email_change' => 92, 'wp_cron_disabled' => 90, 'debug_log_found' => 88, 'large_autoloaded_options' => 86, 'file_mods_disabled' => 84, 'plugin_updates' => 82, 'theme_update' => 80, 'woo_checkout_content_missing' => 78, 'memory_plugin_conflict' => 76, 'multi_security' => 74, // WordPress may sit behind a trusted HTTPS-terminating proxy, so // this signal remains intentionally below clearer local findings. 'no_ssl' => 72, 'woo_multi_cache' => 70, 'multi_cache' => 68, 'woo_no_cache' => 66, 'old_php' => 64, 'low_memory' => 62, 'large_images' => 58, 'heavy_plugins' => 56, 'woo_many_plugins' => 55, 'many_plugins' => 54, 'elementor_addons_many' => 53, 'elementor_woo_complexity' => 52, 'multiple_page_builders' => 50, 'complex_plugin_setup' => 48, 'elementor_addons_several' => 47, 'multi_seo' => 46, 'site_url_home_mismatch' => 44, 'many_revisions' => 42, 'xmlrpc' => 34, 'inactive_plugins' => 30, 'inactive_themes' => 28, ]; } private function v10_sort_attention_items(array $issues) { $priority = $this->v10_attention_priority_map(); usort($issues, function($a, $b) use ($priority) { $a_key = (string) ($a['key'] ?? 'generic'); $b_key = (string) ($b['key'] ?? 'generic'); $a_priority = $priority[$a_key] ?? 40; $b_priority = $priority[$b_key] ?? 40; if ($a_priority !== $b_priority) return $b_priority <=> $a_priority; $a_conf = ($a['confidence'] ?? '') === 'high' ? 2 : 1; $b_conf = ($b['confidence'] ?? '') === 'high' ? 2 : 1; if ($a_conf !== $b_conf) return $b_conf <=> $a_conf; return $this->severity_sort_weight($b['severity'] ?? self::SEV_LOW) <=> $this->severity_sort_weight($a['severity'] ?? self::SEV_LOW); }); return $issues; } private function get_last_scan_timestamp() { return $this->review_repository->get_last_scan_timestamp(); } private function set_last_scan_timestamp() { $this->review_repository->set_last_scan_timestamp(); } private function clear_large_images_cache() { delete_transient('aifq_sr_large_images_count'); delete_transient('aifq_sr_autoloaded_options_size'); delete_transient('aifq_sr_post_revisions_count'); } private function sanitize_text_list($items) { if (!is_array($items)) { return []; } return array_values(array_filter(array_map('sanitize_text_field', $items))); } private function get_reviewed_findings() { return $this->review_repository->get_reviewed_findings(); } private function save_reviewed_findings(array $reviewed) { $this->review_repository->save_reviewed_findings($reviewed); } private function current_visible_issue_map() { $scan = $this->normalize_scan($this->review_repository->get_last_scan()); if (!is_array($scan)) { return []; } $issues = isset($scan['issues']) && is_array($scan['issues']) ? $scan['issues'] : []; $suggestions = isset($scan['suggestions']) && is_array($scan['suggestions']) ? $scan['suggestions'] : []; $passed = isset($scan['passed']) && is_array($scan['passed']) ? $scan['passed'] : []; $final_items = $this->finalize_scan_items($issues, $suggestions, $passed); $map = []; foreach ($final_items['issues'] as $issue) { if (!is_array($issue)) { continue; } $key = sanitize_key((string) ($issue['key'] ?? '')); if ($key !== '') { $map[$key] = $issue; } } return $map; } private function current_visible_actionable_map(): array { $scan = $this->normalize_scan($this->review_repository->get_last_scan()); if (!is_array($scan)) { return []; } $issues = isset($scan['issues']) && is_array($scan['issues']) ? $scan['issues'] : []; $suggestions = isset($scan['suggestions']) && is_array($scan['suggestions']) ? $scan['suggestions'] : []; $passed = isset($scan['passed']) && is_array($scan['passed']) ? $scan['passed'] : []; $final_items = $this->finalize_scan_items($issues, $suggestions, $passed); $map = []; foreach ($final_items['issues'] as $issue) { if (!is_array($issue)) { continue; } $key = sanitize_key((string) ($issue['key'] ?? '')); if ($key !== '') { $issue['_aifq_presentation'] = AIFQ_SR_Finding_Catalog::PRESENTATION_ATTENTION; $map[$key] = $issue; } } foreach ($final_items['suggestions'] as $suggestion) { if (!is_array($suggestion)) { continue; } $key = sanitize_key((string) ($suggestion['key'] ?? '')); if ($key !== '') { $suggestion['_aifq_presentation'] = AIFQ_SR_Finding_Catalog::PRESENTATION_OPPORTUNITY; $map[$key] = $suggestion; } } return $map; } private function mark_current_finding_reviewed(string $key, array $finding, ?int $reviewed_at = null, bool $decision_managed = false): void { $key = sanitize_key($key); if ($key === '') { return; } $severity = sanitize_key((string) ($finding['severity'] ?? self::SEV_LOW)); if (!in_array($severity, [self::SEV_HIGH, self::SEV_MEDIUM, self::SEV_LOW], true)) { $severity = self::SEV_LOW; } $reviewed = $this->get_reviewed_findings(); $reviewed[$key] = [ 'reviewed_at' => $reviewed_at && $reviewed_at > 0 ? $reviewed_at : time(), 'severity_baseline' => $severity, ]; if ($decision_managed) { $reviewed[$key]['decision_managed'] = true; } $this->save_reviewed_findings($reviewed); } /** * Reconcile legacy reviewed state when a new review is created. * * This keeps persistence out of the rendering path: severity escalation is * handled as part of scan/application flow and persisted with the scan. */ private function reconcile_reviewed_findings_for_scan(array $issues) { return $this->reviewed_findings_service->reconcile_for_scan($issues); } private function split_reviewed_issues(array $issues, array $reviewed_map) { return $this->reviewed_findings_service->split($issues, $reviewed_map); } private function format_last_checked_label($timestamp) { $timestamp = (int) $timestamp; if ($timestamp <= 0) { return ''; } $days = (int) floor(max(0, time() - $timestamp) / DAY_IN_SECONDS); $time_label = date_i18n(get_option('time_format'), $timestamp); if ($days <= 0) { return sprintf( /* translators: %s: formatted scan time. */ __('today at %s', 'aifq-site-review'), $time_label ); } $date_label = $this->format_scan_date($timestamp); return sprintf( /* translators: 1: formatted scan date, 2: number of days since last scan. */ _n('%1$s (%2$d day ago)', '%1$s (%2$d days ago)', $days, 'aifq-site-review'), $date_label, $days ); } public function handle_mark_reviewed() { if (!current_user_can('manage_options')) { wp_die( esc_html__('Insufficient permissions.', 'aifq-site-review'), esc_html__('Access denied', 'aifq-site-review'), ['response' => 403] ); } check_admin_referer('aifq_sr_mark_reviewed'); $key = isset($_POST['issue_key']) ? sanitize_key(wp_unslash($_POST['issue_key'])) : ''; $current_issues = $this->current_visible_issue_map(); if ($key !== '' && isset($current_issues[$key])) { if (AIFQ_SR_Finding_Catalog::is_decision_eligible($key)) { wp_safe_redirect(add_query_arg('aifq_sr_notice', 'decision_required', admin_url('admin.php?page=aifq-site-review'))); exit; } $this->mark_current_finding_reviewed($key, $current_issues[$key]); wp_safe_redirect(add_query_arg('aifq_sr_notice', 'reviewed', admin_url('admin.php?page=aifq-site-review'))); exit; } wp_safe_redirect(admin_url('admin.php?page=aifq-site-review')); exit; } public function handle_save_decision() { if (!current_user_can('manage_options')) { wp_die( esc_html__('Insufficient permissions.', 'aifq-site-review'), esc_html__('Access denied', 'aifq-site-review'), ['response' => 403] ); } check_admin_referer('aifq_sr_save_decision'); $key = isset($_POST['finding_key']) ? sanitize_key(wp_unslash($_POST['finding_key'])) : ''; $decision_type = isset($_POST['decision_type']) ? sanitize_key(wp_unslash($_POST['decision_type'])) : ''; $decision_note = isset($_POST['decision_note']) ? sanitize_textarea_field(wp_unslash($_POST['decision_note'])) : ''; $expected_id = isset($_POST['expected_decision_id']) ? sanitize_text_field(wp_unslash($_POST['expected_decision_id'])) : ''; $current_items = $this->current_visible_actionable_map(); if ($key === '' || !isset($current_items[$key]) || !AIFQ_SR_Finding_Catalog::is_decision_eligible($key)) { wp_safe_redirect(add_query_arg('aifq_sr_notice', 'decision_save_failed', admin_url('admin.php?page=aifq-site-review'))); exit; } $finding = $current_items[$key]; $severity = sanitize_key((string) ($finding['severity'] ?? self::SEV_LOW)); if (!in_array($severity, [self::SEV_HIGH, self::SEV_MEDIUM, self::SEV_LOW], true)) { $severity = self::SEV_LOW; } $user_id = get_current_user_id(); if ($expected_id !== '') { $result = $this->decision_memory_service->supersede_current_decision( $key, $expected_id, $decision_type, $decision_note, $severity, $user_id ); } else { $result = $this->decision_memory_service->create_first_decision( $key, $decision_type, $decision_note, $severity, $user_id ); } $status = (string) ($result['status'] ?? ''); $reason = (string) ($result['reason'] ?? ''); if ($status === AIFQ_SR_Decision_Memory_Service::STATUS_CREATED && is_array($result['record'] ?? null)) { $record = $result['record']; $this->mark_current_finding_reviewed($key, $finding, (int) ($record['decided_at'] ?? time()), true); $this->research_store->record('decision_saved', [ 'finding_key' => $key, 'presentation' => sanitize_key((string) ($finding['_aifq_presentation'] ?? '')), 'decision_type' => $decision_type, 'note_used' => trim($decision_note) !== '', 'superseding' => $expected_id !== '', ]); wp_safe_redirect(add_query_arg('aifq_sr_notice', 'decision_saved', admin_url('admin.php?page=aifq-site-review'))); exit; } if ($status === AIFQ_SR_Decision_Memory_Service::STATUS_INVALID && $reason === 'other_requires_note') { $notice = 'decision_note_required'; } elseif ($status === AIFQ_SR_Decision_Memory_Service::STATUS_CONFLICT) { $notice = 'decision_conflict'; } elseif ($status === AIFQ_SR_Decision_Memory_Service::STATUS_UNAVAILABLE) { $notice = 'decision_unavailable'; } else { $notice = 'decision_save_failed'; } wp_safe_redirect(add_query_arg('aifq_sr_notice', $notice, admin_url('admin.php?page=aifq-site-review'))); exit; } public function handle_unmark_reviewed() { if (!current_user_can('manage_options')) { wp_die( esc_html__('Insufficient permissions.', 'aifq-site-review'), esc_html__('Access denied', 'aifq-site-review'), ['response' => 403] ); } check_admin_referer('aifq_sr_unmark_reviewed'); $key = isset($_POST['issue_key']) ? sanitize_key(wp_unslash($_POST['issue_key'])) : ''; if ($key !== '') { $reviewed = $this->get_reviewed_findings(); if (isset($reviewed[$key])) { unset($reviewed[$key]); $this->save_reviewed_findings($reviewed); $this->research_store->record('manual_reactivation', ['finding_key' => $key]); } } wp_safe_redirect(add_query_arg('aifq_sr_notice', 'unreviewed', admin_url('admin.php?page=aifq-site-review'))); exit; } public function handle_scan() { if (!current_user_can('manage_options')) { wp_die( esc_html__('Insufficient permissions.', 'aifq-site-review'), esc_html__('Access denied', 'aifq-site-review'), ['response' => 403] ); } check_admin_referer('aifq_sr_run_scan'); $last_scan_timestamp = $this->get_last_scan_timestamp(); if ($last_scan_timestamp && (time() - $last_scan_timestamp) < self::SCAN_COOLDOWN) { wp_safe_redirect(add_query_arg('aifq_sr_notice', 'cooldown', admin_url('admin.php?page=aifq-site-review'))); exit; } $scan_lock = new AIFQ_SR_Review_Lock(); if (!$scan_lock->acquire()) { wp_safe_redirect(add_query_arg('aifq_sr_notice', 'scan_in_progress', admin_url('admin.php?page=aifq-site-review'))); exit; } // Memory is optional: initialize its trustworthy history boundary when // possible, but never block the established Site Review flow. $this->ensure_memory_foundation(true); $this->review_repository->rotate_last_scan_to_previous(); $this->clear_large_images_cache(); $data = $this->site_collector->collect(); $evaluation = $this->evaluation_pipeline->evaluate($data); $issues = $this->reconcile_reviewed_findings_for_scan($evaluation['issues']); $suggestions = $this->reconcile_reviewed_findings_for_scan($evaluation['suggestions']); $passed = $evaluation['passed']; $context_items = $evaluation['context_items']; $signals = $this->build_profile($data); $scan = [ 'version' => self::VERSION, 'issues' => $issues, 'suggestions' => $suggestions, 'passed' => $passed, 'context_items'=> $context_items, 'signals' => $signals, 'evaluation_schema_version' => 1, 'evaluation_results' => $evaluation['evaluation_results'], 'created_at' => time(), ]; if (!$this->review_repository->save_last_scan($scan)) { $scan_lock->release(); wp_safe_redirect(add_query_arg('aifq_sr_notice', 'save_failed', admin_url('admin.php?page=aifq-site-review'))); exit; } // Gate B research instrumentation is local-only and optional. The // review itself is already safely stored before this path runs. $opportunity_keys = []; foreach (array_merge($issues, $suggestions) as $finding) { if (!is_array($finding)) { continue; } $finding_key = sanitize_key((string) ($finding['key'] ?? '')); if ($finding_key !== '' && AIFQ_SR_Finding_Catalog::is_decision_eligible($finding_key)) { $opportunity_keys[] = $finding_key; } } $this->research_store->record_review_completed($opportunity_keys, count($issues), count($suggestions)); // Only after the established review is safely stored do we advance the // optional Memory anchor. Any Memory failure degrades to less history; // it never changes whether the Site Review itself succeeds. $memory_ok = $this->memory_processor->process_successful_review(); if ($memory_ok) { AIFQ_SR_Research_Store::clear_memory_error(); } else { AIFQ_SR_Research_Store::record_memory_error('review_memory_process_degraded'); } $this->set_last_scan_timestamp(); $scan_lock->release(); wp_safe_redirect(admin_url('admin.php?page=aifq-site-review')); exit; } public function handle_research_start() { if (!current_user_can('manage_options') || !$this->research_console_enabled()) { wp_die( esc_html__('Insufficient permissions.', 'aifq-site-review'), esc_html__('Access denied', 'aifq-site-review'), ['response' => 403] ); } check_admin_referer('aifq_sr_research_start'); $this->research_store->start_session(get_current_user_id()); wp_safe_redirect(add_query_arg('aifq_sr_research_notice', 'started', admin_url('admin.php?page=aifq-site-review-research'))); exit; } public function handle_research_reminder() { if (!current_user_can('manage_options') || !$this->research_console_enabled()) { wp_die( esc_html__('Insufficient permissions.', 'aifq-site-review'), esc_html__('Access denied', 'aifq-site-review'), ['response' => 403] ); } check_admin_referer('aifq_sr_research_reminder'); $session = $this->research_store->get_session(); $review_count = max(0, (int) ($session['review_count'] ?? 0)); $reminder_sent_at = max(0, (int) ($session['reminder_sent_at'] ?? 0)); if (empty($session['active'])) { $notice = 'session_required'; } elseif ($review_count === 0) { $notice = 'baseline_required'; } elseif ($review_count >= 2) { $notice = 'reminder_not_applicable'; } elseif ($reminder_sent_at > 0) { $notice = 'reminder_already_marked'; } else { $ok = $this->research_store->mark_reminder_sent(get_current_user_id()); $notice = $ok ? 'reminder_marked' : 'reminder_not_applicable'; } wp_safe_redirect(add_query_arg('aifq_sr_research_notice', $notice, admin_url('admin.php?page=aifq-site-review-research'))); exit; } public function handle_research_event() { if (!current_user_can('manage_options') || !$this->research_console_enabled()) { wp_send_json_error(['reason' => 'forbidden'], 403); } check_ajax_referer('aifq_sr_research_event', 'nonce'); $event_type = isset($_POST['event_type']) ? sanitize_key(wp_unslash($_POST['event_type'])) : ''; $allowed = [ 'decision_picker_opened', 'decision_picker_cancelled', 'previous_decision_shown', 'reviewed_decision_shown', 'copy_summary', 'copy_handoff', ]; if (!in_array($event_type, $allowed, true)) { wp_send_json_error(['reason' => 'invalid_event'], 400); } $context = [ 'finding_key' => isset($_POST['finding_key']) ? sanitize_key(wp_unslash($_POST['finding_key'])) : '', 'presentation' => isset($_POST['presentation']) ? sanitize_key(wp_unslash($_POST['presentation'])) : '', 'has_current' => !empty($_POST['has_current']), ]; $stored = $this->research_store->record($event_type, $context); wp_send_json_success(['stored' => $stored]); } public function render_research_console() { if (!current_user_can('manage_options') || !$this->research_console_enabled()) { wp_die( esc_html__('You do not have permission to access this page.', 'aifq-site-review'), esc_html__('Access denied', 'aifq-site-review'), ['response' => 403] ); } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only notice parameter on an admin-only internal page. $notice = isset($_GET['aifq_sr_research_notice']) ? sanitize_key(wp_unslash($_GET['aifq_sr_research_notice'])) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended $session = $this->research_store->get_session(); $summary = $this->research_store->summary(); $diagnostics = AIFQ_SR_Research_Diagnostics::snapshot($this->site_collector); $events = array_reverse(array_slice($this->research_store->get_events(), -25)); $registry = is_array($diagnostics['registry'] ?? null) ? $diagnostics['registry'] : []; $capability_registry = is_array($diagnostics['capability_registry'] ?? null) ? $diagnostics['capability_registry'] : []; $evaluation_results = is_array($diagnostics['evaluation_results'] ?? null) ? $diagnostics['evaluation_results'] : []; $evidence = is_array($diagnostics['evidence'] ?? null) ? $diagnostics['evidence'] : []; $decisions = is_array($diagnostics['decisions'] ?? null) ? $diagnostics['decisions'] : []; $cache = is_array($diagnostics['cache'] ?? null) ? $diagnostics['cache'] : []; $last_error = is_array($diagnostics['last_memory_error'] ?? null) ? $diagnostics['last_memory_error'] : []; ?>

format_scan_date((int) ($session['started_at'] ?? 0)), (int) ($session['review_count'] ?? 0))); ?>

0; ?>
= 2): ?>

render_research_metric(__('Reviews completed', 'aifq-site-review'), (string) $summary['reviews_completed']); ?> render_research_metric(__('Decision opportunities', 'aifq-site-review'), (string) $summary['decision_opportunities']); ?> render_research_metric(__('Picker opened', 'aifq-site-review'), (string) $summary['picker_opened']); ?> render_research_metric(__('Decisions saved', 'aifq-site-review'), (string) $summary['decisions_saved']); ?> render_research_metric(__('Picker completion', 'aifq-site-review'), $summary['picker_completion_percent'] === null ? '—' : $summary['picker_completion_percent'] . '%'); ?> render_research_metric(__('Picker cancelled', 'aifq-site-review'), (string) $summary['picker_cancelled']); ?> render_research_metric(__('Notes used', 'aifq-site-review'), (string) $summary['notes_used']); ?> render_research_metric(__('Superseding decisions', 'aifq-site-review'), (string) $summary['superseding_decisions']); ?> render_research_metric(__('Manual reactivations', 'aifq-site-review'), (string) $summary['manual_reactivations']); ?> render_research_metric(__('Previous decision shown', 'aifq-site-review'), (string) $summary['previous_decision_shown']); ?> render_research_metric(__('Reviewed decision shown', 'aifq-site-review'), (string) $summary['reviewed_decision_shown']); ?> render_research_metric(__('Copy summary', 'aifq-site-review'), (string) $summary['copy_summary']); ?> render_research_metric(__('Handoff copy', 'aifq-site-review'), (string) $summary['copy_handoff']); ?> render_research_metric(__('Organic returns', 'aifq-site-review'), (string) $summary['organic_returns']); ?> render_research_metric(__('Prompted returns', 'aifq-site-review'), (string) $summary['prompted_returns']); ?>

format_scan_date((int) $session['reminder_sent_at'])) : '—'; ?>

$count): ?>
decision_type_label((string) $type)); ?>

  • · / ·

persistent_object_cache_status_label($cache)); ?>
· format_scan_date((int) ($last_error['recorded_at'] ?? 0))); ?> ·

$stats): ?>

format_scan_date((int) ($event['event_at'] ?? 0))); ?>
isset($result['issues']) && is_array($result['issues']) ? $result['issues'] : [], 'suggestions' => isset($result['suggestions']) && is_array($result['suggestions']) ? $result['suggestions'] : [], 'passed' => isset($result['passed']) && is_array($result['passed']) ? $result['passed'] : [], 'context_items' => isset($result['context_items']) && is_array($result['context_items']) ? $result['context_items'] : [], ]; /** * Filter AIFQ's local result set after built-in checks run. * * This is intended for trusted local integrations such as hosting or * agency tooling that need to add context or suppress known platform * false positives without editing AIFQ core files. * * @param array $defaults Result sections: issues, suggestions, passed, context_items. * @param array $data The locally collected site data used for this review. */ $filtered = apply_filters('aifq_sr_findings', $defaults, $data); if (!is_array($filtered)) { $filtered = $defaults; } foreach (array_keys($defaults) as $section) { if (!isset($filtered[$section]) || !is_array($filtered[$section])) { $filtered[$section] = $defaults[$section]; } } $issues = []; foreach ($filtered['issues'] as $item) { $clean = $this->normalize_issue($item); if ($clean) { $clean['type'] = 'attention'; $issues[] = $clean; } } $suggestions = []; foreach ($filtered['suggestions'] as $item) { $clean = $this->normalize_issue($item); if ($clean) { $clean['type'] = 'opportunity'; $suggestions[] = $clean; } } $passed = []; foreach ($filtered['passed'] as $item) { $clean = $this->review_normalizer->normalize_passed($item); if ($clean) { $passed[] = $clean; } } $context_items = []; foreach ($filtered['context_items'] as $item) { if (!is_array($item)) { continue; } $context_items[] = [ 'key' => sanitize_key((string) ($item['key'] ?? '')), 'category' => sanitize_text_field((string) ($item['category'] ?? 'Technical context')), 'title' => sanitize_text_field((string) ($item['title'] ?? 'Technical context')), 'description' => sanitize_text_field((string) ($item['description'] ?? '')), 'details' => sanitize_text_field((string) ($item['details'] ?? '')), ]; } return [ 'issues' => array_values($issues), 'suggestions' => array_values($suggestions), 'passed' => array_values($passed), 'context_items' => array_values($context_items), ]; } // ------------------------------------------------------------------------- // Checks // ------------------------------------------------------------------------- private function run_local_checks(array $data) { $issues = []; $suggestions = []; $passed = []; $context_items = []; $memory_mb = $this->memory_to_mb($data['memory_limit']); $cache_count = count($data['cache_plugins']); $seo_count = count($data['seo_plugins']); $security_count = count($data['security_plugins']); $heavy_count = count($data['heavy_plugins']); $elementor_count = count($data['elementor_plugins']); $backup_count = count($data['backup_plugins']); $mail_delivery_count = isset($data['mail_delivery_plugins']) ? count($data['mail_delivery_plugins']) : 0; $page_builder_count = isset($data['page_builder_plugins']) ? count($data['page_builder_plugins']) : 0; $updates_total = $data['plugin_updates_count'] + $data['theme_updates_count'] + $data['core_updates_count']; $environment_type = isset($data['environment_type']) ? (string) $data['environment_type'] : 'production'; $is_production_environment = ($environment_type === 'production'); $is_heavier_setup = !empty($data['woocommerce_active']) || !empty($data['elementor_active']) || ((int) $data['active_plugin_count'] >= 25) || ((int) $data['large_images_count'] >= 10); // ----------------------------------------------------------------- // Performance // ----------------------------------------------------------------- if ($cache_count >= 2) { $issues[] = $this->issue('multi_cache', 'Performance', 'Multiple caching plugins active', self::SEV_HIGH, 'Multiple caching or optimization plugins are active. They can work against each other and cause slower pages, broken layout or cache issues.', 'Use one caching solution and deactivate duplicate optimization plugins.', 'Active caching plugins found: ' . implode(', ', $this->plugin_names($data['cache_plugins'])) ); } elseif ($cache_count === 1) { $passed[] = $this->passed('active_cache_plugin', 'Performance', 'Active caching plugin found', 'One active caching plugin was found: ' . implode(', ', $this->plugin_names($data['cache_plugins'])) . '.'); } elseif (!$data['cache_detected'] && $is_heavier_setup && empty($data['woocommerce_active'])) { $suggestions[] = $this->suggestion('no_cache_layer', 'Performance', 'No local page-cache signal detected on this heavier setup', 'AIFQ did not find a local WordPress page-cache signal on this heavier setup. Server- or edge-level page caching can exist without being visible from WordPress.', 'Verify whether page caching is already provided by your hosting platform. If it is, no extra WordPress page-cache plugin may be needed.', 'Local page-cache signals checked: known cache/performance plugins, WP_CACHE and advanced-cache.php. Active plugins: ' . $data['active_plugin_count'] . '. Large images: ' . $data['large_images_count'] . '.' ); } elseif (!$data['cache_detected']) { $suggestions[] = $this->suggestion('no_cache', 'Performance', 'No local page-cache signal detected', 'AIFQ did not find a local WordPress page-cache signal. Server- or edge-level page caching can exist without being visible from WordPress.', 'Verify whether page caching is already handled by your hosting platform. A separate WordPress page-cache plugin is not automatically required.', 'Local page-cache signals checked: known cache/performance plugins, WP_CACHE and advanced-cache.php. Light sites do not always need a separate page-cache layer.' ); } else { $context_parts = []; if (!empty($data['cache_constant_enabled'])) { $context_parts[] = 'WP_CACHE is enabled'; } if (!empty($data['cache_dropin_found'])) { $context_parts[] = 'advanced-cache.php drop-in found'; } $suggestions[] = $this->suggestion('cache_signal_no_plugin', 'Performance', 'Page-cache signal without active cache plugin', 'A local WordPress page-cache signal exists, but no active known cache/performance plugin was detected.', 'Check whether page caching is managed by your hosting platform, WP_CACHE or an advanced-cache.php drop-in left by an earlier cache setup.', !empty($context_parts) ? implode('. ', $context_parts) . '.' : 'No active known cache/performance plugin was found.' ); } if ($seo_count >= 2) { $issues[] = $this->issue('multi_seo', 'Performance', 'Multiple SEO plugins active', self::SEV_MEDIUM, 'Multiple SEO plugins are active. This can create duplicate metadata, conflicting sitemaps and unclear SEO settings.', 'Choose one SEO plugin as the main solution and remove or deactivate the rest.', 'Found: ' . implode(', ', $this->plugin_names($data['seo_plugins'])) ); } elseif ($seo_count === 1) { $passed[] = $this->passed('one_seo_plugin', 'Performance', 'One SEO plugin active', 'One SEO plugin was found.'); } else { $context_items[] = $this->context_item('seo_tooling_not_detected', 'Setup', 'No known SEO plugin detected', 'AIFQ did not find a known SEO plugin. This does not establish that SEO work or tooling is absent.', 'Search visibility can also be managed manually or through other services. No action is required solely because a plugin was not detected.' ); } if (empty($data['search_engine_visible'])) { if ($is_production_environment) { $issues[] = $this->issue('search_visibility_disabled', 'Maintenance', 'Search engine visibility is disabled', self::SEV_HIGH, 'WordPress is currently asking search engines not to index this site.', 'For a live public website, turn search engine visibility back on in Settings → Reading. While this is off, search engines are asked not to index the site.', 'WordPress option blog_public is set to 0.' ); } else { $suggestions[] = $this->suggestion('search_visibility_disabled', 'Maintenance', 'Search engine visibility is disabled', 'WordPress is currently asking search engines not to index this site. On staging, development or local sites, this is often intentional.', 'If this site is public, turn search engine visibility back on in Settings → Reading. If this is a staging or development site, this setting may be correct.', 'Environment type: ' . $environment_type . '. WordPress option blog_public is set to 0.' ); } } else { $passed[] = $this->passed('search_visibility_enabled', 'Maintenance', 'Search engine visibility is enabled', 'WordPress is not discouraging search engines from indexing the site.'); } if (empty($data['permalink_structure'])) { $suggestions[] = $this->suggestion('plain_permalinks', 'Performance', 'Plain permalink structure detected', 'The site appears to use the plain/default permalink structure.', 'If this is a new site, consider using a readable permalink structure such as Post name. If this is an existing live site, changing permalinks can break existing links unless redirects are handled correctly. Ask your developer first if the site already has published content.', 'This is mostly a usability and SEO improvement opportunity.' ); } else { $passed[] = $this->passed('readable_permalink_structure_active', 'Performance', 'Readable permalink structure active', 'A custom permalink structure is configured.'); } if ($heavy_count >= 3) { $issues[] = $this->issue('heavy_plugins', 'Performance', 'Several heavier plugins active', self::SEV_MEDIUM, 'Several active plugins are commonly used on larger or more dynamic sites. They are not automatically a problem, but together they can make caching, performance and troubleshooting more important.', 'Review whether each heavier plugin still has a clear purpose and avoid overlapping plugins that solve the same problem.', 'Found: ' . implode(', ', $this->plugin_names($data['heavy_plugins'])) ); } elseif ($heavy_count > 0) { $passed[] = $this->passed('heavy_plugins_limited', 'Performance', 'Heavy plugins limited', 'Some resource-heavy plugins are present, but not enough to flag as a setup-complexity signal.'); } else { $passed[] = $this->passed('no_known_heavy_plugins_found', 'Performance', 'No known heavy plugins found', 'No plugins from the resource-heavy plugin list were found.'); } if ($data['woocommerce_active'] && !$data['cache_detected']) { $suggestions[] = $this->suggestion('woo_no_cache', 'Performance', 'No local page-cache signal detected for WooCommerce', 'WooCommerce is active and AIFQ did not find a local WordPress page-cache signal. A hosting platform may still provide server- or edge-level page caching that WordPress cannot confirm here.', 'Verify the current page-cache setup with your hosting provider before adding anything. If page caching is already handled at server level, no extra WordPress page-cache plugin may be needed.', 'Local page-cache signals checked: known cache/performance plugins, WP_CACHE and advanced-cache.php. WooCommerce cart, checkout and account pages require careful cache rules.' ); } if (empty($data['object_cache_dropin_found']) && ($data['woocommerce_active'] || (int) $data['active_plugin_count'] >= 25)) { $suggestions[] = $this->suggestion('no_object_cache_dropin', 'Performance', 'No object cache drop-in detected', 'No persistent object cache was found. Object caching is separate from normal page caching plugins and can help some larger or dynamic WordPress sites, especially WooCommerce sites. It is not always needed.', 'Ask your hosting provider whether object caching is available and useful for this site.', 'object-cache.php was not found in wp-content.' ); } elseif (!empty($data['object_cache_dropin_found']) && !empty($data['object_cache_runtime_active'])) { $passed[] = $this->passed('object_cache_found', 'Performance', 'Object cache drop-in found', 'Persistent object caching is active. This is separate from normal page caching.'); } if ($data['woocommerce_active'] && (int) $data['active_plugin_count'] >= 25) { $issues[] = $this->issue('woo_many_plugins', 'Maintenance', 'Many plugins active on this WooCommerce site', self::SEV_MEDIUM, 'WooCommerce sites often need more plugins than simpler sites. This is not automatically a problem, but checkout, payments and background tasks can depend on several plugins working correctly together.', 'Review the WooCommerce plugins in a controlled way. Check whether any plugins overlap, are no longer needed, or should be tested in staging before changes are made.', 'WooCommerce active. Active plugins: ' . $data['active_plugin_count'] . '.' ); } if ($data['woocommerce_active'] && $mail_delivery_count === 0) { $suggestions[] = $this->suggestion('woo_no_mail_delivery_plugin', 'Maintenance', 'No known email delivery plugin found for WooCommerce', 'WooCommerce sends important customer emails, and AIFQ did not find a known WordPress mail-delivery plugin. Delivery may still be handled by the server, hosting platform or another external service.', 'Verify how transactional email is delivered and monitored on this shop before changing anything.', 'WooCommerce is active. No known WordPress SMTP or transactional email delivery plugin was detected.' ); } $as_failed = (int) $data['action_scheduler_failed_count']; $as_overdue = (int) $data['action_scheduler_pending_old_count']; if ($as_failed >= 10 || $as_overdue >= 25) { $issues[] = $this->issue('action_scheduler_backlog', 'Maintenance', 'Scheduled actions appear to be falling behind', self::SEV_MEDIUM, 'Action Scheduler has a persistent-looking group of failed or overdue tasks. These tasks can support shop, email, subscription and maintenance processes.', 'Ask your developer or hosting provider to review Scheduled Actions, starting with repeated failures and the oldest overdue task.', 'Failed actions: ' . $as_failed . '. Pending actions older than one hour: ' . $as_overdue . '. Only local status counts were checked.' ); } elseif ($as_failed > 0) { $suggestions[] = $this->suggestion('action_scheduler_small_failure', 'Maintenance', 'A small number of scheduled actions failed', 'A few failed scheduled actions were found. Occasional failures can resolve on retry and do not always indicate an active problem.', 'Review them if the count continues to grow or related functionality is not completing as expected.', 'Failed actions counted: ' . $as_failed . '.' ); } if ($data['woocommerce_active'] && $cache_count > 1) { $issues[] = $this->issue('woo_multi_cache', 'Performance', 'WooCommerce with multiple caching plugins', self::SEV_HIGH, 'This webshop uses multiple caching or optimization plugins. On WooCommerce sites, overlapping cache rules can cause cart, checkout or session issues.', 'Use one main caching solution and make sure cart, checkout and account pages are excluded from aggressive caching.', 'Cache/optimization plugins found: ' . implode(', ', $this->plugin_names($data['cache_plugins'])) . '.' ); } if ($data['elementor_active'] && $elementor_count >= 5) { $issues[] = $this->issue('elementor_addons_many', 'Performance', 'Elementor with many add-ons', self::SEV_MEDIUM, 'Elementor is active together with many Elementor add-ons. This can add extra CSS, JavaScript and editor load.', 'Review which Elementor add-ons are really needed and remove duplicate or unused add-ons.', 'Elementor-related plugins found: ' . implode(', ', $this->plugin_names($data['elementor_plugins'])) . '.' ); } elseif ($data['elementor_active'] && $elementor_count >= 3) { $issues[] = $this->issue('elementor_addons_several', 'Performance', 'Elementor with several add-ons', self::SEV_MEDIUM, 'Elementor is active with several add-on plugins. This can increase frontend weight and make troubleshooting harder.', 'Review whether each Elementor add-on is still needed and avoid multiple add-ons that provide similar widgets.', 'Elementor-related plugins found: ' . implode(', ', $this->plugin_names($data['elementor_plugins'])) . '.' ); } if ($data['elementor_active'] && $data['woocommerce_active'] && (int) $data['active_plugin_count'] >= 30) { $issues[] = $this->issue('elementor_woo_complexity', 'Performance', 'Elementor and WooCommerce complexity detected', self::SEV_MEDIUM, 'This site combines WooCommerce, Elementor and many active plugins. That setup can work well, but it makes controlled maintenance, caching and troubleshooting more important.', 'Keep updates controlled, test important changes first and avoid adding new plugins for small features unless really needed.', 'WooCommerce and Elementor active. Active plugins: ' . $data['active_plugin_count'] . '.' ); } if ($page_builder_count >= 2) { $issues[] = $this->issue('multiple_page_builders', 'Performance', 'Multiple page builders active', self::SEV_MEDIUM, 'Multiple page builders appear to be active. This can make styling, performance and maintenance harder to manage.', 'If this is not part of a migration or redesign, ask your developer to review which page builder should be the main solution.', 'Page builders found: ' . implode(', ', $this->plugin_names($data['page_builder_plugins'])) . '.' ); } if ($data['large_images_count'] >= 10) { $issues[] = $this->issue('large_images', 'Performance', 'Several large images found', self::SEV_MEDIUM, 'Some large images were found in the checked recent media sample. Large image files can make pages load slower, especially on mobile connections.', 'Optimize large images or use reliable image compression. Make a backup first if you are replacing existing media files.', 'Large image files detected in the checked recent media sample. Count: ' . $data['large_images_count'] . '.' ); } else { $passed[] = $this->passed('large_images_ok', 'Performance', 'No unusually high number of large images', 'Fewer than 10 large images were found in the checked recent media sample. The full media library was not scanned.'); } if ($data['active_plugin_count'] >= 25) { $issues[] = $this->issue('many_plugins', 'Performance', 'Many active plugins found', self::SEV_MEDIUM, 'This is not automatically a problem, but many active plugins can make maintenance, performance and troubleshooting more demanding — particularly on shared hosting or in complex WooCommerce or Elementor setups.', 'Review whether all active plugins are still needed, maintained and not overlapping with another plugin.', 'Detected ' . $data['active_plugin_count'] . ' active plugins.' ); } else { $passed[] = $this->passed('active_plugin_count_looks_normal', 'Performance', 'Active plugin count looks normal', 'The number of active plugins is below the attention threshold.'); } // ----------------------------------------------------------------- // Security // ----------------------------------------------------------------- if ($security_count >= 2) { $issues[] = $this->issue('multi_security', 'Security', 'Multiple security plugins active', self::SEV_MEDIUM, 'Multiple security plugins are active. They can sometimes overlap or affect login, REST API and firewall rules.', 'Use one main security plugin and avoid duplicate firewall or login protection features.', 'Found: ' . implode(', ', $this->plugin_names($data['security_plugins'])) ); } elseif ($security_count === 1) { $passed[] = $this->passed('security_plugin_active', 'Security', 'One security plugin active', 'One security plugin was found.'); } else { $context_items[] = $this->context_item('security_tooling_not_detected', 'Security', 'No known security plugin detected', 'AIFQ did not find a known WordPress security plugin. Hosting, firewall or external security layers may still protect the site.', 'Plugin presence alone is not proof of security, and plugin absence is not proof that protection is missing.' ); } if (!empty($data['default_admin_username_exists'])) { $issues[] = $this->issue('default_admin_username', 'Security', 'Default admin username exists', self::SEV_LOW, 'The username “admin” exists on this site. This is a common target for automated login attempts.', 'Ask your developer to replace the default admin username safely. Do not delete the “admin” account directly; create a new administrator account first, log in with that account, reassign any content if needed and only then remove the old account.', 'A user with the username admin exists. No user email, ID or role data was scanned.' ); } else { $passed[] = $this->passed('admin_username_not_found', 'Security', 'Default admin username not found', 'No user with the username admin was found.'); } if (!empty($data['users_can_register']) && !empty($data['default_role_dangerous_caps'])) { $role_name = $data['default_role_name'] !== '' ? $data['default_role_name'] : $data['default_role_key']; $issues[] = $this->issue('open_registration_privileged_role', 'Security', 'New registrations receive a privileged role', self::SEV_HIGH, 'Anyone can create an account on this site, and new accounts receive the “' . $role_name . '” role. That role includes permissions beyond a basic user account.', 'Review Settings → General. Keep registration enabled if the site needs it, but choose a role with only the permissions new users require.', 'Registration is enabled. Default role: ' . $role_name . '. Elevated capabilities found: ' . implode(', ', $data['default_role_dangerous_caps']) . '.' ); } elseif (!empty($data['users_can_register'])) { $passed[] = $this->passed('open_registration_does_not_grant_elevated_permissions', 'Security', 'Open registration does not grant elevated permissions', 'New accounts receive a limited default role. Open registration may be intentional for shops, memberships or communities.'); } else { $passed[] = $this->passed('public_registration_disabled', 'Security', 'Public user registration is disabled', 'Visitors cannot create WordPress accounts through the default registration setting.'); } if (!empty($data['pending_admin_email']) && strtolower($data['pending_admin_email']) !== strtolower($data['current_admin_email'])) { $suggestions[] = $this->suggestion('pending_admin_email_change', 'Maintenance', 'Site administrator email change is waiting for confirmation', 'WordPress is waiting for confirmation before changing the site administrator email. Until then, administrative messages continue to use the current address.', 'Confirm the change from the email WordPress sent, or return to Settings → General if the requested address is no longer correct.', 'A pending site administrator email change was found. Email addresses are not included in this report.' ); } if (!empty($data['wp_cron_disabled'])) { $suggestions[] = $this->suggestion('wp_cron_disabled', 'Maintenance', 'WordPress page-load cron is disabled', 'WordPress is configured not to start scheduled tasks during normal page visits. This is often intentional when the server runs WordPress cron separately.', 'Confirm that the hosting platform or server calls wp-cron.php on a schedule. No change is needed when an external cron job is already configured.', 'DISABLE_WP_CRON is true. This scan does not inspect the server crontab or make a loopback request.' ); } if ((int) $data['administrator_count'] >= 4) { $suggestions[] = $this->suggestion('multiple_administrator_accounts', 'Security', sprintf('This site has %d administrator accounts', (int) $data['administrator_count']), 'Several accounts have full administrative access. This can be appropriate for a team, agency or hosting provider.', 'Check whether each administrator still needs full access. Keep legitimate team and service accounts in place.', 'Administrator accounts counted for this site: ' . (int) $data['administrator_count'] . '. No usernames, emails or last-login claims are included.' ); } if ($data['xmlrpc_enabled']) { $issues[] = $this->issue('xmlrpc', 'Security', 'XML-RPC is available in WordPress', self::SEV_LOW, 'XML-RPC is available at the WordPress level. Your hosting provider, firewall or security layer may still block it before it is reachable from outside.', 'If you do not use XML-RPC, ask your developer or hosting provider whether it is already blocked at server level or should be disabled safely. If you use the WordPress mobile app, Jetpack or external publishing tools, this may be intentional.', 'AIFQ Site Review checks the WordPress-side XML-RPC signal only. Hosting, firewall or security layers may still block XML-RPC before it is reachable from outside.' ); } else { $passed[] = $this->passed('xmlrpc_disabled', 'Security', 'XML-RPC is disabled in WordPress', 'The WordPress-side XML-RPC signal is disabled.'); } if ($data['file_editor_enabled']) { $issues[] = $this->issue('file_editor', 'Security', 'Dashboard file editing is enabled', self::SEV_MEDIUM, 'WordPress allows theme and plugin files to be edited from the dashboard. This increases risk if an admin account is ever compromised.', 'Set DISALLOW_FILE_EDIT to true in wp-config.php.', 'If someone gains access to an admin account, this feature can be abused to change code.' ); } else { $passed[] = $this->passed('file_editor_disabled', 'Security', 'Dashboard file editing is disabled', 'This is safer if an admin account is ever compromised.'); } if (!empty($data['file_mods_disabled'])) { $suggestions[] = $this->suggestion('file_mods_disabled', 'Maintenance', 'File modifications are disabled', 'WordPress file modifications are disabled. On managed hosting this can be intentional, but it can also explain why updates are not handled from the dashboard.', 'Check whether DISALLOW_FILE_MODS is intentionally enabled. If updates are managed elsewhere, this may be fine.', 'DISALLOW_FILE_MODS is set to true.' ); } if (!$data['is_ssl']) { $issues[] = $this->issue('no_ssl', 'Security', 'HTTPS is not active in WordPress admin', self::SEV_MEDIUM, 'AIFQ could not confirm HTTPS from the current WordPress request or configured public site URL. A reverse proxy or load balancer can still provide HTTPS externally.', 'Verify the public site and WordPress admin over HTTPS. If a proxy terminates HTTPS before WordPress, confirm that WordPress receives the correct forwarded HTTPS information.', 'Neither the current WordPress request nor the configured public home URL confirmed HTTPS.' ); } else { $passed[] = $this->passed('https_is_active', 'Security', 'HTTPS is active', 'WordPress admin appears to be running over HTTPS in this context.'); } if (!empty($data['site_home_mismatch'])) { $issues[] = $this->issue('site_url_home_mismatch', 'Health', 'WordPress Address and Site Address are different', self::SEV_LOW, 'The WordPress Address and Site Address settings are different. This can be intentional, but it can also cause redirect, login or URL issues when not configured deliberately.', 'Do not change these values unless you know why they differ. If this was not intentional, ask your developer or hosting provider to review the URL settings.', 'WordPress Address: ' . $data['site_url_option'] . '. Site Address: ' . $data['home_url_option'] . '.' ); } // ----------------------------------------------------------------- // Maintenance // ----------------------------------------------------------------- if (version_compare($data['wordpress_version'], '6.0', '<')) { $issues[] = $this->issue('old_wp_version', 'Maintenance', 'Very old WordPress version', self::SEV_HIGH, 'The website runs on an old major WordPress version. This can increase compatibility and security risk.', 'Plan a controlled WordPress update with a backup first.', 'Current WordPress version: ' . $data['wordpress_version'] . '.' ); } elseif ($data['core_updates_count'] > 0) { $issues[] = $this->issue('core_update', 'Maintenance', 'WordPress core update available', self::SEV_HIGH, 'A WordPress core update is currently reported as available. Core updates can include security, stability and compatibility improvements.', 'Create a backup first and then update WordPress.', 'Available core updates: ' . $data['core_updates_count'] . '. Current WordPress version: ' . $data['wordpress_version'] . '.' ); } else { $passed[] = $this->passed('core_up_to_date', 'Maintenance', 'WordPress core appears up to date', 'No pending WordPress core update is currently reported.'); } if ($data['plugin_updates_count'] > 0) { $issues[] = $this->issue('plugin_updates', 'Maintenance', 'Plugin updates available', self::SEV_MEDIUM, 'Plugin updates are currently reported as available by WordPress. Updates often include security and bug fixes.', 'Create a backup first and then update the listed plugins.', 'Plugins with updates: ' . implode(', ', $data['plugin_update_names']) . '.' ); } else { $passed[] = $this->passed('plugins_up_to_date', 'Maintenance', 'Plugins appear up to date', 'No pending plugin updates are currently reported by WordPress.'); } if ($data['theme_updates_count'] > 0) { $issues[] = $this->issue('theme_update', 'Maintenance', 'Theme update available', self::SEV_MEDIUM, 'Theme updates are currently reported as available by WordPress.', 'Update the theme after creating a backup.', 'Themes with updates: ' . implode(', ', $data['theme_update_names']) . '.' ); } else { $passed[] = $this->passed('theme_up_to_date', 'Maintenance', 'Theme appears up to date', 'No pending theme updates are currently reported by WordPress.'); } if ($data['inactive_plugin_count'] >= 7) { $issues[] = $this->issue('inactive_plugins', 'Maintenance', 'Several inactive plugins found', self::SEV_MEDIUM, 'Several inactive plugins are installed. Inactive plugins can still add maintenance overhead and may become outdated over time.', 'Review inactive plugins and remove the ones you no longer need. Keep a backup first if you are unsure what a plugin does.', 'Inactive plugins: ' . $data['inactive_plugin_count'] . '.' ); } elseif ($data['inactive_plugin_count'] >= 3) { $suggestions[] = $this->suggestion('inactive_plugins_soft', 'Maintenance', 'Several inactive plugins', 'A few inactive plugins are installed. These are not a direct risk, but they add maintenance work and can become outdated over time.', 'Review your inactive plugins. If you no longer use them, removing them keeps your installation lean and easier to maintain.', 'Inactive plugins are not automatically dangerous, but they add maintenance clutter.' ); } else { $passed[] = $this->passed('inactive_plugin_count_is_not_high', 'Maintenance', 'Inactive plugin count is not high', 'The number of inactive plugins is not unusually high.'); } if ($data['inactive_theme_count'] >= 3) { $issues[] = $this->issue('inactive_themes', 'Maintenance', 'Several inactive themes found', self::SEV_LOW, 'Several inactive themes are installed. Inactive themes add extra files and may become outdated over time.', 'Keep the active theme and, if useful, one fallback theme. Remove unused themes after confirming they are not needed.', 'Inactive themes: ' . $data['inactive_theme_count'] . '.' ); } elseif ($data['inactive_theme_count'] >= 2) { $suggestions[] = $this->suggestion('inactive_themes_soft', 'Maintenance', 'Several inactive themes', 'Several inactive themes are installed.', 'Keep the active theme and, if useful, one fallback theme. Remove unused themes after confirming they are not needed.', 'This is mostly a cleanup opportunity.' ); } else { $passed[] = $this->passed('inactive_theme_count_is_not_high', 'Maintenance', 'Inactive theme count is not high', 'The number of inactive themes is not unusually high.'); } if ($backup_count === 0) { $context_items[] = $this->context_item('backup_tooling_not_detected', 'Reliability', 'No known backup plugin detected', 'AIFQ did not find a known backup plugin. Hosting or an external service may still create backups.', 'What matters is whether a recent backup exists and can be restored; plugin presence alone cannot prove that.' ); } else { $passed[] = $this->passed('backup_found', 'Maintenance', 'Backup-related plugin found', 'A backup-related plugin is active. Verify that backups are actually running and can be restored. Found: ' . implode(', ', $this->plugin_names($data['backup_plugins'])) . '.'); } // ----------------------------------------------------------------- // Health // ----------------------------------------------------------------- if ($memory_mb < 128) { $issues[] = $this->issue('low_memory', 'Health', 'Low PHP memory limit', self::SEV_MEDIUM, 'The website has relatively little PHP memory available. Larger or more dynamic plugin setups can hit limits sooner.', 'Ask your hosting provider to increase the PHP memory limit. For larger or more dynamic WordPress setups, 256 MB or more is often a better baseline.', 'Current memory limit: ' . $data['memory_limit'] . '.' ); } else { $passed[] = $this->passed('php_memory_limit_appears_sufficient', 'Health', 'PHP memory limit appears sufficient', 'Memory limit: ' . $data['memory_limit'] . '.'); } if ($data['active_plugin_count'] >= 20 && $memory_mb < 256) { $issues[] = $this->issue('memory_plugin_conflict', 'Health', 'Many plugins with limited memory', self::SEV_HIGH, 'Many active plugins are present while the PHP memory limit is relatively limited. This can make wp-admin feel slower or increase the chance of memory-related errors.', 'Consider increasing the memory limit to 256 MB or more and reviewing whether all active plugins are still needed.', 'Active plugins: ' . $data['active_plugin_count'] . '. Memory limit: ' . $data['memory_limit'] . '.' ); } if (version_compare($data['php_version'], '8.1', '<')) { if ($updates_total >= 5) { $issues[] = $this->issue('old_php_updates', 'Health', 'Old PHP version with many updates', self::SEV_HIGH, 'This website runs on PHP ' . $data['php_version'] . ' and also has several pending updates. That combination increases the chance of compatibility problems.', 'Create a backup first. Then ask your hosting provider or developer to plan the PHP and plugin/theme updates carefully.', 'Total updates: ' . $updates_total . '. PHP version: ' . $data['php_version'] . '.' ); } else { $issues[] = $this->issue('old_php', 'Health', 'Old PHP version', self::SEV_MEDIUM, 'This website runs on PHP ' . $data['php_version'] . '. Newer PHP versions are faster and better supported.', 'Ask your hosting provider about upgrading to PHP 8.1 or later.', 'Current PHP version: ' . $data['php_version'] . '.' ); } } else { $passed[] = $this->passed('php_version_is_modern_enough', 'Health', 'PHP version is modern enough', 'PHP ' . $data['php_version'] . ' is active.'); } if (!$data['upload_dir_writable']) { $issues[] = $this->issue('upload_not_writable', 'Health', 'Upload directory is not writable', self::SEV_HIGH, 'WordPress may not be able to save files in the uploads directory.', 'Check the file permissions for wp-content/uploads.', 'This can cause problems when uploading images, documents and updates.' ); } else { $passed[] = $this->passed('upload_directory_is_writable', 'Health', 'Upload directory is writable', 'WordPress can save files in the uploads directory.'); } if ($data['debug_enabled']) { if ($is_production_environment && !empty($data['debug_display_enabled'])) { $issues[] = $this->issue('wp_debug_display', 'Health', 'Error display is enabled', self::SEV_MEDIUM, 'Error display is enabled on this site. On a live site, this can show technical error messages to visitors.', 'Ask your developer to disable WP_DEBUG_DISPLAY on the live site.', 'WP_DEBUG and WP_DEBUG_DISPLAY are both enabled. Environment type: ' . $environment_type . '.' ); } elseif ($is_production_environment) { $issues[] = $this->issue('debug_on', 'Health', 'WP_DEBUG is enabled', self::SEV_MEDIUM, 'Debug mode appears to be active. This is useful during development, but not recommended on a live website.', 'Ask your developer to disable WP_DEBUG on live websites.', 'Debug messages can expose technical information visitors should not see.' ); } else { $suggestions[] = $this->suggestion('debug_on', 'Health', 'WP_DEBUG is enabled', 'Debug mode appears to be active. On staging, development or local sites, this can be intentional.', 'If this is a live public website, ask your developer to disable WP_DEBUG. If this is a development environment, this may be fine.', 'Environment type: ' . $environment_type . '.' ); } } else { $passed[] = $this->passed('debug_disabled', 'Health', 'Debug mode is disabled', 'Good for a normal live website.'); } if (!empty($data['savequeries_enabled'])) { if ($is_production_environment) { $issues[] = $this->issue('savequeries_enabled', 'Health', 'Query logging is enabled', self::SEV_MEDIUM, 'A developer diagnostic setting is active on this site. SAVEQUERIES logs database activity for debugging, but can add unnecessary load on a live website.', 'Ask your developer to disable SAVEQUERIES after debugging is complete.', 'SAVEQUERIES is enabled. Environment type: ' . $environment_type . '.' ); } else { $suggestions[] = $this->suggestion('savequeries_enabled', 'Health', 'Query logging is enabled', 'WordPress query logging is active. On staging, development or local sites, this can be intentional.', 'If this is a live public website, ask your developer to disable SAVEQUERIES after debugging is complete. If this is a development environment, this may be fine.', 'SAVEQUERIES is enabled. Environment type: ' . $environment_type . '.' ); } } if (!empty($data['debug_log_exists']) && (int) $data['debug_log_size'] > 0) { $issues[] = $this->issue('debug_log_found', 'Health', 'Debug log file found', self::SEV_MEDIUM, 'A WordPress error log exists and contains data. This can mean there are active errors or warnings that should be reviewed by a developer or hosting provider.', 'Ask your developer or hosting provider to review the WordPress error log, investigate what is causing the errors, and clear the log only after the underlying issue is resolved.', 'Debug log size: ' . size_format((int) $data['debug_log_size']) . '.' ); } else { $passed[] = $this->passed('debug_log_clean', 'Health', 'No active debug log found', 'No non-empty wp-content/debug.log file was found.'); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core Site Health filter; the name must remain unprefixed. $autoload_limit = (int) apply_filters('site_status_autoloaded_options_size_limit', 800000); if ((int) $data['autoloaded_options_size'] > $autoload_limit) { $top_parts = []; foreach (array_slice((array) $data['autoloaded_options_top'], 0, 5) as $item) { $top_parts[] = $item['name'] . ' (' . size_format((int) $item['size']) . ')'; } $context = 'Autoloaded data: ' . size_format((int) $data['autoloaded_options_size']) . '. Current Site Health threshold: ' . size_format($autoload_limit) . '.'; if (!empty($top_parts)) $context .= ' Largest entries: ' . implode(', ', $top_parts) . '. Names indicate entries, not proven plugin ownership.'; $issues[] = $this->issue('large_autoloaded_options', 'Performance', 'More WordPress data loads automatically than usual', self::SEV_MEDIUM, 'WordPress loads saved option data on every request. The current total is above the Site Health threshold and may add work to uncached page and dashboard requests.', 'Review the largest entries before making changes. Do not delete options or change autoload values without confirming what created them and whether they are still needed.', $context ); } else { $passed[] = $this->passed('autoloaded_option_data_is_within_the_current_threshold', 'Performance', 'Autoloaded option data is within the current threshold', 'The automatically loaded option data is below the current WordPress Site Health threshold.'); } if ($data['post_revisions_count'] >= 500) { $issues[] = $this->issue('many_revisions', 'Health', 'Many revisions in the database', self::SEV_MEDIUM, 'WordPress automatically saves older versions of posts and pages while you edit them. Over time, many revisions can make the database larger than needed.', 'Make a backup first. Then clean up old revisions with a trusted database optimization tool, or ask your developer to do this safely.', 'Found: about ' . $data['post_revisions_count'] . ' post revisions.' ); } else { $passed[] = $this->passed('revision_count_is_not_high', 'Health', 'Revision count is not high', 'The database does not appear to contain an unusually high number of old revisions.'); } $environment_source = defined('WP_ENVIRONMENT_TYPE') ? 'explicitly configured' : 'WordPress default'; $context_items[] = $this->context_item('environment_type', 'Site context', 'WordPress environment: ' . $environment_type, 'AIFQ uses this context when interpreting search visibility and development settings.', 'Source: ' . $environment_source . '.'); if ((int) $data['application_password_count'] > 0) { $context_items[] = $this->context_item('application_passwords_in_use', 'Access context', 'Application Passwords are in use', sprintf('%1$d application password(s) are connected to %2$d user account(s). These credentials are commonly used by integrations, apps and automated tools.', (int) $data['application_password_count'], (int) $data['application_password_users']), 'Review credentials in the relevant user profiles and revoke only those that are no longer needed. No hashes or IP addresses are stored in the scan.'); } if ((int) $data['mu_plugin_count'] > 0 || (int) $data['dropin_count'] > 0) { $details = sprintf('%1$d must-use plugin(s) and %2$d WordPress drop-in(s) found.', (int) $data['mu_plugin_count'], (int) $data['dropin_count']); if (!empty($data['dropin_names'])) $details .= ' Drop-ins: ' . implode(', ', array_slice($data['dropin_names'], 0, 8)) . '.'; $context_items[] = $this->context_item('advanced_plugins_context', 'Technical context', 'Advanced WordPress components are present', 'Must-use plugins and drop-ins can be supplied by the hosting platform, an agency or another plugin. Their presence is not a problem by itself.', $details . ' Keep this context in mind when troubleshooting or moving the site.'); } return ['issues' => $issues, 'suggestions' => $suggestions, 'passed' => $passed, 'context_items' => $context_items]; } private function severity_sort_value_for_key($severity) { if ($severity === self::SEV_HIGH) { return 3; } if ($severity === self::SEV_MEDIUM) { return 2; } return 1; } private function dedupe_issues_by_key(array $items) { $deduped = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $key = isset($item['key']) && $item['key'] !== '' ? (string) $item['key'] : $this->title_to_key((string) ($item['title'] ?? '')); if ($key === '') { $key = 'generic_' . md5(wp_json_encode($item)); } $item['key'] = $key; if (!isset($deduped[$key])) { $deduped[$key] = $item; continue; } $existing = $deduped[$key]; $new_severity = $this->severity_sort_value_for_key($item['severity'] ?? self::SEV_LOW); $existing_severity = $this->severity_sort_value_for_key($existing['severity'] ?? self::SEV_LOW); if ($new_severity > $existing_severity) { $deduped[$key] = $item; continue; } if ($new_severity === $existing_severity && strlen((string) ($item['context'] ?? '')) > strlen((string) ($existing['context'] ?? ''))) { $deduped[$key] = $item; } } return array_values($deduped); } private function suppress_general_cache_when_woocommerce_specific(array $items) { $keys = array_column($items, 'key'); $suppressed_keys = []; if (in_array('woo_multi_cache', $keys, true)) { $suppressed_keys[] = 'multi_cache'; } if (in_array('woo_no_cache', $keys, true)) { $suppressed_keys[] = 'no_cache'; } if (empty($suppressed_keys)) { return $items; } $filtered = []; foreach ($items as $item) { if (in_array(($item['key'] ?? ''), $suppressed_keys, true)) { continue; } $filtered[] = $item; } return $filtered; } private function consolidate_development_settings_cluster(array $issues) { $by_key = []; foreach ($issues as $issue) { if (!is_array($issue)) { continue; } $key = isset($issue['key']) ? (string) $issue['key'] : $this->title_to_key((string) ($issue['title'] ?? '')); if ($key !== '') { $by_key[$key] = $issue; } } $debug_key = isset($by_key['wp_debug_display']) ? 'wp_debug_display' : (isset($by_key['debug_on']) ? 'debug_on' : ''); if ($debug_key === '' || !isset($by_key['savequeries_enabled']) || !isset($by_key['debug_log_found'])) { return $issues; } $signals = []; $signals[] = __('WP_DEBUG is enabled', 'aifq-site-review'); if ($debug_key === 'wp_debug_display') { $signals[] = __('WP_DEBUG_DISPLAY is enabled', 'aifq-site-review'); } $signals[] = __('SAVEQUERIES is enabled', 'aifq-site-review'); $signals[] = __('WordPress error log contains data', 'aifq-site-review'); $cluster = $this->issue( 'development_settings_active', 'Health', 'Development settings are still active', self::SEV_HIGH, 'Several debugging settings are active on this site. These settings are useful while troubleshooting, but should usually be reviewed and turned off or cleaned up on a live website.', 'Ask your developer or hosting provider to review the errors, turn off debugging settings when troubleshooting is complete, and clear the log only after the underlying issue is resolved.', implode('. ', $signals) . '.' ); $cluster['confidence'] = 'high'; $cluster['cluster_signals'] = $signals; $cluster['cluster_child_keys'] = array_values(array_filter([$debug_key, 'savequeries_enabled', 'debug_log_found'])); $child_keys = [ 'debug_on' => true, 'wp_debug_display' => true, 'savequeries_enabled' => true, 'debug_log_found' => true, ]; $filtered = [$cluster]; foreach ($issues as $issue) { $key = isset($issue['key']) ? (string) $issue['key'] : $this->title_to_key((string) ($issue['title'] ?? '')); if (isset($child_keys[$key])) { continue; } $filtered[] = $issue; } return $filtered; } private function cluster_signal_label_for_key($key, array $issue) { $context = $this->clean_context_text($issue['context'] ?? ''); switch ($key) { case 'many_plugins': if (preg_match('/(\d+) active plugins/i', $context, $m)) { return sprintf( /* translators: %d: number of active plugins. */ __('Many active plugins (%d active)', 'aifq-site-review'), (int) $m[1] ); } return __('Many active plugins', 'aifq-site-review'); case 'woo_many_plugins': if (preg_match('/Active plugins:\s*(\d+)/i', $context, $m)) { return sprintf( /* translators: %d: number of active plugins. */ __('Many plugins active on this WooCommerce site (%d active)', 'aifq-site-review'), (int) $m[1] ); } return __('Many plugins active on this WooCommerce site', 'aifq-site-review'); case 'heavy_plugins': return $context !== '' ? __('Several heavier plugins active', 'aifq-site-review') . ' — ' . $context : __('Several heavier plugins active', 'aifq-site-review'); case 'elementor_addons_many': case 'elementor_addons_several': return $context !== '' ? __('Elementor with add-ons', 'aifq-site-review') . ' — ' . $context : __('Elementor with add-ons', 'aifq-site-review'); case 'elementor_woo_complexity': return $context !== '' ? __('WooCommerce and Elementor complexity', 'aifq-site-review') . ' — ' . $context : __('WooCommerce and Elementor complexity', 'aifq-site-review'); case 'woo_failed_scheduled_actions': if (preg_match('/counted:\s*(\d+)/i', $context, $m)) { return sprintf( /* translators: %d: number of failed scheduled actions. */ __('Failed scheduled actions: %d', 'aifq-site-review'), (int) $m[1] ); } return __('Failed scheduled actions found', 'aifq-site-review'); case 'woo_scheduled_actions_backlog': if (preg_match('/counted:\s*(\d+)/i', $context, $m)) { return sprintf( /* translators: %d: number of pending scheduled actions. */ __('Pending actions older than one hour: %d', 'aifq-site-review'), (int) $m[1] ); } return __('Pending actions older than one hour found', 'aifq-site-review'); } return $this->display_issue_title($issue); } private function max_child_severity(array $children) { $max = self::SEV_LOW; $max_weight = $this->severity_sort_value_for_key($max); foreach ($children as $child) { $severity = $child['severity'] ?? self::SEV_LOW; $weight = $this->severity_sort_value_for_key($severity); if ($weight > $max_weight) { $max = $severity; $max_weight = $weight; } } return $max; } private function consolidate_complex_plugin_cluster(array $issues) { $child_keys = [ 'many_plugins' => true, 'woo_many_plugins' => true, 'heavy_plugins' => true, 'elementor_addons_many' => true, 'elementor_woo_complexity' => true, ]; $children = []; foreach ($issues as $issue) { if (!is_array($issue)) { continue; } $key = isset($issue['key']) ? (string) $issue['key'] : $this->title_to_key((string) ($issue['title'] ?? '')); if (isset($child_keys[$key])) { $children[$key] = $issue; } } if (count($children) < 2) { return $issues; } $signals = []; foreach ($children as $key => $child) { $signals[] = $this->cluster_signal_label_for_key($key, $child); } $cluster = $this->issue( 'complex_plugin_setup', 'Performance', 'Complex plugin setup needs review', $this->max_child_severity($children), 'Several plugin and builder signals point to a setup that benefits from periodic maintenance review.', 'Review the plugin setup with your developer. Focus on whether all active plugins are still needed and whether any plugins overlap.', implode('. ', array_map('wp_strip_all_tags', $signals)) . '.' ); $cluster['confidence'] = 'high'; $cluster['cluster_signals'] = $signals; $cluster['cluster_child_keys'] = array_keys($children); $filtered = [$cluster]; foreach ($issues as $issue) { $key = isset($issue['key']) ? (string) $issue['key'] : $this->title_to_key((string) ($issue['title'] ?? '')); if (isset($child_keys[$key])) { continue; } $filtered[] = $issue; } return $filtered; } private function consolidate_woocommerce_background_tasks_cluster(array $issues) { $required_keys = [ 'woo_failed_scheduled_actions' => true, 'woo_scheduled_actions_backlog' => true, ]; $children = []; foreach ($issues as $issue) { if (!is_array($issue)) { continue; } $key = isset($issue['key']) ? (string) $issue['key'] : $this->title_to_key((string) ($issue['title'] ?? '')); if (isset($required_keys[$key])) { $children[$key] = $issue; } } if (count($children) < 2) { return $issues; } $signals = []; foreach ($children as $key => $child) { $signals[] = $this->cluster_signal_label_for_key($key, $child); } $cluster = $this->issue( 'woo_background_tasks', 'Maintenance', 'WooCommerce background tasks need review', $this->max_child_severity($children), 'WooCommerce has both failed and delayed background tasks that should be reviewed together.', 'Ask your developer or hosting provider to check WooCommerce scheduled actions and WP-Cron reliability.', implode('. ', array_map('wp_strip_all_tags', $signals)) . '.' ); $cluster['confidence'] = 'high'; $cluster['cluster_signals'] = $signals; $cluster['cluster_child_keys'] = array_keys($children); $filtered = [$cluster]; foreach ($issues as $issue) { $key = isset($issue['key']) ? (string) $issue['key'] : $this->title_to_key((string) ($issue['title'] ?? '')); if (isset($required_keys[$key])) { continue; } $filtered[] = $issue; } return $filtered; } private function finalize_scan_items(array $issues, array $suggestions, array $passed) { $issues = $this->dedupe_issues_by_key($issues); $issues = $this->suppress_general_cache_when_woocommerce_specific($issues); $issues = $this->consolidate_development_settings_cluster($issues); $issues = $this->consolidate_complex_plugin_cluster($issues); $issues = $this->consolidate_woocommerce_background_tasks_cluster($issues); $suggestions = $this->dedupe_issues_by_key($suggestions); $suggestions = $this->suppress_general_cache_when_woocommerce_specific($suggestions); $passed = $this->filter_conflicting_passed_items($passed, $issues, $suggestions); return [ 'issues' => $issues, 'suggestions' => $suggestions, 'passed' => $passed, ]; } private function conflict_group_for_key($key) { $map = [ 'active_cache_plugin' => 'cache_plugins', 'multi_cache' => 'cache_plugins', 'no_cache' => 'cache_plugins', 'no_cache_layer' => 'cache_plugins', 'cache_signal_no_plugin' => 'cache_plugins', 'no_object_cache_dropin' => 'object_cache', 'object_cache_found' => 'object_cache', 'object_cache_runtime_failure'=> 'object_cache', 'multi_seo' => 'seo_plugins', 'no_seo' => 'seo_plugins', 'multi_security' => 'security_plugins', 'no_security' => 'security_plugins', 'security_plugin_active' => 'security_plugins', 'search_visibility_enabled' => 'search_visibility', 'search_visibility_disabled'=> 'search_visibility', 'core_up_to_date' => 'core_updates', 'core_update' => 'core_updates', 'old_wp_version' => 'core_updates', 'plugins_up_to_date' => 'plugin_updates', 'plugin_updates' => 'plugin_updates', 'theme_up_to_date' => 'theme_updates', 'theme_update' => 'theme_updates', 'debug_disabled' => 'debug_mode', 'debug_on' => 'debug_mode', 'wp_debug_display' => 'debug_display', 'debug_log_clean' => 'debug_log', 'debug_log_found' => 'debug_log', 'large_images' => 'large_images', 'large_images_ok' => 'large_images', 'autoload_ok' => 'autoloaded_options', 'large_autoloaded_options' => 'autoloaded_options', 'xmlrpc_disabled' => 'xmlrpc', 'xmlrpc' => 'xmlrpc', 'file_editor_disabled' => 'file_editor', 'file_editor' => 'file_editor', 'file_mods_disabled' => 'file_mods', 'site_url_home_mismatch' => 'url_settings', 'savequeries_enabled' => 'savequeries', 'development_settings_active'=> 'development_settings', 'default_admin_username' => 'admin_username', 'public_user_registration' => 'user_registration', 'woo_no_mail_delivery_plugin'=> 'mail_delivery', 'woo_failed_scheduled_actions'=> 'action_scheduler', 'woo_scheduled_actions_backlog'=> 'action_scheduler', 'multiple_page_builders' => 'page_builders', 'admin_username_not_found' => 'admin_username', 'public_registration_disabled'=> 'user_registration', 'no_backup' => 'backup_solution', 'backup_found' => 'backup_solution', ]; return $map[$key] ?? ''; } private function passed_title_to_key($title) { return AIFQ_SR_Finding_Catalog::legacy_passed_key_for_title($title); } private function filter_conflicting_passed_items(array $passed, array $issues, array $suggestions = []) { $blocked_groups = []; foreach (array_merge($issues, $suggestions) as $item) { $key = isset($item['key']) ? (string) $item['key'] : $this->title_to_key((string) ($item['title'] ?? '')); $group = $this->conflict_group_for_key($key); if ($group !== '') { $blocked_groups[$group] = true; } } $filtered = []; foreach ($passed as $point) { $title = (string) ($point['title'] ?? ''); $key = sanitize_key((string) ($point['key'] ?? '')); if ($key === '') { $key = $this->passed_title_to_key($title); } $group = $this->conflict_group_for_key($key); if ($group !== '' && isset($blocked_groups[$group])) { continue; } $filtered[] = $point; } return $filtered; } private function build_profile(array $data) { $profile = []; if (!empty($data['wordpress_version'])) { $profile[] = 'WordPress ' . $data['wordpress_version']; } if (!empty($data['environment_type'])) { $profile[] = 'Environment: ' . $data['environment_type']; } if ($data['woocommerce_active']) { $profile[] = 'WooCommerce active'; } if ($data['elementor_active']) { $profile[] = 'Elementor active'; } if (count($data['seo_plugins']) > 0) { $profile[] = 'SEO plugin found'; } if (!empty($data['active_cache_plugin_count'])) { $profile[] = 'Active cache plugin found'; } elseif (!empty($data['cache_constant_enabled'])) { $profile[] = 'Cache constant enabled'; } elseif (!empty($data['cache_dropin_found'])) { $profile[] = 'Cache drop-in found'; } elseif (!empty($data['object_cache_dropin_found'])) { $profile[] = 'Object cache drop-in found'; } else { $profile[] = 'No local page-cache signal detected'; } $profile[] = $data['active_plugin_count'] . ' active plugins'; $profile[] = 'PHP ' . $data['php_version']; return $this->sanitize_text_list($profile); } // ------------------------------------------------------------------------- // Issue / passed helpers // ------------------------------------------------------------------------- private function issue($key, $category, $title, $severity, $explanation, $recommendation, $context = '', $registry_presentation = 'attention') { $key = sanitize_key((string) $key); AIFQ_SR_Finding_Catalog::note_runtime_occurrence($key, $registry_presentation); $issue = [ 'type' => 'attention', 'category_id' => AIFQ_SR_Finding_Catalog::category_id_for_key($key, $category), 'category' => $category, 'title' => $title, 'key' => $key, 'severity' => $severity, 'explanation' => $explanation, 'recommendation' => $recommendation, 'context' => $context, ]; $issue['confidence'] = $this->issue_confidence($issue); $issue['simple_explanation'] = $this->issue_simple_explanation($issue); $issue['if_ignored'] = $this->issue_if_ignored($issue); return $issue; } private function suggestion($key, $category, $title, $explanation, $recommendation, $context = '') { $suggestion = $this->issue($key, $category, $title, self::SEV_LOW, $explanation, $recommendation, $context, 'opportunity'); $suggestion['type'] = 'opportunity'; $suggestion['confidence'] = 'medium'; return $suggestion; } private function passed($key, $category, $title, $description) { $key = sanitize_key((string) $key); return [ 'key' => $key, 'category_id' => AIFQ_SR_Finding_Catalog::category_id_for_key($key, $category), 'category' => $category, 'title' => $title, 'description' => $description, ]; } private function context_item($key, $category, $title, $description, $details = '') { return [ 'key' => sanitize_key($key), 'category' => sanitize_text_field($category), 'title' => sanitize_text_field($title), 'description' => sanitize_text_field($description), 'details' => sanitize_text_field($details), ]; } // Maps issue title → stable key used for all dispatch (replaces fragile strpos chains) private function title_to_key($title) { return AIFQ_SR_Finding_Catalog::legacy_key_for_title($title); } // ------------------------------------------------------------------------- // Scoring // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- // Summary / grouping helpers // ------------------------------------------------------------------------- private function get_first_actions(array $issues, $limit = 3) { // Keep handoff/recommended actions aligned with the same calm priority order used by Start here. return array_slice($this->v10_sort_attention_items($issues), 0, max(0, (int) $limit)); } private function severity_sort_weight($severity) { if ($severity === self::SEV_HIGH) { return 3; } if ($severity === self::SEV_MEDIUM) { return 2; } return 1; } private function get_good_points(array $passed, $limit = 6) { $weights = [ 'HTTPS is active' => 100, 'WordPress core appears up to date' => 95, 'Plugins appear up to date' => 90, 'PHP version is modern enough' => 85, 'PHP memory limit appears sufficient' => 80, 'Caching signal found' => 75, 'One security plugin active' => 70, 'Search engine visibility is enabled' => 65, 'Readable permalink structure active' => 60, 'One SEO plugin active' => 55, 'Debug mode is disabled' => 50, 'Dashboard file editing is disabled' => 45, 'XML-RPC appears disabled' => 40, 'XML-RPC is disabled in WordPress' => 40, ]; usort($passed, function($a, $b) use ($weights) { $title_a = $this->display_passed_title($a); $title_b = $this->display_passed_title($b); return ($weights[$title_b] ?? 0) <=> ($weights[$title_a] ?? 0); }); return array_slice($passed, 0, $limit); } private function get_first_suggestions(array $suggestions, $limit = 3) { $priority = [ 'no_backup' => 100, 'no_security' => 90, 'no_seo' => 80, 'no_cache' => 70, 'cache_signal_no_plugin' => 68, 'no_object_cache_dropin' => 66, 'woo_no_mail_delivery_plugin' => 65, 'public_user_registration' => 62, 'plain_permalinks' => 60, 'inactive_plugins_soft' => 50, 'inactive_themes_soft' => 45, ]; usort($suggestions, function($a, $b) use ($priority) { $priority_a = $priority[$a['key'] ?? 'generic'] ?? 10; $priority_b = $priority[$b['key'] ?? 'generic'] ?? 10; return $priority_b <=> $priority_a; }); return array_slice($suggestions, 0, $limit); } private function scan_timestamp($value) { if (is_numeric($value)) { $timestamp = (int) $value; return $timestamp > 0 ? $timestamp : 0; } $timestamp = strtotime((string) $value); return $timestamp ? (int) $timestamp : 0; } private function format_scan_date($scan_time) { $timestamp = $this->scan_timestamp($scan_time); if (!$timestamp) { return ''; } return wp_date(get_option('date_format', 'Y-m-d') . ' ' . get_option('time_format', 'H:i'), $timestamp); } private function issue_handler_label($issue) { $key = isset($issue['key']) ? (string) $issue['key'] : ''; $type = isset($issue['type']) ? (string) $issue['type'] : 'attention'; $handler_id = AIFQ_SR_Finding_Catalog::handler_id_for_key($key); $labels = [ 'site_owner' => __('Site owner', 'aifq-site-review'), 'developer' => __('Developer', 'aifq-site-review'), 'hosting_provider' => __('Hosting provider', 'aifq-site-review'), 'developer_or_host' => __('Developer or hosting provider', 'aifq-site-review'), 'site_owner_or_developer' => __('Site owner or developer', 'aifq-site-review'), ]; if ($handler_id !== '' && isset($labels[$handler_id])) { return $labels[$handler_id]; } // Fail-soft compatibility for trusted extension output that is not part // of the built-in registry. Unknown extension keys never become // Decision-Memory eligible merely because they can be displayed. if (in_array($type, ['suggestion', 'opportunity'], true)) { return __('Site owner', 'aifq-site-review'); } return __('Developer or hosting provider', 'aifq-site-review'); } private function issue_preview($issue) { switch ($issue['key'] ?? 'generic') { case 'development_settings_active': return __('Several debugging settings are active on this site. These settings are useful while troubleshooting, but should usually be reviewed and turned off or cleaned up on a live website.', 'aifq-site-review'); case 'complex_plugin_setup': return __('Several plugin and builder signals point to a setup that benefits from periodic maintenance review.', 'aifq-site-review'); case 'woo_background_tasks': return __('WooCommerce has both failed and delayed background tasks that should be reviewed together.', 'aifq-site-review'); case 'savequeries_enabled': return __('A developer diagnostic setting is active on this site. SAVEQUERIES logs database activity for debugging, but can add unnecessary load on a live website.', 'aifq-site-review'); case 'default_admin_username': return __('The username admin exists on this site. This is a common target for automated login attempts.', 'aifq-site-review'); case 'public_user_registration': return __('Visitors may be able to create accounts on this WordPress site.', 'aifq-site-review'); case 'woo_no_mail_delivery_plugin': return __('WooCommerce sends important emails such as order confirmations, password resets and customer notifications. This scan did not find a known transactional email delivery plugin.', 'aifq-site-review'); case 'woo_failed_scheduled_actions': return __('WooCommerce uses background tasks for scheduled work. Some tasks appear to have failed and may need review.', 'aifq-site-review'); case 'woo_scheduled_actions_backlog': return __('Some WooCommerce background tasks appear to be waiting longer than expected.', 'aifq-site-review'); case 'woo_database_update_required': return __('WooCommerce reports that its database update has not been completed yet.', 'aifq-site-review'); case 'woo_checkout_page_missing': return __('WooCommerce does not currently have a usable Checkout page assigned.', 'aifq-site-review'); case 'woo_checkout_page_not_public': return __('The assigned WooCommerce Checkout page exists, but it is not published for normal visitors.', 'aifq-site-review'); case 'woo_checkout_content_missing': return __('The assigned Checkout page is public, but WooCommerce does not detect its Checkout block or legacy checkout shortcode on that page.', 'aifq-site-review'); case 'object_cache_runtime_failure': return __('The persistent object-cache provider reports that its integration failed to load or initialize correctly.', 'aifq-site-review'); case 'multiple_page_builders': return __('Multiple page builders can make styling, performance and maintenance harder to manage.', 'aifq-site-review'); case 'multi_cache': return __('Multiple caching layers can work against each other instead of making the site faster.', 'aifq-site-review'); case 'no_cache': case 'no_cache_layer': return __('This scan did not find a known local page-cache plugin, WP_CACHE constant or advanced-cache.php drop-in. Your hosting provider may still have server- or edge-level page caching enabled.', 'aifq-site-review'); case 'no_object_cache_dropin': return __('No persistent object cache was found. Object caching is separate from normal page caching plugins and can help some larger or dynamic WordPress sites, especially WooCommerce sites. It is not always needed.', 'aifq-site-review'); case 'cache_signal_no_plugin': return __('A local page-cache signal exists, but no active known cache/performance plugin was detected.', 'aifq-site-review'); case 'search_visibility_disabled': return __('WordPress is currently asking search engines not to index this site.', 'aifq-site-review'); case 'plain_permalinks': return __('The site appears to use the plain/default permalink structure.', 'aifq-site-review'); case 'multi_seo': return __('Multiple SEO plugins can create overlapping settings for metadata, sitemaps and redirects.', 'aifq-site-review'); case 'no_seo': return __('No known SEO plugin was detected. This can be an improvement opportunity if search visibility matters.', 'aifq-site-review'); case 'multi_security': return __('Multiple security plugins can create overlapping rules, blocks or login protection.', 'aifq-site-review'); case 'no_security': return __('No known security plugin was found. Bots commonly target WordPress login pages and forms. A security plugin can help with monitoring, blocking and alerts.', 'aifq-site-review'); case 'heavy_plugins': return __('These plugins are common on larger or more dynamic sites. They are not automatically a problem, but caching and maintenance become more important.', 'aifq-site-review'); case 'many_plugins': return __('Having many active plugins is not automatically a problem, but it can make maintenance, performance and troubleshooting more demanding.', 'aifq-site-review'); case 'elementor_addons_many': return __('Many Elementor add-ons can add extra CSS, JavaScript and editor load.', 'aifq-site-review'); case 'elementor_addons_several': return __('Several Elementor add-ons can add extra CSS, JavaScript and editor load.', 'aifq-site-review'); case 'woo_no_cache': return __('WooCommerce sites need careful page-cache rules because cart, checkout and account behavior is dynamic.', 'aifq-site-review'); case 'woo_many_plugins': return __('WooCommerce is active with many plugins. That is common on stores, but checkout, payments and background tasks benefit from occasional review.', 'aifq-site-review'); case 'woo_multi_cache': return __('Multiple caching plugins on WooCommerce can overlap around cart, checkout and sessions.', 'aifq-site-review'); case 'elementor_woo_complexity': return __('WooCommerce, Elementor and many active plugins benefit from controlled maintenance.', 'aifq-site-review'); case 'old_wp_version': return __('The site is running an old major WordPress version.', 'aifq-site-review'); case 'core_update': case 'plugin_updates': case 'theme_update': return __('Pending updates are important for maintenance, security and compatibility.', 'aifq-site-review'); case 'old_php': case 'old_php_updates': case 'low_memory': case 'memory_plugin_conflict': return __('PHP settings have a direct effect on stability and compatibility.', 'aifq-site-review'); case 'xmlrpc': return __('XML-RPC is available at the WordPress level. Hosting, firewall or security layers may still block it before it is reachable from outside.', 'aifq-site-review'); case 'file_editor': return __('Dashboard file editing increases risk if an admin account is ever compromised.', 'aifq-site-review'); case 'file_mods_disabled': return __('Dashboard updates may be blocked because file modifications are disabled.', 'aifq-site-review'); case 'site_url_home_mismatch': return __('The WordPress Address and Site Address are different. This can be intentional, but may cause URL, login or redirect issues if not configured deliberately.', 'aifq-site-review'); case 'large_autoloaded_options': return __('WordPress is loading more saved settings than normal on each visit. Extra autoloaded data can make the site or wp-admin feel slower.', 'aifq-site-review'); case 'large_images': return __('Some large images were found in the checked recent media sample.', 'aifq-site-review'); case 'debug_log_found': return __('A WordPress error log exists and contains data. This can mean there are active errors or warnings that should be reviewed by a developer or hosting provider.', 'aifq-site-review'); case 'wp_debug_display': return __('Error display is enabled. On a live site, this can show technical error messages to visitors.', 'aifq-site-review'); case 'many_revisions': return __('WordPress automatically saves older versions of posts and pages while you edit them. Over time, many revisions can make the database larger than needed.', 'aifq-site-review'); case 'no_backup': return __('No known backup plugin was detected. Check whether your hosting already provides reliable backups.', 'aifq-site-review'); case 'inactive_plugins': return __('Inactive plugins can add maintenance overhead over time, even when they are not actively used.', 'aifq-site-review'); case 'inactive_themes': return __('Unused themes add extra files and can become outdated over time.', 'aifq-site-review'); default: return __('This can make the WordPress setup harder to maintain or troubleshoot over time.', 'aifq-site-review'); } } private function issue_confidence($issue) { $high_confidence_keys = [ 'multi_cache', 'multi_seo', 'multi_security', 'woo_many_plugins', 'woo_multi_cache', 'elementor_addons_many', 'elementor_addons_several', 'elementor_woo_complexity', 'old_wp_version', 'core_update', 'plugin_updates', 'theme_update', 'old_php', 'upload_not_writable', 'search_visibility_disabled', 'debug_log_found', 'large_autoloaded_options', 'wp_debug_display', 'site_url_home_mismatch', 'no_object_cache_dropin', 'savequeries_enabled', 'development_settings_active', 'complex_plugin_setup', 'woo_background_tasks', 'default_admin_username', 'public_user_registration', 'woo_failed_scheduled_actions', 'woo_scheduled_actions_backlog', 'multiple_page_builders', 'core_database_update_required', 'woo_database_update_required', 'woo_checkout_page_missing', 'woo_checkout_page_not_public', 'woo_checkout_content_missing', 'object_cache_runtime_failure', ]; return in_array($issue['key'] ?? '', $high_confidence_keys, true) ? 'high' : 'medium'; } private function issue_simple_explanation($issue) { switch ($issue['key'] ?? 'generic') { case 'multi_cache': case 'no_cache': case 'no_cache_layer': case 'cache_signal_no_plugin': return __('Page caching stores reusable page responses. It can help performance, but overlapping plugins or leftover page-cache signals can make the setup unclear.', 'aifq-site-review'); case 'search_visibility_disabled': return __('This WordPress setting tells search engines not to index the site. That can be useful on staging sites, but risky on live websites.', 'aifq-site-review'); case 'plain_permalinks': return __('Readable permalinks are easier for visitors to understand and are usually cleaner for public content websites.', 'aifq-site-review'); case 'multi_seo': case 'no_seo': return __('An SEO plugin helps manage search basics such as metadata, sitemaps and page titles.', 'aifq-site-review'); case 'development_settings_active': return __('Debugging settings can help while troubleshooting, but they should usually be turned off or cleaned up again on a live website.', 'aifq-site-review'); case 'complex_plugin_setup': return __('A complex plugin setup is not automatically a problem, but it benefits from occasional review so maintenance and troubleshooting stay manageable.', 'aifq-site-review'); case 'file_editor': return __('WordPress can edit plugin and theme files from the dashboard. If an administrator account is compromised, that ability can make it easier to change site code.', 'aifq-site-review'); case 'action_scheduler_backlog': case 'action_scheduler_small_failure': return __('Scheduled background work helps plugins complete jobs outside normal page loads. A backlog or repeated failures can delay maintenance, emails or other automated tasks.', 'aifq-site-review'); case 'multiple_page_builders': return __('Using several page builders can be intentional, but it creates more styling systems and plugin interactions to maintain over time.', 'aifq-site-review'); case 'large_images': return __('Large image files can increase page weight and make pages slower to load, especially on mobile connections.', 'aifq-site-review'); case 'woo_background_tasks': return __('WooCommerce relies on scheduled background tasks for store maintenance and order-related work. Failed or delayed tasks can point to cron or Action Scheduler reliability issues.', 'aifq-site-review'); case 'multi_security': case 'no_security': case 'default_admin_username': case 'public_user_registration': return __('Security plugins and basic account settings can help reduce common automated abuse, but they are not guarantees.', 'aifq-site-review'); case 'woo_no_cache': case 'woo_multi_cache': case 'woo_many_plugins': case 'woo_no_mail_delivery_plugin': case 'woo_failed_scheduled_actions': case 'woo_scheduled_actions_backlog': return __('WooCommerce makes a website more complex than a normal brochure site. Email delivery, background tasks, plugin count and checkout behavior need extra care.', 'aifq-site-review'); case 'elementor_addons_many': case 'elementor_addons_several': case 'elementor_woo_complexity': return __('Elementor and WooCommerce can work well, but the combination adds frontend weight and more plugin interaction points.', 'aifq-site-review'); case 'large_autoloaded_options': return __('WordPress loads some saved settings on every page visit. On this site that data is larger than normal, often because of old plugins, page builders or webshop extensions. This can make the website or wp-admin feel slower.', 'aifq-site-review'); case 'debug_log_found': return __('A debug log can be useful for troubleshooting, but a non-empty log often means something is actively producing warnings or errors.', 'aifq-site-review'); case 'old_php': case 'old_php_updates': case 'low_memory': case 'memory_plugin_conflict': return __('PHP is the technology WordPress runs on. Outdated or limited PHP settings can cause slow pages or errors.', 'aifq-site-review'); case 'core_database_update_required': return __('WordPress sometimes needs to update its database structure after the core files change. The stored database version does not currently match what the loaded WordPress core expects.', 'aifq-site-review'); case 'woo_database_update_required': return __('WooCommerce sometimes needs to update stored database data after the plugin changes. WooCommerce currently reports that this database work is still required and no database-update task is queued.', 'aifq-site-review'); case 'woo_checkout_page_missing': return __('WooCommerce cannot currently resolve a usable Checkout page from its saved page assignment.', 'aifq-site-review'); case 'woo_checkout_page_not_public': return __('The assigned WooCommerce Checkout page exists, but it is not published for normal visitors.', 'aifq-site-review'); case 'woo_checkout_content_missing': return __('The assigned Checkout page is public, but WooCommerce does not detect its Checkout block or legacy checkout shortcode on that page.', 'aifq-site-review'); case 'object_cache_runtime_failure': return __('Persistent object caching can reduce repeated database work. Here, the cache provider itself reports that the integration failed to load or initialize correctly.', 'aifq-site-review'); case 'old_wp_version': return __('Older major WordPress versions can miss important security, compatibility and editor improvements.', 'aifq-site-review'); case 'core_update': case 'plugin_updates': case 'theme_update': return __('Updates fix bugs and improve security. Create a backup before updating many things at once.', 'aifq-site-review'); case 'xmlrpc': return __('XML-RPC is an older way for apps to communicate with WordPress. This check reflects the WordPress-side signal only; hosting, firewall or security layers may block it separately.', 'aifq-site-review'); case 'open_registration_privileged_role': return __('Public registration can be intentional, but giving every new account a privileged role can grant permissions that ordinary visitors should not receive.', 'aifq-site-review'); case 'no_ssl': return __('HTTPS protects login and admin traffic in transit. This WordPress-side signal can be misleading behind a trusted reverse proxy, so the public site should be verified before changing anything.', 'aifq-site-review'); default: return __('This can affect stability, security or speed.', 'aifq-site-review'); } } private function issue_if_ignored($issue) { $key = (string) ($issue['key'] ?? ''); switch ($key) { case 'search_visibility_disabled': return __('On a public production site, search engines are being asked not to index the site. On staging or development sites, this may be intentional.', 'aifq-site-review'); case 'no_ssl': return __('If HTTPS is already terminated by a trusted proxy and the public/admin pages are secure, no change may be needed. If the site is actually served over HTTP, login and admin traffic remain less protected.', 'aifq-site-review'); case 'wp_cron_disabled': return __('If an external server cron already runs WordPress scheduled tasks, this may be the intended setup. Without an external runner, scheduled work can be delayed or missed.', 'aifq-site-review'); case 'no_cache': case 'no_cache_layer': case 'woo_no_cache': return __('If hosting already provides appropriate page caching, no change may be needed. Without useful page caching, heavier sites can require more server work for repeated page requests.', 'aifq-site-review'); case 'core_database_update_required': return __('WordPress still considers its database upgrade incomplete. wp-admin may continue to ask for the database update until the stored database version matches the loaded core.', 'aifq-site-review'); case 'woo_database_update_required': return __('WooCommerce will continue to report that its database needs updating until the database update process completes.', 'aifq-site-review'); case 'woo_checkout_page_missing': return __('WooCommerce has no usable assigned Checkout page. Customers may be unable to reach the standard checkout flow until a valid page is restored or assigned.', 'aifq-site-review'); case 'woo_checkout_page_not_public': return __('The configured Checkout page remains unavailable to normal visitors until it is published or another public Checkout page is assigned.', 'aifq-site-review'); case 'woo_checkout_content_missing': return __('WooCommerce will continue to report that its standard Checkout block or shortcode is missing. A custom checkout may be intentional; otherwise the normal checkout form may not be available on the assigned page.', 'aifq-site-review'); case 'object_cache_runtime_failure': return __('The cache provider is still reporting an integration error. Depending on the failure, persistent object caching may remain unavailable or degraded until the provider files, configuration or backend service are repaired.', 'aifq-site-review'); case 'plugin_updates': case 'theme_update': case 'core_update': return __('Available fixes, compatibility updates or security improvements remain unapplied until the update is reviewed and installed.', 'aifq-site-review'); case 'file_editor': return __('The site can continue to work normally, but administrators retain the ability to edit plugin and theme code from the dashboard.', 'aifq-site-review'); case 'debug_on': case 'wp_debug_display': case 'development_settings_active': return __('The site may continue to work, but development output or extra diagnostic work can remain active on a live environment.', 'aifq-site-review'); case 'large_autoloaded_options': return __('The site may remain functional, but WordPress continues loading this data on normal requests, which can add avoidable database and memory work.', 'aifq-site-review'); case 'default_admin_username': return __('The account can continue to work, but the predictable username remains an easy target for automated login attempts.', 'aifq-site-review'); } if (($issue['severity'] ?? self::SEV_LOW) === self::SEV_HIGH) { return __('Leaving this unchanged keeps a meaningful reliability, security or maintenance concern in place.', 'aifq-site-review'); } if (($issue['severity'] ?? self::SEV_LOW) === self::SEV_MEDIUM) { return __('This does not always require immediate action, but leaving it unchanged can keep avoidable maintenance or troubleshooting risk in place.', 'aifq-site-review'); } return __('This is usually not urgent. Leaving it as-is may simply keep some extra complexity or maintenance overhead in the setup.', 'aifq-site-review'); } private function display_issue_title($issue) { switch ($issue['key'] ?? 'generic') { case 'development_settings_active': return __('Development settings are still active', 'aifq-site-review'); case 'complex_plugin_setup': return __('Complex plugin setup needs review', 'aifq-site-review'); case 'woo_background_tasks': return __('WooCommerce background tasks need review', 'aifq-site-review'); case 'savequeries_enabled': return __('Query logging is enabled', 'aifq-site-review'); case 'default_admin_username': return __('Default admin username exists', 'aifq-site-review'); case 'public_user_registration': return __('Public user registration is enabled', 'aifq-site-review'); case 'woo_no_mail_delivery_plugin': return __('No known email delivery plugin found for WooCommerce', 'aifq-site-review'); case 'woo_failed_scheduled_actions': return __('WooCommerce failed background tasks found', 'aifq-site-review'); case 'woo_scheduled_actions_backlog': return __('WooCommerce background task backlog found', 'aifq-site-review'); case 'multiple_page_builders': return __('Multiple page builders active', 'aifq-site-review'); case 'multi_cache': return __('Multiple caching plugins active', 'aifq-site-review'); case 'no_cache': return __('No local page-cache signal detected', 'aifq-site-review'); case 'no_cache_layer': return __('No local page-cache signal detected on this heavier setup', 'aifq-site-review'); case 'cache_signal_no_plugin': return __('Page-cache signal without active cache plugin', 'aifq-site-review'); case 'no_object_cache_dropin': return __('No persistent object cache detected', 'aifq-site-review'); case 'object_cache_runtime_failure': return __('Persistent object cache reported a failure', 'aifq-site-review'); case 'multi_seo': return __('Multiple SEO plugins active', 'aifq-site-review'); case 'no_seo': return __('No SEO plugin found', 'aifq-site-review'); case 'search_visibility_disabled': return __('Search engine visibility is disabled', 'aifq-site-review'); case 'action_scheduler_backlog': return __('Scheduled actions appear to be falling behind', 'aifq-site-review'); case 'action_scheduler_small_failure': return __('A small number of scheduled actions failed', 'aifq-site-review'); case 'open_registration_privileged_role': return __('New registrations receive a privileged role', 'aifq-site-review'); case 'pending_admin_email_change': return __('Site administrator email change is waiting for confirmation', 'aifq-site-review'); case 'wp_cron_disabled': return __('WordPress page-load cron is disabled', 'aifq-site-review'); case 'multiple_administrator_accounts': $administrator_count = 0; if (preg_match('/This site has (\d+) administrator accounts?/', (string) ($issue['title'] ?? ''), $matches)) { $administrator_count = (int) $matches[1]; } if ($administrator_count > 0) { return sprintf( /* translators: %d: number of administrator accounts */ _n('This site has %d administrator account', 'This site has %d administrator accounts', $administrator_count, 'aifq-site-review'), $administrator_count ); } return __('Several administrator accounts found', 'aifq-site-review'); case 'plain_permalinks': return __('Plain permalink structure detected', 'aifq-site-review'); case 'multi_security': return __('Multiple security plugins active', 'aifq-site-review'); case 'no_security': return __('No known security plugin found', 'aifq-site-review'); case 'heavy_plugins': return __('Several heavier plugins active', 'aifq-site-review'); case 'many_plugins': return __('Many active plugins found', 'aifq-site-review'); case 'elementor_addons_many': return __('Elementor with many add-ons', 'aifq-site-review'); case 'elementor_addons_several': return __('Elementor with several add-ons', 'aifq-site-review'); case 'woo_no_cache': return __('No local page-cache signal detected for WooCommerce', 'aifq-site-review'); case 'woo_many_plugins': return __('Many plugins active on this WooCommerce site', 'aifq-site-review'); case 'woo_multi_cache': return __('WooCommerce with multiple caching plugins', 'aifq-site-review'); case 'elementor_woo_complexity': return __('Elementor and WooCommerce complexity detected', 'aifq-site-review'); case 'large_images': return __('Several large images found', 'aifq-site-review'); case 'xmlrpc': return __('XML-RPC is available in WordPress', 'aifq-site-review'); case 'file_editor': return __('Dashboard file editing is enabled', 'aifq-site-review'); case 'file_mods_disabled': return __('File modifications are disabled', 'aifq-site-review'); case 'no_ssl': return __('HTTPS could not be confirmed by WordPress', 'aifq-site-review'); case 'site_url_home_mismatch': return __('WordPress Address and Site Address are different', 'aifq-site-review'); case 'core_database_update_required': return __('WordPress database update is required', 'aifq-site-review'); case 'woo_database_update_required': return __('WooCommerce database update is still required', 'aifq-site-review'); case 'woo_checkout_page_missing': return __('WooCommerce checkout page is missing', 'aifq-site-review'); case 'woo_checkout_page_not_public': return __('WooCommerce checkout page is not public', 'aifq-site-review'); case 'woo_checkout_content_missing': return __('WooCommerce checkout page is missing checkout content', 'aifq-site-review'); case 'old_wp_version': return __('Very old WordPress version', 'aifq-site-review'); case 'core_update': return __('WordPress core update available', 'aifq-site-review'); case 'plugin_updates': return __('Plugin updates available', 'aifq-site-review'); case 'theme_update': return __('Theme update available', 'aifq-site-review'); case 'inactive_plugins': return __('Several inactive plugins found', 'aifq-site-review'); case 'inactive_plugins_soft': return __('Several inactive plugins', 'aifq-site-review'); case 'inactive_themes': return __('Several inactive themes found', 'aifq-site-review'); case 'inactive_themes_soft': return __('Several inactive themes', 'aifq-site-review'); case 'no_backup': return __('No known backup plugin found', 'aifq-site-review'); case 'old_php': return __('Old PHP version', 'aifq-site-review'); case 'low_memory': return __('Low PHP memory limit', 'aifq-site-review'); case 'memory_plugin_conflict': return __('Many plugins with limited memory', 'aifq-site-review'); case 'old_php_updates': return __('Old PHP version with many updates', 'aifq-site-review'); case 'upload_not_writable': return __('Upload directory is not writable', 'aifq-site-review'); case 'debug_on': return __('WP_DEBUG is enabled', 'aifq-site-review'); case 'wp_debug_display': return __('Error display is enabled', 'aifq-site-review'); case 'debug_log_found': return __('Debug log file found', 'aifq-site-review'); case 'large_autoloaded_options': return __('More WordPress data loads automatically than usual', 'aifq-site-review'); case 'many_revisions': return __('Many revisions in the database', 'aifq-site-review'); default: return sanitize_text_field((string) ($issue['title'] ?? 'Attention point')); } } private function display_issue_recommendation($issue) { switch ($issue['key'] ?? 'generic') { case 'development_settings_active': return __('Ask your developer or hosting provider to review the errors, turn off debugging settings when troubleshooting is complete, and clear the log only after the underlying issue is resolved.', 'aifq-site-review'); case 'complex_plugin_setup': return __('Review the plugin setup with your developer. Focus on whether all active plugins are still needed and whether any plugins overlap.', 'aifq-site-review'); case 'woo_background_tasks': return __('Ask your developer or hosting provider to check WooCommerce scheduled actions and WP-Cron reliability.', 'aifq-site-review'); case 'savequeries_enabled': return __('Ask your developer to disable SAVEQUERIES after debugging is complete.', 'aifq-site-review'); case 'default_admin_username': return __('Ask your developer to replace the default admin username safely. Do not delete the admin account directly; create a new administrator account first and reassign content if needed.', 'aifq-site-review'); case 'public_user_registration': return __('If your site does not need public accounts, consider turning registration off in Settings → General. If you run a membership, community, shop or learning site, this may be intentional.', 'aifq-site-review'); case 'woo_no_mail_delivery_plugin': return __('Check how WooCommerce emails are sent on this site. Your hosting provider may already handle email delivery, but many shops use an SMTP or transactional email delivery plugin for more reliable delivery.', 'aifq-site-review'); case 'woo_failed_scheduled_actions': return __('Ask your developer to review WooCommerce scheduled actions and check whether failed tasks are still relevant.', 'aifq-site-review'); case 'woo_scheduled_actions_backlog': return __('Ask your developer or hosting provider to check whether WP-Cron and scheduled actions are running normally.', 'aifq-site-review'); case 'multiple_page_builders': return __('If this is not part of a migration or redesign, ask your developer to review which page builder should be the main solution.', 'aifq-site-review'); case 'multi_cache': return __('Use one caching solution and deactivate duplicate optimization plugins.', 'aifq-site-review'); case 'no_cache': return __('Verify whether page caching is already handled by your hosting platform. A separate WordPress page-cache plugin is not automatically required.', 'aifq-site-review'); case 'no_cache_layer': return __('Verify whether page caching is already handled by your hosting platform. If server- or edge-level page caching is active, no additional WordPress page-cache plugin may be needed.', 'aifq-site-review'); case 'cache_signal_no_plugin': return __('Check whether page caching is managed by hosting, WP_CACHE or an advanced-cache.php drop-in left by an earlier cache setup.', 'aifq-site-review'); case 'no_object_cache_dropin': return __('Ask your hosting provider whether object caching is available and useful for this site. Object caching is separate from normal page caching plugins and is not always needed.', 'aifq-site-review'); case 'object_cache_runtime_failure': return __('Ask your developer or hosting provider to verify the object-cache provider files, configuration and backend service. If your host manages object caching, repair or re-enable it through the hosting layer instead of deleting object-cache.php manually.', 'aifq-site-review'); case 'action_scheduler_backlog': return __('Ask your developer or hosting provider to review Scheduled Actions, starting with repeated failures and the oldest overdue task.', 'aifq-site-review'); case 'action_scheduler_small_failure': return __('Review the failed scheduled actions if the count continues to grow or related functionality is not completing as expected.', 'aifq-site-review'); case 'open_registration_privileged_role': return __('Review Settings → General. Keep registration enabled if the site needs it, but choose a role with only the permissions new users require.', 'aifq-site-review'); case 'pending_admin_email_change': return __('Confirm the change from the email WordPress sent, or return to Settings → General if the requested address is no longer correct.', 'aifq-site-review'); case 'wp_cron_disabled': return __('Confirm that the hosting platform or server calls wp-cron.php on a schedule. No change is needed when an external cron job is already configured.', 'aifq-site-review'); case 'multiple_administrator_accounts': return __('Check whether each administrator still needs full access. Keep legitimate team and service accounts in place.', 'aifq-site-review'); case 'search_visibility_disabled': return __('For a live public website, turn search engine visibility back on in Settings → Reading. While this is off, search engines are asked not to index the site.', 'aifq-site-review'); case 'plain_permalinks': return __('If this is a new site, consider using a readable permalink structure such as Post name. If this is an existing live site, changing permalinks can break existing links unless redirects are handled correctly. Ask your developer first if the site already has published content.', 'aifq-site-review'); case 'multi_seo': return __('Choose one SEO plugin as the main solution and remove or deactivate the rest.', 'aifq-site-review'); case 'no_seo': return __('Verify how SEO basics are managed if search visibility matters. Plugin presence alone is not required.', 'aifq-site-review'); case 'multi_security': return __('Use one main security plugin and avoid duplicate firewall or login protection features.', 'aifq-site-review'); case 'no_security': return __('Verify what protection is already provided by your hosting, firewall or other security tooling before adding another plugin.', 'aifq-site-review'); case 'heavy_plugins': return __('Review whether each heavier plugin still has a clear purpose and avoid overlapping plugins that solve the same problem.', 'aifq-site-review'); case 'many_plugins': return __('Review whether all active plugins are still needed, maintained and not overlapping with another plugin.', 'aifq-site-review'); case 'elementor_addons_many': case 'elementor_addons_several': return __('Review whether each Elementor add-on is still needed and avoid multiple add-ons that provide similar widgets.', 'aifq-site-review'); case 'woo_no_cache': return __('Check with your hosting provider first — server- or edge-level page caching may already be active. If not, use one WooCommerce-safe page-cache solution and exclude cart, checkout and account pages from aggressive caching.', 'aifq-site-review'); case 'woo_many_plugins': return __('Review the WooCommerce plugins in a controlled way. Check whether any plugins overlap, are no longer needed, or should be tested in staging before changes are made.', 'aifq-site-review'); case 'woo_multi_cache': return __('Use one main caching solution and make sure cart, checkout and account pages are excluded from aggressive caching.', 'aifq-site-review'); case 'elementor_woo_complexity': return __('Keep updates controlled, test important changes first and avoid adding new plugins for small features unless really needed.', 'aifq-site-review'); case 'large_images': return __('Optimize large images or use reliable image compression. Make a backup first if you are replacing existing media files.', 'aifq-site-review'); case 'xmlrpc': return __('If you do not use XML-RPC, ask your developer or hosting provider whether it is already blocked at server level or should be disabled safely. If you use the WordPress mobile app, Jetpack or external publishing tools, this may be intentional.', 'aifq-site-review'); case 'file_editor': return __('Ask your developer to disable dashboard file editing safely in wp-config.php.', 'aifq-site-review'); case 'file_mods_disabled': return __('Ask your developer or web host whether DISALLOW_FILE_MODS is intentionally enabled. If updates are managed elsewhere, this may be fine.', 'aifq-site-review'); case 'no_ssl': return __('Verify the public site and WordPress admin over HTTPS. If a reverse proxy or load balancer is used, confirm that WordPress receives the correct HTTPS information.', 'aifq-site-review'); case 'site_url_home_mismatch': return __('Do not change these values unless you know why they differ. If this was not intentional, ask your developer or hosting provider to review the URL settings.', 'aifq-site-review'); case 'core_database_update_required': return __('Create or verify a recent backup, then complete the WordPress database update from the WordPress upgrade screen. If it does not complete normally, ask your developer or hosting provider to investigate.', 'aifq-site-review'); case 'woo_database_update_required': return __('Create or verify a recent backup, then start the WooCommerce database update. If the update does not start or complete normally, ask your developer to review WooCommerce Scheduled Actions and the update process.', 'aifq-site-review'); case 'woo_checkout_page_missing': return __('Create or restore a Checkout page, add the WooCommerce Checkout block or [woocommerce_checkout] shortcode, then assign that page under WooCommerce Settings → Advanced.', 'aifq-site-review'); case 'woo_checkout_page_not_public': return __('Publish or restore the configured Checkout page, or assign another published Checkout page under WooCommerce Settings → Advanced.', 'aifq-site-review'); case 'woo_checkout_content_missing': return __('If this store uses WooCommerce’s standard checkout page, restore the Checkout block or [woocommerce_checkout] shortcode. If a custom checkout intentionally replaces the standard page content, verify that flow before changing the page.', 'aifq-site-review'); case 'old_wp_version': return __('Plan a controlled WordPress update with a backup first.', 'aifq-site-review'); case 'core_update': return __('Create a backup first and then review the WordPress core update currently reported. On WooCommerce or complex sites, consider testing updates in staging or asking your developer.', 'aifq-site-review'); case 'plugin_updates': return __('Create a backup first and then review the plugin updates currently reported by WordPress. On WooCommerce or complex sites, consider testing updates in staging or asking your developer.', 'aifq-site-review'); case 'theme_update': return __('Create a backup first and then review the theme updates currently reported by WordPress. On WooCommerce or complex sites, consider testing updates in staging or asking your developer.', 'aifq-site-review'); case 'inactive_plugins': return __('Review inactive plugins and remove the ones you no longer need. Keep a backup first if you are unsure what a plugin does.', 'aifq-site-review'); case 'inactive_plugins_soft': return __('Review your inactive plugins. If you no longer use them, removing them keeps your installation lean and easier to maintain.', 'aifq-site-review'); case 'inactive_themes': return __('Keep the active theme and, if useful, one fallback theme. Remove unused themes after confirming they are not needed.', 'aifq-site-review'); case 'inactive_themes_soft': return __('Keep the active theme and, if useful, one fallback theme. Remove unused themes after confirming they are not needed.', 'aifq-site-review'); case 'no_backup': return __('Verify that a recent backup exists and that you know how it can be restored. The backup may be provided by hosting or another service.', 'aifq-site-review'); case 'old_php': return __('Ask your hosting provider to move the site to a currently supported PHP version when the site is compatible.', 'aifq-site-review'); case 'low_memory': return __('Ask your hosting provider to increase the PHP memory limit. For larger or more dynamic WordPress setups, 256 MB or more is often a better baseline.', 'aifq-site-review'); case 'memory_plugin_conflict': return __('Consider increasing the memory limit and removing plugins that are no longer needed.', 'aifq-site-review'); case 'old_php_updates': return __('Ask your developer or hosting provider to test the PHP and plugin updates carefully after making a backup.', 'aifq-site-review'); case 'upload_not_writable': return __('Ask your developer or web host to check the file permissions for wp-content/uploads.', 'aifq-site-review'); case 'debug_on': return __('Ask your developer to disable WP_DEBUG on live websites.', 'aifq-site-review'); case 'wp_debug_display': return __('Ask your developer to disable WP_DEBUG_DISPLAY on the live site.', 'aifq-site-review'); case 'debug_log_found': return __('Ask your developer or hosting provider to review the WordPress error log, investigate what is causing the errors, and clear the log only after the underlying issue is resolved.', 'aifq-site-review'); case 'large_autoloaded_options': return __('Do not delete database data yourself unless you know exactly what it is. This usually needs a careful review of plugin data, not a quick delete-all cleanup. Ask your developer, hosting provider or website manager to check the largest autoloaded options safely.', 'aifq-site-review'); case 'many_revisions': return __('Make a backup first. Then clean up old revisions with a trusted database optimization tool, or ask your developer to do this safely.', 'aifq-site-review'); default: return sanitize_text_field((string) ($issue['recommendation'] ?? __('Review this point.', 'aifq-site-review'))); } } private function clean_context_text($context) { return sanitize_text_field((string) $context); } private function display_passed_title($point) { $title = isset($point['title']) ? (string) $point['title'] : ''; $map = [ 'Active caching plugin found' => 'Active caching plugin found', 'Object cache drop-in found' => 'Persistent object caching is active', 'Caching signal found' => 'Caching signal found', 'One SEO plugin active' => 'One SEO plugin active', 'Search engine visibility is enabled' => 'Search engine visibility is enabled', 'Readable permalink structure active' => 'Readable permalink structure is active', 'One security plugin active' => 'One security plugin active', 'Default admin username not found' => 'Default admin username not found', 'Public user registration is disabled' => 'Public user registration is disabled', 'XML-RPC appears disabled' => 'XML-RPC is disabled in WordPress', 'XML-RPC is disabled in WordPress' => 'XML-RPC is disabled in WordPress', 'Dashboard file editing is disabled' => 'Dashboard file editing is disabled', 'WordPress core appears up to date' => 'WordPress core appears up to date', 'Plugins appear up to date' => 'Plugins appear up to date', 'Theme appears up to date' => 'Theme appears up to date', 'Inactive plugin count is not high' => 'Inactive plugin count is not high', 'Inactive theme count is not high' => 'Inactive theme count is not high', 'Backup solution found' => 'Backup-related plugin found', 'Backup-related plugin found' => 'Backup-related plugin found', 'PHP version is modern enough' => 'PHP version is up-to-date', 'No active debug log found' => 'No active debug log found', 'Autoloaded options size looks normal' => 'Autoloaded options size looks normal', 'PHP memory limit appears sufficient' => 'PHP memory limit appears sufficient', 'Upload directory is writable' => 'Upload directory is writable', 'Debug mode is disabled' => 'Debug mode is disabled', 'Revision count is not high' => 'Revision count is not high', ]; return $map[$title] ?? $title; } private function display_passed_description($point) { $title = (string) ($point['title'] ?? ''); $description = (string) ($point['description'] ?? ''); $map = [ 'Caching signal found' => 'A caching signal or WP_CACHE setting was found.', 'Active caching plugin found' => 'One active caching plugin was found.', 'Object cache drop-in found' => 'Persistent object caching appears to be active. This is separate from normal page caching.', 'One SEO plugin active' => 'One SEO plugin was found.', 'Search engine visibility is enabled' => 'WordPress is not discouraging search engines from indexing the site.', 'Readable permalink structure active' => 'A custom permalink structure is configured.', 'Active plugin count looks normal' => 'The number of active plugins is below the attention threshold.', 'Heavy plugins limited' => 'Some resource-heavy plugins are present, but not enough to flag as a setup-complexity signal.', 'No known heavy plugins found' => 'No plugins from the resource-heavy plugin list were found.', 'One security plugin active' => 'One security plugin was found.', 'Default admin username not found' => 'No user with the username admin was found.', 'Public user registration is disabled' => 'Visitors cannot create WordPress accounts through the default registration setting.', 'XML-RPC appears disabled' => 'The WordPress-side XML-RPC signal is disabled.', 'XML-RPC is disabled in WordPress' => 'The WordPress-side XML-RPC signal is disabled.', 'Dashboard file editing is disabled' => 'This is safer if an admin account is ever compromised.', 'HTTPS is active' => 'WordPress admin appears to be running over HTTPS in this context.', 'WordPress core appears up to date' => 'No pending WordPress core update is currently reported.', 'Plugins appear up to date' => 'No pending plugin updates are currently reported by WordPress.', 'Theme appears up to date' => 'No pending theme updates are currently reported by WordPress.', 'Inactive plugin count is not high' => 'The number of inactive plugins is not unusually high.', 'Inactive theme count is not high' => 'The number of inactive themes is not unusually high.', 'Backup solution found' => 'A backup-related plugin is active. Verify that backups are actually running and can be restored.', 'Backup-related plugin found' => 'A backup-related plugin is active. Verify that backups are actually running and can be restored.', 'Upload directory is writable' => 'WordPress can save files in the uploads directory.', 'Debug mode is disabled' => 'Good for a normal live website.', 'No active debug log found' => 'No non-empty wp-content/debug.log file was found.', 'Autoloaded options size looks normal' => 'Autoloaded option data is below the attention threshold.', 'Revision count is not high' => 'The database does not appear to contain an unusually high number of old revisions.', 'No unusually high number of large images' => 'Fewer than 10 large images were found in the checked recent media sample. The full media library was not scanned.', 'PHP version is modern enough' => 'Your site is running on a modern and well-supported PHP version.', ]; return $map[$title] ?? $this->clean_context_text($description); } // ------------------------------------------------------------------------- // Plugin detection lists // ------------------------------------------------------------------------- private function plugin_names(array $plugins) { return array_values($plugins); } // ------------------------------------------------------------------------- // Low-level data helpers // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- // Since last scan: comparison helper // ------------------------------------------------------------------------- private function diff_represented_keys(array $item) { $key = isset($item['key']) ? (string) $item['key'] : $this->title_to_key((string) ($item['title'] ?? '')); $keys = []; if (!empty($item['cluster_child_keys']) && is_array($item['cluster_child_keys'])) { foreach ($item['cluster_child_keys'] as $child_key) { $child_key = sanitize_key((string) $child_key); if ($child_key !== '') { $keys[$child_key] = true; } } } if ($key === 'complex_plugin_setup' && empty($keys)) { foreach (['many_plugins', 'woo_many_plugins', 'heavy_plugins', 'elementor_addons_many', 'elementor_woo_complexity'] as $child_key) { $keys[$child_key] = true; } } elseif ($key === 'woo_background_tasks' && empty($keys)) { foreach (['woo_failed_scheduled_actions', 'woo_scheduled_actions_backlog'] as $child_key) { $keys[$child_key] = true; } } elseif ($key === 'development_settings_active' && empty($keys)) { foreach (['wp_debug_display', 'debug_on', 'savequeries_enabled', 'debug_log_found'] as $child_key) { $keys[$child_key] = true; } } if ($key !== '' && empty($keys)) { $keys[$key] = true; } return array_keys($keys); } private function diff_items_by_represented_key(array $items) { $map = []; foreach ($items as $item) { if (!is_array($item)) { continue; } foreach ($this->diff_represented_keys($item) as $key) { $map[$key] = $item; } } return $map; } private function unique_diff_items(array $items) { $unique = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $key = isset($item['key']) ? (string) $item['key'] : $this->title_to_key((string) ($item['title'] ?? '')); if ($key === '') { $key = md5(wp_json_encode($item)); } $unique[$key] = $item; } return array_values($unique); } /** * Compare the current scan with the previous scan. * * Returns null when comparison is not safe or not meaningful. * Uses stable issue keys (not titles) for counting changes. * Designed to be forward-compatible with future scan history. * * @param array $current_scan Normalized current scan array. * @param array $previous_scan Normalized previous scan array. * @return array|null Diff array or null if comparison cannot be made. */ private function calculate_scan_diff(array $current_scan, array $previous_scan) { $prev_date = (string) ($previous_scan['created_at'] ?? ''); $curr_date = (string) ($current_scan['created_at'] ?? ''); if ($prev_date === '' || $curr_date === '' || $prev_date === $curr_date) { return null; } $prev_time = $this->scan_timestamp($prev_date); $curr_time = $this->scan_timestamp($curr_date); if (!$prev_time || !$curr_time || $prev_time >= $curr_time) { return null; } $prev_version = (string) ($previous_scan['version'] ?? '0.0.0'); if (version_compare($prev_version, '1.1.0', '<')) { return null; } // 1.10.1 deliberately changes finding and comparison semantics. The first // 1.10.1 review becomes a clean reference point so a plugin update is // not presented as a change that happened to the site itself. if (version_compare((string) ($current_scan['version'] ?? self::VERSION), '1.10.1', '>=') && version_compare($prev_version, '1.10.1', '<')) { return null; } $prev_issues = (isset($previous_scan['issues']) && is_array($previous_scan['issues'])) ? $previous_scan['issues'] : []; $curr_issues = (isset($current_scan['issues']) && is_array($current_scan['issues'])) ? $current_scan['issues'] : []; $prev_issues_by_key = $this->diff_items_by_represented_key($prev_issues); $curr_issues_by_key = $this->diff_items_by_represented_key($curr_issues); $resolved_issue_objs = $this->unique_diff_items(array_values(array_intersect_key( $prev_issues_by_key, array_diff_key($prev_issues_by_key, $curr_issues_by_key) ))); $new_issue_objs = $this->unique_diff_items(array_values(array_intersect_key( $curr_issues_by_key, array_diff_key($curr_issues_by_key, $prev_issues_by_key) ))); $still_present_issue_objs = $this->unique_diff_items(array_values(array_intersect_key( $curr_issues_by_key, $prev_issues_by_key ))); $prev_suggestions = (isset($previous_scan['suggestions']) && is_array($previous_scan['suggestions'])) ? $previous_scan['suggestions'] : []; $curr_suggestions = (isset($current_scan['suggestions']) && is_array($current_scan['suggestions'])) ? $current_scan['suggestions'] : []; $prev_suggestions_by_key = []; foreach ($prev_suggestions as $suggestion) { $key = (string) ($suggestion['key'] ?? ''); if ($key !== '') { $prev_suggestions_by_key[$key] = $suggestion; } } $curr_suggestions_by_key = []; foreach ($curr_suggestions as $suggestion) { $key = (string) ($suggestion['key'] ?? ''); if ($key !== '') { $curr_suggestions_by_key[$key] = $suggestion; } } $resolved_suggestion_objs = array_values(array_intersect_key( $prev_suggestions_by_key, array_diff_key($prev_suggestions_by_key, $curr_suggestions_by_key) )); $new_suggestion_objs = array_values(array_intersect_key( $curr_suggestions_by_key, array_diff_key($curr_suggestions_by_key, $prev_suggestions_by_key) )); return [ 'is_comparable' => true, 'previous_date' => $prev_date, 'new_issues_count' => count($new_issue_objs), 'resolved_issues_count' => count($resolved_issue_objs), 'still_present_issues_count' => count($still_present_issue_objs), 'new_suggestions_count' => count($new_suggestion_objs), 'resolved_suggestions_count' => count($resolved_suggestion_objs), 'new_issues' => $new_issue_objs, 'resolved_issues' => $resolved_issue_objs, 'still_present_issues' => $still_present_issue_objs, 'new_suggestions' => $new_suggestion_objs, 'resolved_suggestions' => $resolved_suggestion_objs, ]; } private function memory_to_mb($value) { $value = trim((string) $value); $unit = strtolower(substr($value, -1)); $number = (int) $value; if ($number === -1) { return PHP_INT_MAX; } switch ($unit) { case 'g': return $number * 1024; case 'm': return $number; case 'k': return (int) ($number / 1024); default: return (int) ($number / 1024 / 1024); } } } add_action('plugins_loaded', function() { new AIFQ_SR_Plugin(); });