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'), ] ); } 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 ); } private function normalize_scan($scan) { if (!is_array($scan)) { return false; } $stored_version = isset($scan['version']) ? (string) $scan['version'] : '0.0.0'; if (version_compare($stored_version, '0.9.8.6', '<')) { return false; } $normalized = [ 'version' => sanitize_text_field((string) ($scan['version'] ?? self::VERSION)), 'score' => max(0, min(100, (int) ($scan['score'] ?? 100))), 'subscores' => [], 'issues' => [], 'suggestions' => [], 'passed' => [], 'signals' => [], 'created_at' => sanitize_text_field((string) ($scan['created_at'] ?? '')), ]; $default_subscores = ['Performance' => 100, 'Security' => 100, 'Maintenance' => 100, 'Health' => 100]; $subscores = isset($scan['subscores']) && is_array($scan['subscores']) ? $scan['subscores'] : []; foreach ($default_subscores as $category => $default_score) { $normalized['subscores'][$category] = max(0, min(100, (int) ($subscores[$category] ?? $default_score))); } if (isset($scan['issues']) && is_array($scan['issues'])) { foreach ($scan['issues'] as $issue) { $clean = $this->normalize_issue($issue); if ($clean) { $normalized['issues'][] = $clean; } } } if (isset($scan['suggestions']) && is_array($scan['suggestions'])) { foreach ($scan['suggestions'] as $suggestion) { $clean = $this->normalize_issue($suggestion); if ($clean) { $clean['type'] = 'suggestion'; $normalized['suggestions'][] = $clean; } } } if (isset($scan['passed']) && is_array($scan['passed'])) { foreach ($scan['passed'] as $passed) { if (!is_array($passed)) { continue; } $normalized['passed'][] = [ 'category' => sanitize_text_field((string) ($passed['category'] ?? '')), 'title' => sanitize_text_field((string) ($passed['title'] ?? '')), 'description' => sanitize_text_field((string) ($passed['description'] ?? '')), ]; } } foreach (['signals'] as $list_key) { $items = isset($scan[$list_key]) && is_array($scan[$list_key]) ? $scan[$list_key] :[]; foreach ($items as $item) { if (is_scalar($item)) { $normalized[$list_key][] = sanitize_text_field((string) $item); } } } return $normalized; } private function normalize_issue($issue) { if (!is_array($issue)) { return false; } $severity = sanitize_text_field((string) ($issue['severity'] ?? self::SEV_LOW)); $legacy_severity_map = [ 'Hoog' => self::SEV_HIGH, 'Middel' => self::SEV_MEDIUM, 'Laag' => self::SEV_LOW, ]; if (isset($legacy_severity_map[$severity])) { $severity = $legacy_severity_map[$severity]; } if (!in_array($severity, [self::SEV_HIGH, self::SEV_MEDIUM, self::SEV_LOW], true)) { $severity = self::SEV_LOW; } $type = sanitize_key((string) ($issue['type'] ?? 'attention')); if (!in_array($type, ['attention', 'suggestion', 'opportunity'], true)) { $type = 'attention'; } $title = sanitize_text_field((string) ($issue['title'] ?? 'Attention point')); $key = sanitize_key((string) ($issue['key'] ?? '')); if ($key === '') { $key = $this->title_to_key($title); } elseif ($key === 'elementor_addons') { // 1.3.0: old scans used one shared key and disambiguated by the stored English title. // Resolve it to the new stable key so display and i18n remain fully key-based. $key = $this->title_to_key($title); } $clean = [ 'type' => $type, 'category' => sanitize_text_field((string) ($issue['category'] ?? 'Maintenance')), 'title' => $title, 'key' => $key, 'severity' => $severity, 'explanation' => sanitize_text_field((string) ($issue['explanation'] ?? '')), 'recommendation' => sanitize_text_field((string) ($issue['recommendation'] ?? '')), 'context' => sanitize_text_field((string) ($issue['context'] ?? '')), ]; $clean['confidence'] = $this->issue_confidence($clean); $clean['time_estimate'] = $this->issue_time_estimate($clean); $clean['simple_explanation'] = $this->issue_simple_explanation($clean); $clean['if_ignored'] = $this->issue_if_ignored($clean); return $clean; } 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(get_option(self::OPTION_LAST_SCAN)); $previous_scan = $this->normalize_scan(get_option(self::OPTION_PREVIOUS_SCAN)); ?>

clean_signal_label($item)); ?>

finalize_scan_items($issues, $suggestions, $passed); $issues = $final_items['issues']; $suggestions = $final_items['suggestions']; $passed = $final_items['passed']; $previous_score = null; if (is_array($previous_scan)) { $previous_subscores = (isset($previous_scan['subscores']) && is_array($previous_scan['subscores'])) ? $previous_scan['subscores'] : null; $previous_score = $previous_subscores ? $this->calculate_total_score($previous_subscores) : (isset($previous_scan['score']) ? max(0, min(100, (int) $previous_scan['score'])) : null); } $summary = $this->get_issue_summary($issues); $grouped = $this->group_issues_by_category($issues); $grouped_suggestions = $this->group_issues_by_category($suggestions); $grouped_passed = $this->group_passed_by_category($passed); $subscores = (isset($scan['subscores']) && is_array($scan['subscores'])) ? $scan['subscores'] : $this->calculate_subscores($issues); $score = $this->calculate_total_score($subscores); $scan['score'] = $score; $potential = $this->calculate_potential_score($score, $issues); if ($previous_score !== null && $previous_score < $score) { printf( '
%s %s
', esc_html__('↗ Good progress.', 'aifq-site-review'), esc_html(sprintf( /* translators: 1: previous score, 2: new score. */ __('Your score increased from %1$d to %2$d.', 'aifq-site-review'), (int) $previous_score, (int) $score )) ); } $first_actions = $this->get_first_actions($issues, 3); $first_suggestions = $this->get_first_suggestions($suggestions, 3); $top_suggestions_count = count($first_suggestions); $total_suggestions_count = count($suggestions); $remaining_suggestions_count = max(0, $total_suggestions_count - $top_suggestions_count); $has_suggestions = !empty($suggestions); $remaining_actions_count = max(0, count($issues) - count($first_actions)); $good_points = $this->get_good_points($passed, max(1, count($passed))); $scan_diff = (is_array($previous_scan)) ? $this->calculate_scan_diff($scan, $previous_scan) : null; $copy_summary = $this->build_copy_summary($scan, $issues, $suggestions, $first_actions, $scan_diff); $developer_message = $this->build_developer_host_message($scan, $issues, $first_suggestions, $first_actions); ?>
score_status_indicator_html($score), $this->safe_ui_kses_args()); ?> score_status_label($score)); ?>

format_scan_date($scan['created_at']) )); ?>

render_scan_diff($scan_diff); ?>

get_human_summary($summary, $grouped, $score, $potential)); ?>

0) ? __('Recommended first step:', 'aifq-site-review') : __('Recommended next step:', 'aifq-site-review')); ?>
short_action_title($first_actions[0])); ?>
short_action_reason($first_actions[0]) )); ?>
$mini_score): ?>
category_label_html($mini_category, 'mini'), $this->safe_ui_kses_args()); ?> /100

    $issue): ?>
  1. display_issue_title($issue)); ?> display_issue_recommendation($issue)); ?>
0): ?>

0): ?>

  • display_issue_title($suggestion)); ?> display_issue_recommendation($suggestion)); ?>

render_category_card('Performance', $grouped, $grouped_suggestions, $grouped_passed); ?> render_category_card('Security', $grouped, $grouped_suggestions, $grouped_passed); ?> render_category_card('Maintenance', $grouped, $grouped_suggestions, $grouped_passed); ?> render_category_card('Health', $grouped, $grouped_suggestions, $grouped_passed); ?>

  • display_passed_title($point)); ?>
    display_passed_description($point)); ?>
score_status_label((int) $score); $lines[] = sprintf( /* translators: 1: score, 2: score status label */ __('Score: %1$d/100 — %2$s', 'aifq-site-review'), (int) $score, $status_label ); if (!empty($scan['created_at'])) { $lines[] = sprintf( /* translators: %s: formatted scan date */ __('Last scan: %s', 'aifq-site-review'), $this->format_scan_date($scan['created_at']) ); } $lines[] = ''; $lines[] = __('Since last scan:', 'aifq-site-review'); if (is_array($scan_diff) && !empty($scan_diff['is_comparable'])) { $delta = (int) $scan_diff['score_delta']; $prev_score = (int) $scan_diff['previous_score']; $prev_date = $this->format_scan_date($scan_diff['previous_date']); if ($delta > 0) { $lines[] = sprintf( /* translators: 1: previous score, 2: current score, 3: score delta, 4: previous scan date */ __('- Score improved from %1$d to %2$d (+%3$d points) since %4$s.', 'aifq-site-review'), $prev_score, (int) $score, $delta, $prev_date ); } elseif ($delta < 0) { $lines[] = sprintf( /* translators: 1: previous score, 2: current score, 3: negative score delta, 4: previous scan date */ __('- Score decreased from %1$d to %2$d (%3$d points) since %4$s.', 'aifq-site-review'), $prev_score, (int) $score, $delta, $prev_date ); } else { $lines[] = sprintf( /* translators: 1: current score, 2: previous scan date */ __('- Score stayed the same at %1$d since %2$s.', 'aifq-site-review'), (int) $score, $prev_date ); } $new_issues_count = (int) ($scan_diff['new_issues_count'] ?? 0); $res_issues_count = (int) ($scan_diff['resolved_issues_count'] ?? 0); $new_sugg_count = (int) ($scan_diff['new_suggestions_count'] ?? 0); $res_sugg_count = (int) ($scan_diff['resolved_suggestions_count'] ?? 0); if ($new_issues_count > 0) { $lines[] = '- ' . sprintf( /* translators: %d: number of new attention points */ _n('%d new attention point found.', '%d new attention points found.', $new_issues_count, 'aifq-site-review'), $new_issues_count ); } if ($res_issues_count > 0) { $lines[] = '- ' . sprintf( /* translators: %d: number of attention points that appear resolved */ _n('%d attention point appears resolved.', '%d attention points appear resolved.', $res_issues_count, 'aifq-site-review'), $res_issues_count ); } if ($new_sugg_count > 0) { $lines[] = '- ' . sprintf( /* translators: %d: number of new opportunities */ _n('%d new opportunity found.', '%d new opportunities found.', $new_sugg_count, 'aifq-site-review'), $new_sugg_count ); } if ($res_sugg_count > 0) { $lines[] = '- ' . sprintf( /* translators: %d: number of opportunities that appear resolved */ _n('%d opportunity appears resolved.', '%d opportunities appear resolved.', $res_sugg_count, 'aifq-site-review'), $res_sugg_count ); } if ($new_issues_count === 0 && $res_issues_count === 0 && $new_sugg_count === 0 && $res_sugg_count === 0 && $delta === 0) { $lines[] = __('- No major changes since the previous scan.', 'aifq-site-review'); } $view_resolved_issues = (isset($scan_diff['resolved_issues']) && is_array($scan_diff['resolved_issues'])) ? $scan_diff['resolved_issues'] : []; $view_new_issues = (isset($scan_diff['new_issues']) && is_array($scan_diff['new_issues'])) ? $scan_diff['new_issues'] : []; $view_resolved_suggestions = (isset($scan_diff['resolved_suggestions']) && is_array($scan_diff['resolved_suggestions'])) ? $scan_diff['resolved_suggestions'] : []; $view_new_suggestions = (isset($scan_diff['new_suggestions']) && is_array($scan_diff['new_suggestions'])) ? $scan_diff['new_suggestions'] : []; $change_sections = [ ['label' => __('Resolved attention points:', 'aifq-site-review'), 'items' => $view_resolved_issues], ['label' => __('New attention points:', 'aifq-site-review'), 'items' => $view_new_issues], ['label' => __('Opportunities that appear resolved:', 'aifq-site-review'), 'items' => $view_resolved_suggestions], ['label' => __('New opportunities:', 'aifq-site-review'), 'items' => $view_new_suggestions], ]; foreach ($change_sections as $section) { if (empty($section['items'])) { continue; } $lines[] = ''; $lines[] = $section['label']; $shown = 0; foreach ($section['items'] as $item) { if ($shown >= 5) break; $title = $this->display_issue_title($item); if ($title !== '') { $lines[] = '- ' . $title; $shown++; } } if (count($section['items']) > $shown) { $lines[] = sprintf( /* translators: %d: number of hidden items */ __('- + %d more in AIFQ Site Review.', 'aifq-site-review'), count($section['items']) - $shown ); } } } else { $lines[] = __('- No previous scan to compare yet.', 'aifq-site-review'); } $lines[] = ''; if (!empty($first_actions[0])) { $main = $first_actions[0]; $lines[] = __('Main attention point:', 'aifq-site-review'); $lines[] = '- ' . $this->display_issue_title($main) . ': ' . $this->display_issue_recommendation($main); } else { $lines[] = __('Main attention point: none found.', 'aifq-site-review'); } $lines[] = ''; $summary_issues = $issues; $has_main_attention_point = !empty($first_actions[0]); if ($has_main_attention_point) { $main_key = $first_actions[0]['key'] ?? ''; $summary_issues = array_values(array_filter($issues, function ($issue) use ($main_key) { return ($issue['key'] ?? '') !== $main_key; })); } if (!empty($summary_issues)) { $lines[] = sprintf( /* translators: %d: number of attention points */ $has_main_attention_point ? __('Other attention points (%d):', 'aifq-site-review') : __('Attention points (%d):', 'aifq-site-review'), count($summary_issues) ); foreach (array_slice($summary_issues, 0, 10) as $issue) { $lines[] = '- ' . $this->display_issue_title($issue) . ': ' . $this->display_issue_recommendation($issue); } if (count($summary_issues) > 10) { $lines[] = sprintf( /* translators: %d: number of hidden attention points */ __('- + %d more in AIFQ Site Review.', 'aifq-site-review'), count($summary_issues) - 10 ); } } else { $lines[] = $has_main_attention_point ? __('Other attention points: none found.', 'aifq-site-review') : __('Attention points: none found.', 'aifq-site-review'); } $lines[] = ''; if (!empty($suggestions)) { $lines[] = sprintf( /* translators: %d: number of improvement opportunities */ __('Improvement opportunities (%d):', 'aifq-site-review'), count($suggestions) ); foreach (array_slice($suggestions, 0, 5) as $suggestion) { $lines[] = '- ' . $this->display_issue_title($suggestion) . ': ' . $this->display_issue_recommendation($suggestion); } if (count($suggestions) > 5) { $lines[] = sprintf( /* translators: %d: number of hidden improvement opportunities */ __('- + %d more in AIFQ Site Review.', 'aifq-site-review'), count($suggestions) - 5 ); } } else { $lines[] = __('Improvement opportunities: none found.', 'aifq-site-review'); } $lines[] = ''; $area_score_parts = []; foreach ($subscores as $category => $subscore) { $area_score_parts[] = sprintf( /* translators: 1: category name, 2: score */ __('%1$s: %2$d/100', 'aifq-site-review'), $this->category_label($category), (int) $subscore ); } if (!empty($area_score_parts)) { $lines[] = __('Area scores:', 'aifq-site-review'); $lines[] = implode(' · ', $area_score_parts); } $lines[] = ''; $lines[] = __('Note: AIFQ Site Review results are practical indications based on local WordPress setup signals.', 'aifq-site-review'); $lines[] = __('No scan data was sent to an external service.', 'aifq-site-review'); return implode("\n", $lines); } private function current_signal_text_for_copy(array $issue) { if (!empty($issue['cluster_signals']) && is_array($issue['cluster_signals'])) { $signals = []; foreach ($issue['cluster_signals'] as $signal) { $signal = trim(wp_strip_all_tags((string) $signal)); if ($signal !== '') { $signals[] = rtrim($signal, '.'); } } if (!empty($signals)) { return implode('. ', $signals) . '.'; } } return sanitize_text_field((string) ($issue['context'] ?? '')); } private function build_developer_host_message(array $scan, array $issues, array $suggestions, array $first_actions) { $lines = []; $score = (int) ($scan['score'] ?? 100); $status_label = $this->score_status_label($score); $lines[] = __('Hi,', 'aifq-site-review'); $lines[] = ''; $lines[] = __('I ran an AIFQ Site Review scan on my WordPress site and would like your help reviewing the main findings below.', 'aifq-site-review'); $site_url = home_url(); if (!empty($site_url) && $site_url !== 'http://localhost' && $site_url !== 'https://localhost') { $lines[] = sprintf( /* translators: %s: site URL */ __('Site: %s', 'aifq-site-review'), esc_url_raw($site_url) ); } $lines[] = sprintf( /* translators: 1: score, 2: score status label */ __('AIFQ Site Review score: %1$d/100 — %2$s', 'aifq-site-review'), $score, $status_label ); if (!empty($scan['created_at'])) { $lines[] = sprintf( /* translators: %s: formatted scan date */ __('Scan date: %s', 'aifq-site-review'), $this->format_scan_date($scan['created_at']) ); } $lines[] = ''; if (!empty($first_actions)) { $lines[] = __('Main points to review:', 'aifq-site-review'); foreach (array_slice($first_actions, 0, 3) as $issue) { $lines[] = '- ' . $this->display_issue_title($issue); $lines[] = ' ' . $this->display_issue_recommendation($issue); if (!empty($issue['context'])) { $lines[] = ' ' . sprintf( /* translators: %s: issue context */ __('Current signal: %s', 'aifq-site-review'), $this->current_signal_text_for_copy($issue) ); } } } else { $lines[] = __('No important attention points were found in the latest AIFQ Site Review scan.', 'aifq-site-review'); } if (!empty($suggestions)) { $lines[] = ''; $lines[] = __('Improvement opportunities:', 'aifq-site-review'); foreach (array_slice($suggestions, 0, 3) as $suggestion) { $lines[] = '- ' . $this->display_issue_title($suggestion) . ': ' . $this->display_issue_recommendation($suggestion); } } $lines[] = ''; $lines[] = __('Could you help review these points and advise what should be changed safely?', 'aifq-site-review'); $lines[] = __('AIFQ Site Review results are practical indications based on local WordPress setup signals.', 'aifq-site-review'); return implode("\n", $lines); } private function render_category_card($category, array $grouped, array $grouped_suggestions = [], array $grouped_passed = []) { $issues = isset($grouped[$category]) ? $grouped[$category] : []; $suggestions = isset($grouped_suggestions[$category]) ? $grouped_suggestions[$category] : []; $passed = isset($grouped_passed[$category]) ? $grouped_passed[$category] : []; $attention_count = count($issues); $opportunity_count = count($suggestions); $passed_count = count($passed); ?>
category_label_html($category, 'category'), $this->safe_ui_kses_args()); ?> category_summary_line($attention_count, $opportunity_count, $passed_count)); ?>

severity_key($issue['severity'])); ?>
severity_indicator_html($issue['severity']), $this->safe_ui_kses_args()); ?> display_issue_title($issue)); ?> friendly_severity_label($issue['severity'])); ?>

issue_time_estimate($issue)); ?> · confidence_label_html($issue['confidence']), $this->safe_ui_kses_args()); ?> · issue_handler_label_html($issue), $this->safe_ui_kses_args()); ?>

issue_preview($issue)); ?>

display_issue_title($suggestion)); ?>

display_issue_recommendation($suggestion)); ?>

issue_handler_label_html($suggestion), $this->safe_ui_kses_args()); ?>

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; } $last_scan = get_option(self::OPTION_LAST_SCAN); if (is_array($last_scan)) { update_option(self::OPTION_PREVIOUS_SCAN, $last_scan, false); } $this->clear_large_images_cache(); $data = $this->collect_site_data(); $result = $this->run_local_checks($data); $issues = $result['issues']; $suggestions = isset($result['suggestions']) ? $result['suggestions'] : []; $passed = $result['passed']; $final_items = $this->finalize_scan_items($issues, $suggestions, $passed); $issues = $final_items['issues']; $suggestions = $final_items['suggestions']; $passed = $final_items['passed']; $subscores = $this->calculate_subscores($issues); $score = $this->calculate_total_score($subscores); $signals = $this->build_profile($data); $scan = [ 'version' => self::VERSION, 'score' => $score, 'subscores' => $subscores, 'issues' => $issues, 'suggestions' => $suggestions, 'passed' => $passed, 'signals' => $signals, 'created_at' => time(), ]; $saved = update_option(self::OPTION_LAST_SCAN, $scan, false); $stored_scan = get_option(self::OPTION_LAST_SCAN); $stored_created_at = is_array($stored_scan) ? (string) ($stored_scan['created_at'] ?? '') : ''; if (!$saved && $stored_created_at !== (string) $scan['created_at']) { wp_safe_redirect(add_query_arg('aifq_sr_notice', 'save_failed', admin_url('admin.php?page=aifq-site-review'))); exit; } $this->set_last_scan_timestamp(); wp_safe_redirect(admin_url('admin.php?page=aifq-site-review')); exit; } // ------------------------------------------------------------------------- // Data collection // ------------------------------------------------------------------------- private function get_plugin_update_names($plugin_updates, array $all_plugins) { $names = []; if (!is_object($plugin_updates) || !isset($plugin_updates->response) || !is_array($plugin_updates->response)) { return $names; } foreach ($plugin_updates->response as $plugin_file => $update) { $names[] = isset($all_plugins[$plugin_file]['Name']) ? $all_plugins[$plugin_file]['Name'] : $this->pretty_plugin_name($plugin_file); } return $names; } private function get_theme_update_names($theme_updates) { $names = []; if (!is_object($theme_updates) || !isset($theme_updates->response) || !is_array($theme_updates->response)) { return $names; } foreach ($theme_updates->response as $theme_slug => $update) { $theme = wp_get_theme($theme_slug); $names[] = ($theme && $theme->exists()) ? $theme->get('Name') : $theme_slug; } return $names; } private function site_urls_are_different($site_url, $home_url) { $site_url = untrailingslashit(trim((string) $site_url)); $home_url = untrailingslashit(trim((string) $home_url)); if ($site_url === '' || $home_url === '') { return false; } return strcasecmp($site_url, $home_url) !== 0; } private function action_scheduler_table_name() { global $wpdb; $table = $wpdb->prefix . 'actionscheduler_actions'; if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) { return ''; } return $table; } private function action_scheduler_table_exists() { global $wpdb; $table = $this->action_scheduler_table_name(); if ($table === '') { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Local Action Scheduler table existence check; no user data is read. $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); return is_string($found) && strtolower($found) === strtolower($table); } private function count_action_scheduler_status($status, $older_than_seconds = 0) { global $wpdb; $table_name = $this->action_scheduler_table_name(); if ($table_name === '' || !$this->action_scheduler_table_exists()) { return 0; } $table = esc_sql($table_name); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is generated from the WP prefix, regex-validated and escaped above. Query values use placeholders. if ($older_than_seconds > 0) { $scheduled_before = gmdate('Y-m-d H:i:s', time() - (int) $older_than_seconds); $sql = $wpdb->prepare( "SELECT COUNT(*) FROM (SELECT action_id FROM `{$table}` WHERE status = %s AND scheduled_date_gmt < %s LIMIT 1000) AS aifq_sr_action_count", (string) $status, $scheduled_before ); } else { $sql = $wpdb->prepare( "SELECT COUNT(*) FROM (SELECT action_id FROM `{$table}` WHERE status = %s LIMIT 1000) AS aifq_sr_action_count", (string) $status ); } // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared above; cached through the scan result. return (int) $wpdb->get_var($sql); } private function known_mail_delivery_plugins() { return [ 'wp-mail-smtp/wp_mail_smtp.php' => 'WP Mail SMTP', 'post-smtp/postman-smtp.php' => 'Post SMTP', 'fluent-smtp/fluent-smtp.php' => 'FluentSMTP', 'easy-wp-smtp/easy-wp-smtp.php' => 'Easy WP SMTP', 'wp-mail-bank/wp-mail-bank.php' => 'WP Mail Bank', 'smtp-mailer/main.php' => 'SMTP Mailer', 'mailgun/mailgun.php' => 'Mailgun', 'sendgrid-email-delivery-simplified/wpsendgrid.php' => 'SendGrid', 'brevo/brevo.php' => 'Brevo', 'mailersend-official-smtp-integration/mailersend-official-smtp-integration.php' => 'MailerSend', 'amazon-ses/amazon-ses.php' => 'Amazon SES', 'wp-ses/wp-ses.php' => 'WP SES', ]; } private function known_page_builder_plugins() { return [ 'elementor/elementor.php' => 'Elementor', 'js_composer/js_composer.php' => 'WPBakery', 'siteorigin-panels/siteorigin-panels.php' => 'SiteOrigin Page Builder', 'bb-plugin/fl-builder.php' => 'Beaver Builder', 'brizy/brizy.php' => 'Brizy', 'oxygen/functions.php' => 'Oxygen', 'breakdance/plugin.php' => 'Breakdance', 'bricks/bricks.php' => 'Bricks', 'divi-builder/divi-builder.php' => 'Divi Builder', 'thrive-visual-editor/thrive-visual-editor.php' => 'Thrive Architect', ]; } private function collect_site_data() { if (!function_exists('get_plugins')) require_once ABSPATH . 'wp-admin/includes/plugin.php'; if (!function_exists('get_core_updates')) require_once ABSPATH . 'wp-admin/includes/update.php'; $all_plugins = get_plugins(); $active_plugins = (array) get_option('active_plugins', []); $sitewide_plugins = is_multisite() ? array_keys((array) get_site_option('active_sitewide_plugins', [])) : []; $active_plugins = array_values(array_unique(array_filter(array_merge($active_plugins, $sitewide_plugins)))); $inactive_plugins = array_diff(array_keys($all_plugins), $active_plugins); $plugin_updates = get_site_transient('update_plugins'); $theme_updates = get_site_transient('update_themes'); $core_updates = get_core_updates(); $themes = wp_get_themes(); $active_stylesheet = get_stylesheet(); $active_template = get_template(); $active_theme_slugs = array_unique(array_filter([$active_stylesheet, $active_template])); $inactive_themes = []; foreach ($themes as $stylesheet => $theme) { if (!in_array($stylesheet, $active_theme_slugs, true)) { $inactive_themes[] = $stylesheet; } } $upload_dir = wp_upload_dir(); $environment_type = function_exists('wp_get_environment_type') ? wp_get_environment_type() : 'production'; $site_url_option = (string) get_option('siteurl', ''); $home_url_option = (string) get_option('home', ''); return [ 'wordpress_version' => get_bloginfo('version'), 'php_version' => PHP_VERSION, 'active_plugin_count' => count($active_plugins), 'inactive_plugin_count' => count($inactive_plugins), 'inactive_theme_count' => count($inactive_themes), 'active_plugins' => $active_plugins, 'plugin_updates_count' => (is_object($plugin_updates) && isset($plugin_updates->response) && is_array($plugin_updates->response)) ? count($plugin_updates->response) : 0, 'plugin_update_names' => $this->get_plugin_update_names($plugin_updates, $all_plugins), 'theme_updates_count' => (is_object($theme_updates) && isset($theme_updates->response) && is_array($theme_updates->response)) ? count($theme_updates->response) : 0, 'theme_update_names' => $this->get_theme_update_names($theme_updates), 'core_updates_count' => $this->count_core_updates($core_updates), 'is_ssl' => $this->is_https_active(), 'environment_type' => $environment_type, 'site_url_option' => $site_url_option, 'home_url_option' => $home_url_option, 'site_home_mismatch' => $this->site_urls_are_different($site_url_option, $home_url_option), 'xmlrpc_enabled' => $this->is_xmlrpc_enabled(), 'file_editor_enabled' => (!defined('DISALLOW_FILE_EDIT') || DISALLOW_FILE_EDIT !== true) && (!defined('DISALLOW_FILE_MODS') || DISALLOW_FILE_MODS !== true), 'file_mods_disabled' => defined('DISALLOW_FILE_MODS') && DISALLOW_FILE_MODS === true, 'memory_limit' => ini_get('memory_limit'), 'upload_dir_writable' => empty($upload_dir['error']) && !empty($upload_dir['basedir']) && wp_is_writable($upload_dir['basedir']), 'post_revisions_count' => $this->count_post_revisions(), 'cache_plugins' => $this->detect_plugins_by_list($active_plugins, $this->known_cache_plugins()), 'active_cache_plugin_count' => count($this->detect_plugins_by_list($active_plugins, $this->known_cache_plugins())), 'cache_constant_enabled' => defined('WP_CACHE') && WP_CACHE, 'cache_dropin_found' => file_exists(WP_CONTENT_DIR . '/advanced-cache.php'), 'object_cache_dropin_found' => file_exists(WP_CONTENT_DIR . '/object-cache.php'), 'cache_detected' => $this->detect_cache_plugin($active_plugins) || (defined('WP_CACHE') && WP_CACHE) || file_exists(WP_CONTENT_DIR . '/advanced-cache.php'), 'seo_plugins' => $this->detect_plugins_by_list($active_plugins, $this->known_seo_plugins()), 'security_plugins' => $this->detect_plugins_by_list($active_plugins, $this->known_security_plugins()), 'heavy_plugins' => $this->detect_plugins_by_list($active_plugins, $this->known_heavy_plugins()), 'elementor_plugins' => $this->detect_elementor_plugins($active_plugins), 'backup_plugins' => $this->detect_plugins_by_list($active_plugins, $this->known_backup_plugins()), 'mail_delivery_plugins' => $this->detect_plugins_by_list($active_plugins, $this->known_mail_delivery_plugins()), 'page_builder_plugins' => $this->detect_plugins_by_list($active_plugins, $this->known_page_builder_plugins()), 'woocommerce_active' => in_array('woocommerce/woocommerce.php', $active_plugins, true), 'elementor_active' => in_array('elementor/elementor.php', $active_plugins, true) || in_array('elementor-pro/elementor-pro.php', $active_plugins, true), 'large_images_count' => $this->count_large_images(), 'debug_enabled' => defined('WP_DEBUG') && WP_DEBUG === true, 'debug_display_enabled' => defined('WP_DEBUG_DISPLAY') && WP_DEBUG_DISPLAY === true, 'savequeries_enabled' => defined('SAVEQUERIES') && SAVEQUERIES === true, 'default_admin_username_exists' => username_exists('admin') ? true : false, 'users_can_register' => ((int) get_option('users_can_register', 0)) === 1, 'action_scheduler_failed_count' => $this->count_action_scheduler_status('failed'), 'action_scheduler_pending_old_count' => $this->count_action_scheduler_status('pending', HOUR_IN_SECONDS), 'debug_log_exists' => $this->debug_log_exists(), 'debug_log_size' => $this->debug_log_size(), 'autoloaded_options_size'=> $this->get_autoloaded_options_size(), 'search_engine_visible' => ((int) get_option('blog_public', 1)) === 1, 'permalink_structure' => (string) get_option('permalink_structure', ''), 'timezone_string' => (string) get_option('timezone_string', ''), 'gmt_offset' => (string) get_option('gmt_offset', ''), ]; } // ------------------------------------------------------------------------- // Checks // ------------------------------------------------------------------------- private function run_local_checks(array $data) { $issues = []; $suggestions = []; $passed = []; $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('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('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'])) { $issues[] = $this->issue('Performance', 'No cache layer detected', self::SEV_MEDIUM, 'This scan did not find a known caching plugin, WP_CACHE constant or cache drop-in on a heavier WordPress setup. Your hosting provider may still have server-level caching enabled.', 'Check whether your hosting already uses server-level caching. If not, consider one caching solution that fits your hosting.', 'Active plugins: ' . $data['active_plugin_count'] . '. Large images: ' . $data['large_images_count'] . '.' ); } elseif (!$data['cache_detected']) { $suggestions[] = $this->suggestion('Performance', 'No caching detected', 'This scan did not find a known caching plugin, WP_CACHE constant or cache drop-in. Your hosting provider may still have server-level caching enabled.', 'Check with your hosting provider first — server-level caching may already be active. If not, caching can help as the website grows.', 'Light sites do not always need a separate caching plugin.' ); } 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('Performance', 'Cache signal without active cache plugin', 'A caching signal exists, but no active caching plugin was detected.', 'Check whether this comes from hosting-level caching, WP_CACHE or leftover cache files from an old cache plugin.', !empty($context_parts) ? implode('. ', $context_parts) . '.' : 'No active caching plugin was found.' ); } if ($seo_count >= 2) { $issues[] = $this->issue('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('Performance', 'One SEO plugin active', 'One SEO plugin was found.'); } else { $suggestions[] = $this->suggestion('Performance', 'No SEO plugin found', 'No known SEO plugin is active.', 'Consider one SEO plugin if search visibility matters for this website.', 'No SEO plugin is not an error, but it can be a missed opportunity if search visibility matters.' ); } if (empty($data['search_engine_visible'])) { if ($is_production_environment) { $issues[] = $this->issue('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('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('Maintenance', 'Search engine visibility is enabled', 'WordPress is not discouraging search engines from indexing the site.'); } if (empty($data['permalink_structure'])) { $suggestions[] = $this->suggestion('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('Performance', 'Readable permalink structure active', 'A custom permalink structure is configured.'); } if ($heavy_count >= 3) { $issues[] = $this->issue('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('Performance', 'Heavy plugins limited', 'Some resource-heavy plugins are present, but not enough to flag as a setup-complexity signal.'); } else { $passed[] = $this->passed('Performance', 'No known heavy plugins found', 'No plugins from the resource-heavy plugin list were found.'); } if ($data['woocommerce_active'] && !$data['cache_detected']) { $issues[] = $this->issue('Performance', 'No caching plugin detected for WooCommerce', self::SEV_MEDIUM, 'WooCommerce is active, but no caching solution appears to be present. Webshops add more database and PHP load than simple websites.', 'Check with your hosting provider first — server-level caching may already be active. If not, use one WooCommerce-safe caching solution and exclude cart, checkout and account pages from aggressive caching.', 'Aggressive caching on cart and checkout can cause problems. Choose a WooCommerce-safe configuration.' ); } if (empty($data['object_cache_dropin_found']) && ($data['woocommerce_active'] || (int) $data['active_plugin_count'] >= 25)) { $suggestions[] = $this->suggestion('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'])) { $passed[] = $this->passed('Performance', 'Object cache drop-in found', 'Persistent object caching appears to be active. This is a server-level caching layer separate from normal page caching plugins.'); } if ($data['woocommerce_active'] && (int) $data['active_plugin_count'] >= 25) { $issues[] = $this->issue('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('Maintenance', 'No known email delivery plugin found for WooCommerce', 'WooCommerce sends important emails such as order confirmations, password resets and customer notifications. This scan did not find a known transactional email delivery plugin.', '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.', 'WooCommerce is active. No known SMTP or transactional email delivery plugin was found.' ); } if ($data['woocommerce_active'] && (int) $data['action_scheduler_failed_count'] >= 10) { $issues[] = $this->issue('Maintenance', 'WooCommerce failed background tasks found', self::SEV_MEDIUM, 'WooCommerce uses background tasks for scheduled work. Some tasks appear to have failed and may need review.', 'Ask your developer to review WooCommerce scheduled actions and check whether failed tasks are still relevant.', 'Failed scheduled actions counted: ' . (int) $data['action_scheduler_failed_count'] . '. Only Action Scheduler status counts were checked.' ); } if ($data['woocommerce_active'] && (int) $data['action_scheduler_pending_old_count'] >= 25) { $issues[] = $this->issue('Maintenance', 'WooCommerce background task backlog found', self::SEV_MEDIUM, 'Some WooCommerce background tasks appear to be waiting longer than expected.', 'Ask your developer or hosting provider to check whether WP-Cron and scheduled actions are running normally.', 'Pending scheduled actions older than one hour counted: ' . (int) $data['action_scheduler_pending_old_count'] . '. Only Action Scheduler status counts were checked.' ); } if ($data['woocommerce_active'] && $cache_count > 1) { $issues[] = $this->issue('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('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('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('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('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('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('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('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('Performance', 'Active plugin count looks normal', 'The number of active plugins is below the attention threshold.'); } // ----------------------------------------------------------------- // Security // ----------------------------------------------------------------- if ($security_count >= 2) { $issues[] = $this->issue('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', 'One security plugin active', 'One security plugin was found.'); } else { $suggestions[] = $this->suggestion('Security', 'No known security plugin found', 'No known security plugin was found. Bots commonly target WordPress login pages and forms. A security plugin can help with monitoring, blocking and alerts.', 'Check what protection your hosting provider already includes. If needed, consider one reliable security plugin for basic monitoring. A security plugin is a helpful signal layer, not a guarantee against all threats.', 'A security plugin is not a guarantee, but it can help spot suspicious traffic and known attack patterns earlier.' ); } if (!empty($data['default_admin_username_exists'])) { $issues[] = $this->issue('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('Security', 'Default admin username not found', 'No user with the username admin was found.'); } if (!empty($data['users_can_register'])) { $suggestions[] = $this->suggestion('Security', 'Public user registration is enabled', 'Visitors may be able to create accounts on this WordPress site.', '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.', 'WordPress option users_can_register is enabled.' ); } else { $passed[] = $this->passed('Security', 'Public user registration is disabled', 'Visitors cannot create WordPress accounts through the default registration setting.'); } if ($data['xmlrpc_enabled']) { $issues[] = $this->issue('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('Security', 'XML-RPC is disabled in WordPress', 'The WordPress-side XML-RPC signal is disabled.'); } if ($data['file_editor_enabled']) { $issues[] = $this->issue('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('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('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('Security', 'HTTPS is not active in WordPress admin', self::SEV_HIGH, 'WordPress admin does not appear to be loaded over HTTPS in this request. This may be normal behind certain proxies or load balancers.', 'Check whether HTTPS is configured correctly for WordPress admin and the public site.', 'HTTPS was not detected for the current WordPress admin request.' ); } else { $passed[] = $this->passed('Security', 'HTTPS is active', 'WordPress admin appears to be running over HTTPS in this context.'); } if (!empty($data['site_home_mismatch'])) { $issues[] = $this->issue('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('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('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('Maintenance', 'WordPress core appears up to date', 'No pending WordPress core update is currently reported.'); } if ($data['plugin_updates_count'] > 0) { $issues[] = $this->issue('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('Maintenance', 'Plugins appear up to date', 'No pending plugin updates are currently reported by WordPress.'); } if ($data['theme_updates_count'] > 0) { $issues[] = $this->issue('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('Maintenance', 'Theme appears up to date', 'No pending theme updates are currently reported by WordPress.'); } if ($data['inactive_plugin_count'] >= 7) { $issues[] = $this->issue('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('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('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('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('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('Maintenance', 'Inactive theme count is not high', 'The number of inactive themes is not unusually high.'); } if ($backup_count === 0) { $suggestions[] = $this->suggestion('Maintenance', 'No known backup plugin found', 'No known backup plugin was found. That does not automatically mean there are no backups; your hosting provider may also create backups.', 'Check whether reliable backups are available through your hosting, or use a separate backup solution.', 'What matters most is knowing how to restore a backup when needed.' ); } else { $passed[] = $this->passed('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('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('Health', 'PHP memory limit appears sufficient', 'Memory limit: ' . $data['memory_limit'] . '.'); } if ($data['active_plugin_count'] >= 20 && $memory_mb < 256) { $issues[] = $this->issue('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('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('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('Health', 'PHP version is modern enough', 'PHP ' . $data['php_version'] . ' is active.'); } if (!$data['upload_dir_writable']) { $issues[] = $this->issue('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('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('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('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('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('Health', 'Debug mode is disabled', 'Good for a normal live website.'); } if (!empty($data['savequeries_enabled'])) { if ($is_production_environment) { $issues[] = $this->issue('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('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('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('Health', 'No active debug log found', 'No non-empty wp-content/debug.log file was found.'); } if ((int) $data['autoloaded_options_size'] > 1024 * 1024) { $issues[] = $this->issue('Performance', 'More WordPress data loads automatically than usual', self::SEV_MEDIUM, '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.', 'Do not delete database data yourself unless you know exactly what it is. Ask your developer, hosting provider or website manager to check the largest autoloaded options and safely clean up old plugin data.', 'Automatically loaded WordPress option data detected. Size: ' . size_format((int) $data['autoloaded_options_size']) . '.' ); } else { $passed[] = $this->passed('Health', 'Autoloaded options size looks normal', 'Autoloaded option data is below the attention threshold.'); } if ($data['post_revisions_count'] >= 500) { $issues[] = $this->issue('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('Health', 'Revision count is not high', 'The database does not appear to contain an unusually high number of old revisions.'); } return ['issues' => $issues, 'suggestions' => $suggestions, 'passed' => $passed]; } 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 $issues) { $keys = array_column($issues, 'key'); if (!in_array('woo_multi_cache', $keys, true)) { return $issues; } $filtered = []; foreach ($issues as $issue) { if (($issue['key'] ?? '') === 'multi_cache') { continue; } $filtered[] = $issue; } 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( '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; $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( '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( '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); $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', '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) { $map = [ 'Active caching plugin found' => 'active_cache_plugin', 'Caching signal found' => 'active_cache_plugin', 'Object cache drop-in found' => 'object_cache_found', 'One SEO plugin active' => 'one_seo_plugin', 'Search engine visibility is enabled' => 'search_visibility_enabled', 'One security plugin active' => 'security_plugin_active', 'XML-RPC appears disabled' => 'xmlrpc_disabled', 'XML-RPC is disabled in WordPress' => 'xmlrpc_disabled', 'Dashboard file editing is disabled' => 'file_editor_disabled', 'WordPress core appears up to date' => 'core_up_to_date', 'Plugins appear up to date' => 'plugins_up_to_date', 'Theme appears up to date' => 'theme_up_to_date', 'Debug mode is disabled' => 'debug_disabled', 'No active debug log found' => 'debug_log_clean', 'Default admin username not found' => 'admin_username_not_found', 'Public user registration is disabled' => 'public_registration_disabled', 'Autoloaded options size looks normal' => 'autoload_ok', 'No unusually high number of large images'=> 'large_images_ok', 'Backup solution found' => 'backup_found', 'Backup-related plugin found' => 'backup_found', ]; return $map[$title] ?? sanitize_key(strtolower(str_replace(' ', '_', (string) $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 = $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 clear caching layer 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($category, $title, $severity, $explanation, $recommendation, $context = '') { $issue = [ 'type' => 'attention', 'category' => $category, 'title' => $title, 'key' => $this->title_to_key($title), 'severity' => $severity, 'explanation' => $explanation, 'recommendation' => $recommendation, 'context' => $context, ]; $issue['confidence'] = $this->issue_confidence($issue); $issue['time_estimate'] = $this->issue_time_estimate($issue); $issue['simple_explanation'] = $this->issue_simple_explanation($issue); $issue['if_ignored'] = $this->issue_if_ignored($issue); return $issue; } private function suggestion($category, $title, $explanation, $recommendation, $context = '') { $suggestion = $this->issue($category, $title, self::SEV_LOW, $explanation, $recommendation, $context); $suggestion['type'] = 'opportunity'; $suggestion['confidence'] = 'medium'; $suggestion['time_estimate'] = __('±5 minutes', 'aifq-site-review'); return $suggestion; } private function passed($category, $title, $description) { return ['category' => $category, 'title' => $title, 'description' => $description]; } // Maps issue title → stable key used for all dispatch (replaces fragile strpos chains) private function title_to_key($title) { $map = [ 'Multiple caching plugins active' => 'multi_cache', 'No caching detected' => 'no_cache', 'No cache layer detected' => 'no_cache_layer', 'Cache signal without active cache plugin'=> 'cache_signal_no_plugin', 'No object cache drop-in detected' => 'no_object_cache_dropin', 'Active caching plugin found' => 'active_cache_plugin', 'Multiple SEO plugins active' => 'multi_seo', 'No SEO plugin found' => 'no_seo', 'Search engine visibility is disabled' => 'search_visibility_disabled', 'Plain permalink structure detected' => 'plain_permalinks', 'Several heavier plugins active' => 'heavy_plugins', 'Multiple heavy plugins active' => 'heavy_plugins', 'Many active plugins found' => 'many_plugins', 'Larger plugin stack detected' => 'many_plugins', 'Many active plugins' => 'many_plugins', 'Elementor with many add-ons' => 'elementor_addons_many', 'WooCommerce without caching' => 'woo_no_cache', // Legacy scan title. 'No caching plugin detected for WooCommerce' => 'woo_no_cache', 'Many plugins active on this WooCommerce site' => 'woo_many_plugins', 'Larger WooCommerce plugin stack detected' => 'woo_many_plugins', 'WooCommerce site with many active plugins' => 'woo_many_plugins', 'WooCommerce with multiple caching plugins' => 'woo_multi_cache', 'Elementor with several add-ons' => 'elementor_addons_several', 'Elementor and WooCommerce complexity detected' => 'elementor_woo_complexity', 'Many large images found' => 'large_images', 'Several large images found' => 'large_images', 'Multiple security plugins active' => 'multi_security', 'No known security plugin found' => 'no_security', 'XML-RPC appears active' => 'xmlrpc', 'XML-RPC is available in WordPress' => 'xmlrpc', 'Dashboard file editing is enabled' => 'file_editor', 'File modifications are disabled' => 'file_mods_disabled', 'HTTPS is not active in WordPress admin' => 'no_ssl', 'WordPress Address and Site Address are different' => 'site_url_home_mismatch', 'WordPress core update available' => 'core_update', 'Very old WordPress version' => 'old_wp_version', 'Plugin updates available' => 'plugin_updates', 'Theme update available' => 'theme_update', 'Many inactive plugins' => 'inactive_plugins', 'Several inactive plugins found' => 'inactive_plugins', 'Several inactive plugins' => 'inactive_plugins_soft', 'Many inactive themes' => 'inactive_themes', 'Several inactive themes found' => 'inactive_themes', 'Several inactive themes' => 'inactive_themes_soft', 'No backup plugin found' => 'no_backup', 'No known backup plugin found' => 'no_backup', 'Old PHP version' => 'old_php', 'Low PHP memory limit' => 'low_memory', 'Larger plugin stack with limited memory' => 'memory_plugin_conflict', 'Many plugins with limited memory' => 'memory_plugin_conflict', 'Old PHP version with many updates' => 'old_php_updates', 'Upload directory is not writable' => 'upload_not_writable', 'WP_DEBUG is enabled' => 'debug_on', 'Error display is enabled' => 'wp_debug_display', 'Debug log file found' => 'debug_log_found', 'Large autoloaded options size' => 'large_autoloaded_options', 'Too much data is loaded on every page visit' => 'large_autoloaded_options', 'Too much WordPress data loads automatically' => 'large_autoloaded_options', 'More WordPress data loads automatically than usual' => 'large_autoloaded_options', 'Many revisions in the database' => 'many_revisions', 'Query logging is enabled' => 'savequeries_enabled', 'Development settings are still active' => 'development_settings_active', 'Complex plugin setup needs review' => 'complex_plugin_setup', 'WooCommerce background tasks need review' => 'woo_background_tasks', 'Default admin username exists' => 'default_admin_username', 'Public user registration is enabled' => 'public_user_registration', 'No known email delivery plugin found for WooCommerce' => 'woo_no_mail_delivery_plugin', 'WooCommerce failed background tasks found' => 'woo_failed_scheduled_actions', 'WooCommerce background task backlog found' => 'woo_scheduled_actions_backlog', 'Multiple page builders active' => 'multiple_page_builders', ]; return isset($map[$title]) ? $map[$title] : sanitize_key(strtolower(str_replace(' ', '_', (string) $title))); } // ------------------------------------------------------------------------- // Scoring // ------------------------------------------------------------------------- private function calculate_subscores(array $issues) { $subscores = [ 'Performance' => 100, 'Security' => 100, 'Maintenance' => 100, 'Health' => 100, ]; foreach ($issues as $issue) { $category = isset($issue['category']) && isset($subscores[$issue['category']]) ? $issue['category'] : 'Health'; $severity = $issue['severity'] ?? self::SEV_LOW; if ($severity === self::SEV_HIGH) { $subscores[$category] -= self::DEDUCT_HIGH; } elseif ($severity === self::SEV_MEDIUM) { $subscores[$category] -= self::DEDUCT_MEDIUM; } else { $subscores[$category] -= self::DEDUCT_LOW; } } foreach ($subscores as $category => $score) { $subscores[$category] = max(0, min(100, (int) $score)); } return $subscores; } private function calculate_total_score(array $subscores) { if (empty($subscores)) return 100; return (int) round(array_sum($subscores) / count($subscores)); } private function calculate_potential_score($current_score, array $issues) { if (empty($issues)) { return (int) $current_score; } $categories = ['Performance', 'Security', 'Maintenance', 'Health']; $improvement_by_category = array_fill_keys($categories, 0); foreach ($issues as $issue) { $category = isset($issue['category']) ? (string) $issue['category'] : 'Health'; if (!isset($improvement_by_category[$category])) { $improvement_by_category[$category] = 0; } $severity = $issue['severity'] ?? self::SEV_LOW; if ($severity === self::SEV_HIGH) { $improvement_by_category[$category] += self::DEDUCT_HIGH; } elseif ($severity === self::SEV_MEDIUM) { $improvement_by_category[$category] += self::DEDUCT_MEDIUM; } else { $improvement_by_category[$category] += self::DEDUCT_LOW; } } $average_gain = (int) round(array_sum($improvement_by_category) / max(1, count($categories))); return min(100, (int) $current_score + $average_gain); } // ------------------------------------------------------------------------- // Summary / grouping helpers // ------------------------------------------------------------------------- private function get_issue_summary(array $issues) { $s = ['high' => 0, 'medium' => 0, 'low' => 0]; foreach ($issues as $i) { if ($i['severity'] === self::SEV_HIGH) $s['high']++; elseif ($i['severity'] === self::SEV_MEDIUM) $s['medium']++; else $s['low']++; } return $s; } private function group_issues_by_category(array $issues) { $grouped = ['Performance' => [], 'Security' => [], 'Maintenance' => [], 'Health' => []]; foreach ($issues as $issue) { $cat = $issue['category']; if (!isset($grouped[$cat])) $grouped[$cat] = []; $grouped[$cat][] = $issue; } return $grouped; } private function get_first_actions(array $issues, $limit = 3) { // Issues are already finalized/deduped by finalize_scan_items() before this is called. $priority = [ 'search_visibility_disabled' => 100, 'no_ssl' => 98, 'core_update' => 96, 'development_settings_active'=> 95, 'woo_background_tasks' => 93, 'complex_plugin_setup' => 79, 'wp_debug_display' => 94, 'woo_failed_scheduled_actions'=> 93, 'woo_scheduled_actions_backlog'=> 93, 'old_wp_version' => 92, 'woo_multi_cache' => 90, 'elementor_woo_complexity' => 88, 'large_autoloaded_options' => 86, 'multi_cache' => 84, 'woo_no_cache' => 82, 'large_images' => 80, 'woo_many_plugins' => 78, 'old_php' => 76, 'plugin_updates' => 74, 'memory_plugin_conflict' => 73, 'elementor_addons_many' => 72, 'elementor_addons_several' => 72, 'upload_not_writable' => 71, 'heavy_plugins' => 70, 'many_plugins' => 68, 'multi_security' => 66, 'savequeries_enabled' => 65, 'multiple_page_builders' => 64, 'file_editor' => 62, 'file_mods_disabled' => 61, 'theme_update' => 60, 'multi_seo' => 59, 'low_memory' => 58, 'debug_log_found' => 57, 'xmlrpc' => 40, 'inactive_plugins' => 40, 'site_url_home_mismatch' => 38, 'default_admin_username' => 37, 'inactive_themes' => 35, ]; usort($issues, function($a, $b) use ($priority) { $key_a = $a['key'] ?? 'generic'; $key_b = $b['key'] ?? 'generic'; $score_a = $priority[$key_a] ?? 10; $score_b = $priority[$key_b] ?? 10; if ($score_a === $score_b) { $sev_a = $this->severity_sort_weight($a['severity'] ?? self::SEV_LOW); $sev_b = $this->severity_sort_weight($b['severity'] ?? self::SEV_LOW); return $sev_b <=> $sev_a; } return $score_b <=> $score_a; }); return array_slice($issues, 0, $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 group_passed_by_category(array $passed) { $grouped = [ 'Performance' => [], 'Security' => [], 'Maintenance' => [], 'Health' => [], ]; foreach ($passed as $point) { $category = isset($point['category']) && isset($grouped[$point['category']]) ? $point['category'] : 'Health'; $grouped[$category][] = $point; } return $grouped; } private function category_summary_line($attention_count, $opportunity_count, $passed_count) { $parts = []; if ($attention_count > 0) { $parts[] = sprintf( /* translators: %d: number of attention points. */ _n('%d attention point', '%d attention points', (int) $attention_count, 'aifq-site-review'), (int) $attention_count ); } if ($opportunity_count > 0) { $parts[] = sprintf( /* translators: %d: number of opportunities. */ _n('%d opportunity', '%d opportunities', (int) $opportunity_count, 'aifq-site-review'), (int) $opportunity_count ); } if (!empty($parts)) { return implode(' · ', $parts); } if ($passed_count > 0) { return __('All checks passed', 'aifq-site-review'); } return __('No attention points found', 'aifq-site-review'); } 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) { $score_a = $priority[$a['key'] ?? 'generic'] ?? 10; $score_b = $priority[$b['key'] ?? 'generic'] ?? 10; return $score_b <=> $score_a; }); return array_slice($suggestions, 0, $limit); } private function get_human_summary(array $summary, array $grouped, $score, $potential) { $total = $summary['high'] + $summary['medium'] + $summary['low']; if ($total === 0) { return __('Good news. No important attention points were found.', 'aifq-site-review'); } if ($summary['high'] > 0) { return sprintf( /* translators: %d: number of important attention points. */ _n( 'There is %d important attention point. Addressing the recommended steps can clearly improve the score.', 'There are %d important attention points. Addressing the recommended steps can clearly improve the score.', (int) $summary['high'], 'aifq-site-review' ), (int) $summary['high'] ); } $main = $this->get_main_problem_category($grouped); if ($main) { return sprintf( /* translators: %s: category phrase describing where remaining items are found. */ __('The website has few attention points. The remaining items are mostly %s.', 'aifq-site-review'), $this->category_focus_phrase($main) ); } return __('The website has few attention points. They are worth reviewing.', 'aifq-site-review'); } private function get_main_problem_category(array $grouped) { $max = null; $max_count = 0; foreach ($grouped as $cat => $issues) { if (count($issues) > $max_count) { $max_count = count($issues); $max = $cat; } } return $max; } 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 score_color($score) { if ($score >= 80) { return '#16a34a'; } if ($score >= 65) { return '#a16207'; } if ($score >= 50) { return '#d97706'; } return '#dc2626'; } private function score_status_label($score) { if ($score >= 90) { return __('Strong and calm', 'aifq-site-review'); } if ($score >= 75) { return __('Good, with attention points', 'aifq-site-review'); } if ($score >= 60) { return __('Needs attention', 'aifq-site-review'); } return __('Several things need attention', 'aifq-site-review'); } private function friendly_confidence_label($confidence) { if ($confidence === 'high') { return __('high confidence', 'aifq-site-review'); } if ($confidence === 'medium') { return __('reasonable confidence', 'aifq-site-review'); } return __('indication', 'aifq-site-review'); } private function friendly_severity_label($severity) { if ($severity === self::SEV_HIGH) { return __('Important', 'aifq-site-review'); } if ($severity === self::SEV_MEDIUM) { return __('Attention', 'aifq-site-review'); } return __('Minor', 'aifq-site-review'); } private function module_status_label_for_category($score, array $issues = [], array $suggestions = []) { if (!empty($issues)) { foreach ($issues as $issue) { if (($issue['severity'] ?? '') === self::SEV_HIGH) { return __('Needs attention', 'aifq-site-review'); } } return __('Needs attention', 'aifq-site-review'); } if (!empty($suggestions)) { return __('Some improvements available', 'aifq-site-review'); } return __('Looks good', 'aifq-site-review'); } private function category_main_issue(array $issues) { if (empty($issues)) { return null; } $priority = [ 'woo_multi_cache' => 120, 'elementor_woo_complexity' => 115, 'woo_no_cache' => 110, 'large_images' => 105, 'heavy_plugins' => 100, 'many_plugins' => 95, 'large_autoloaded_options' => 92, 'multi_cache' => 90, 'memory_plugin_conflict' => 88, 'elementor_addons_many' => 86, 'elementor_addons_several' => 86, 'woo_many_plugins' => 84, 'old_wp_version' => 82, 'core_update' => 80, 'plugin_updates' => 76, 'theme_update' => 72, 'multi_security' => 70, 'xmlrpc' => 68, 'file_editor' => 66, 'search_visibility_disabled' => 64, 'multi_seo' => 45, ]; usort($issues, function($a, $b) use ($priority) { $key_a = $a['key'] ?? 'generic'; $key_b = $b['key'] ?? 'generic'; $score_a = $priority[$key_a] ?? 10; $score_b = $priority[$key_b] ?? 10; if ($score_a === $score_b) { return $this->severity_sort_value_for_key($b['severity'] ?? self::SEV_LOW) <=> $this->severity_sort_value_for_key($a['severity'] ?? self::SEV_LOW); } return $score_b <=> $score_a; }); return $issues[0]; } private function category_label($category) { $map = [ 'Performance' => __('Setup health', 'aifq-site-review'), 'Security' => __('Security signals', 'aifq-site-review'), 'Maintenance' => __('Maintenance', 'aifq-site-review'), 'Health' => __('Site basics', 'aifq-site-review'), ]; return isset($map[$category]) ? $map[$category] : (string) $category; } private function category_icon_name($category) { $map = [ 'Performance' => 'zap', 'Security' => 'shield-check', 'Maintenance' => 'wrench', 'Health' => 'circle-check', ]; return $map[$category] ?? 'circle'; } private function category_label_html($category, $context = 'category') { $context = ($context === 'mini') ? 'mini' : 'category'; $shell_class = ($context === 'mini') ? 'aifq-sr-mini-icon-shell' : 'aifq-sr-category-icon-shell'; return '' . $this->icon_svg($this->category_icon_name($category), $shell_class) . '' . esc_html($this->category_label($category)) . '' . ''; } private function category_focus_phrase($category) { switch ($category) { case 'Performance': return __('setup-health signals', 'aifq-site-review'); case 'Security': return __('security-related signals', 'aifq-site-review'); case 'Maintenance': return __('maintenance-related signals', 'aifq-site-review'); case 'Health': return __('site-basics signals', 'aifq-site-review'); default: return strtolower($this->category_label($category)); } } private function signal_icon_name($signal) { $signal = (string) $signal; if (strpos($signal, 'WordPress') === 0) return 'diamond'; if (strpos($signal, 'WooCommerce') === 0) return 'shopping-cart'; if (strpos($signal, 'Elementor') === 0) return 'palette'; if (strpos($signal, 'SEO') === 0) return 'chart'; if (strpos($signal, 'Active cache') === 0 || strpos($signal, 'Cache') === 0) return 'zap'; if (strpos($signal, 'No caching') === 0) return 'alert'; if (strpos($signal, 'PHP') === 0) return 'server'; if (strpos($signal, 'active plugins') !== false) return 'plug'; return 'circle'; } private function clean_signal_label($signal) { $signal = (string) $signal; return trim(preg_replace('/^[\x{2600}-\x{27BF}\x{1F300}-\x{1FAFF}]\s*/u', '', $signal)); } private function safe_ui_kses_args() { return [ 'span' => [ 'class' => true, 'aria-hidden' => true, ], 'svg' => [ 'class' => true, 'width' => true, 'height' => true, 'viewBox' => true, 'aria-hidden' => true, 'focusable' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ], 'path' => [ 'd' => true, ], 'circle' => [ 'cx' => true, 'cy' => true, 'r' => true, ], 'rect' => [ 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, ], ]; } private function safe_ui_html($html) { return wp_kses((string) $html, $this->safe_ui_kses_args()); } private function severity_indicator_html($severity) { $class = 'low'; if ($severity === self::SEV_HIGH) { $class = 'high'; } elseif ($severity === self::SEV_MEDIUM) { $class = 'medium'; } return ''; } private function score_status_indicator_html($score) { $class = 'good'; if ($score < 60) { $class = 'bad'; } elseif ($score < 85) { $class = 'warn'; } return ''; } private function confidence_label_html($confidence) { $confidence = (string) $confidence; $class = ($confidence === 'high') ? 'high' : (($confidence === 'medium') ? 'medium' : 'low'); return '' . esc_html($this->friendly_confidence_label($confidence)) . ''; } private function icon_svg($name, $class = 'aifq-sr-icon') { $name = sanitize_key((string) $name); $class_parts = preg_split('/\s+/', trim((string) $class)); $class_parts = array_filter(array_map('sanitize_html_class', $class_parts)); $class_attr = esc_attr(trim(implode(' ', $class_parts) . ' aifq-sr-icon-shell aifq-sr-icon-shell-' . $name)); $size = 16; $raw_class = (string) $class; if (strpos($raw_class, 'meta') !== false) { $size = 13; } elseif (strpos($raw_class, 'chip') !== false || strpos($raw_class, 'signal') !== false || strpos($raw_class, 'hero') !== false) { $size = 15; } elseif (strpos($raw_class, 'mini') !== false) { $size = 18; } elseif (strpos($raw_class, 'category') !== false) { $size = 18; } $paths = [ // Selected inline SVG paths from Lucide Icons. All use a 24x24 viewBox, // 2px stroke and currentColor so they align predictably in the admin UI. 'zap' => '', 'shield-check' => '', 'lock' => '', 'wrench' => '', 'heart' => '', 'circle-check' => '', 'diamond' => '', 'shopping-cart' => '', 'palette' => '', 'chart' => '', 'alert' => '', 'server' => '', 'plug' => '', 'clock' => '', 'check' => '', 'circle' => '', ]; $path = isset($paths[$name]) ? $paths[$name] : $paths['circle']; $size_attr = (string) absint($size); return ''; } private function severity_key($severity) { if ($severity === self::SEV_HIGH) { return 'high'; } if ($severity === self::SEV_MEDIUM) { return 'medium'; } return 'low'; } private function issue_handler_label($issue) { $key = isset($issue['key']) ? (string) $issue['key'] : ''; $type = isset($issue['type']) ? (string) $issue['type'] : 'attention'; $map = [ 'multi_cache' => __('Developer', 'aifq-site-review'), 'no_cache' => __('Developer or hosting provider', 'aifq-site-review'), 'no_cache_layer' => __('Hosting provider', 'aifq-site-review'), 'cache_signal_no_plugin' => __('Developer or hosting provider', 'aifq-site-review'), 'no_object_cache_dropin' => __('Hosting provider', 'aifq-site-review'), 'multi_seo' => __('Developer', 'aifq-site-review'), 'no_seo' => __('Site owner', 'aifq-site-review'), 'search_visibility_disabled' => __('Site owner', 'aifq-site-review'), 'plain_permalinks' => __('Site owner', 'aifq-site-review'), 'heavy_plugins' => __('Developer', 'aifq-site-review'), 'many_plugins' => __('Developer', 'aifq-site-review'), 'elementor_addons_many' => __('Developer', 'aifq-site-review'), 'elementor_addons_several' => __('Developer', 'aifq-site-review'), 'woo_no_cache' => __('Developer or hosting provider', 'aifq-site-review'), 'woo_many_plugins' => __('Developer', 'aifq-site-review'), 'woo_no_mail_delivery_plugin' => __('Developer or hosting provider', 'aifq-site-review'), 'woo_failed_scheduled_actions' => __('Developer', 'aifq-site-review'), 'woo_scheduled_actions_backlog'=> __('Developer or hosting provider', 'aifq-site-review'), 'woo_multi_cache' => __('Developer or hosting provider', 'aifq-site-review'), 'elementor_woo_complexity' => __('Developer', 'aifq-site-review'), 'multiple_page_builders' => __('Developer', 'aifq-site-review'), 'large_images' => __('Site owner', 'aifq-site-review'), 'multi_security' => __('Developer', 'aifq-site-review'), 'no_security' => __('Site owner', 'aifq-site-review'), 'default_admin_username' => __('Developer', 'aifq-site-review'), 'public_user_registration' => __('Site owner', 'aifq-site-review'), 'xmlrpc' => __('Developer or hosting provider', 'aifq-site-review'), 'file_editor' => __('Developer', 'aifq-site-review'), 'file_mods_disabled' => __('Developer or hosting provider', 'aifq-site-review'), 'no_ssl' => __('Hosting provider', 'aifq-site-review'), 'site_url_home_mismatch' => __('Developer or hosting provider', 'aifq-site-review'), 'old_wp_version' => __('Developer', 'aifq-site-review'), 'core_update' => __('Site owner', 'aifq-site-review'), 'plugin_updates' => __('Site owner', 'aifq-site-review'), 'theme_update' => __('Site owner', 'aifq-site-review'), 'inactive_plugins' => __('Site owner', 'aifq-site-review'), 'inactive_plugins_soft' => __('Site owner', 'aifq-site-review'), 'inactive_themes' => __('Site owner', 'aifq-site-review'), 'inactive_themes_soft' => __('Site owner', 'aifq-site-review'), 'no_backup' => __('Site owner', 'aifq-site-review'), 'old_php' => __('Hosting provider', 'aifq-site-review'), 'low_memory' => __('Hosting provider', 'aifq-site-review'), 'memory_plugin_conflict' => __('Developer or hosting provider', 'aifq-site-review'), 'old_php_updates' => __('Developer or hosting provider', 'aifq-site-review'), 'upload_not_writable' => __('Hosting provider', 'aifq-site-review'), 'debug_on' => __('Developer', 'aifq-site-review'), 'wp_debug_display' => __('Developer', 'aifq-site-review'), 'savequeries_enabled' => __('Developer', 'aifq-site-review'), 'development_settings_active' => __('Developer or hosting provider', 'aifq-site-review'), 'complex_plugin_setup' => __('Developer', 'aifq-site-review'), 'woo_background_tasks' => __('Developer or hosting provider', 'aifq-site-review'), 'debug_log_found' => __('Developer or hosting provider', 'aifq-site-review'), 'large_autoloaded_options' => __('Developer or hosting provider', 'aifq-site-review'), 'many_revisions' => __('Site owner', 'aifq-site-review'), ]; if (isset($map[$key])) { return $map[$key]; } if (in_array($type, ['suggestion', 'opportunity'], true)) { return __('Site owner', 'aifq-site-review'); } return __('Developer or hosting provider', 'aifq-site-review'); } private function issue_handler_label_html($issue) { return '' . '' . esc_html__('Best handled by:', 'aifq-site-review') . ' ' . '' . esc_html($this->issue_handler_label($issue)) . '' . ''; } private function render_cluster_issue_text($issue) { $signals = []; if (!empty($issue['cluster_signals']) && is_array($issue['cluster_signals'])) { foreach ($issue['cluster_signals'] as $signal) { $signal = trim(wp_strip_all_tags((string) $signal)); if ($signal !== '') { $signals[] = $signal; } } } if (empty($signals)) { $raw_context = $this->clean_context_text($issue['context'] ?? ''); $signals = array_filter(array_map('trim', explode('. ', $raw_context))); } $signal_items = ''; foreach ($signals as $signal) { $signal = rtrim((string) $signal, '.'); if ($signal !== '') { $signal_items .= '
  • ' . esc_html($signal) . '
  • '; } } $key = $issue['key'] ?? 'generic'; if ($key === 'complex_plugin_setup') { $explanation = __('These signals together suggest a setup that benefits from periodic maintenance review.', 'aifq-site-review'); } elseif ($key === 'woo_background_tasks') { $explanation = __('These signals suggest WP-Cron or Action Scheduler reliability issues that benefit from review.', 'aifq-site-review'); } else { $explanation = $this->issue_simple_explanation($issue); } return '

    ' . esc_html__('Found signals:', 'aifq-site-review') . '

    ' . esc_html($explanation) . '

    ' . esc_html__('Advice:', 'aifq-site-review') . '
    ' . esc_html($this->display_issue_recommendation($issue)) . '

    '; } private function render_natural_issue_text($issue) { $ctx = esc_html($this->clean_context_text($issue['context'] ?? '')); $key = $issue['key'] ?? 'generic'; $advice = '

    ' . esc_html__('Advice:', 'aifq-site-review') . '
    ' . esc_html($this->display_issue_recommendation($issue)) . '

    '; $technical_detail = '

    ' . esc_html__('Technical detail:', 'aifq-site-review') . '
    ' . $ctx . '

    '; switch ($key) { case 'complex_plugin_setup': case 'woo_background_tasks': return $this->render_cluster_issue_text($issue); case 'development_settings_active': $debug_signals = []; if (!empty($issue['cluster_signals']) && is_array($issue['cluster_signals'])) { foreach ($issue['cluster_signals'] as $signal) { $signal = trim(wp_strip_all_tags((string) $signal)); if ($signal !== '') { $debug_signals[] = $signal; } } } if (empty($debug_signals)) { $raw_context = $this->clean_context_text($issue['context'] ?? ''); $debug_signals = array_filter(array_map('trim', explode('. ', $raw_context))); } $signal_items = ''; foreach ($debug_signals as $signal) { $signal = rtrim((string) $signal, '.'); if ($signal !== '') { $signal_items .= '
  • ' . esc_html($signal) . '
  • '; } } return '

    ' . esc_html__('Found:', 'aifq-site-review') . '

    ' . esc_html__('These settings are useful while troubleshooting, but should usually be reviewed and turned off or cleaned up on a live website.', 'aifq-site-review') . '

    ' . $advice; case 'multi_cache': return '

    ' . esc_html__('Multiple caching or optimization plugins are active at the same time.', 'aifq-site-review') . '

    ' . $ctx . '

    ' . esc_html__('More caching does not automatically mean more speed. Multiple caching layers can work against each other and cause slower pages, stale cache or unexpected behavior.', 'aifq-site-review') . '

    ' . $advice; case 'multi_seo': return '

    ' . esc_html__('Multiple SEO plugins are active at the same time.', 'aifq-site-review') . '

    ' . $ctx . '

    ' . esc_html__('SEO plugins often manage the same parts of a website, such as metadata, sitemaps and redirects. When more than one SEO plugin controls those areas, settings can overlap or conflict.', 'aifq-site-review') . '

    ' . esc_html__('More SEO plugins does not mean better SEO.', 'aifq-site-review') . '

    ' . $advice; case 'multi_security': return '

    ' . esc_html__('Multiple security plugins are active at the same time.', 'aifq-site-review') . '

    ' . $ctx . '

    ' . esc_html__('Security plugins can overlap in firewall, login protection or blocking rules. That can make behavior harder to predict.', 'aifq-site-review') . '

    ' . $advice; case 'heavy_plugins': return '

    ' . esc_html__('The site uses several plugins that are commonly used on larger or more dynamic sites.', 'aifq-site-review') . '

    ' . $ctx . '

    ' . esc_html__('These plugins are not automatically a problem, but together they can make caching, performance and troubleshooting more important.', 'aifq-site-review') . '

    ' . $advice; case 'woo_many_plugins': case 'woo_multi_cache': case 'elementor_addons_many': case 'elementor_addons_several': case 'elementor_woo_complexity': return '

    ' . esc_html($this->issue_preview($issue)) . '

    ' . $ctx . '

    ' . $advice; case 'many_plugins': return '

    ' . esc_html__('This site has many active plugins.', 'aifq-site-review') . '

    ' . esc_html__('Plugin count alone does not determine site health, but sites with many active plugins benefit from periodic review.', 'aifq-site-review') . '

    ' . $advice; case 'xmlrpc': return '

    ' . esc_html__('XML-RPC is available in WordPress. Many modern WordPress sites no longer need it, but your hosting provider, firewall or security layer may already block it before it is reachable from outside.', 'aifq-site-review') . '

    ' . $advice; case 'file_editor': return '

    ' . esc_html__('WordPress allows theme and plugin files to be edited from the dashboard.', 'aifq-site-review') . '

    ' . esc_html__('This increases the risk if an admin account is ever compromised.', 'aifq-site-review') . '

    ' . $advice; case 'file_mods_disabled': return '

    ' . esc_html__('WordPress file modifications are disabled from the dashboard.', 'aifq-site-review') . '

    ' . esc_html__('This may be intentional on managed setups, but it can also block normal updates.', 'aifq-site-review') . '

    ' . $advice; case 'old_wp_version': case 'core_update': case 'plugin_updates': case 'theme_update': return '

    ' . esc_html__('Updates are available for parts of the website.', 'aifq-site-review') . '

    ' . $ctx . '

    ' . esc_html__('Updates often include bug fixes, security fixes and compatibility improvements. For larger sites or webshops, make a backup first and update in a controlled way.', 'aifq-site-review') . '

    ' . $advice; case 'no_backup': return '

    ' . esc_html__('No known backup plugin was found.', 'aifq-site-review') . '

    ' . esc_html__('That does not automatically mean there are no backups. Your hosting provider may already create backups.', 'aifq-site-review') . '

    ' . $advice; case 'no_seo': case 'no_security': case 'no_cache': case 'plain_permalinks': case 'cache_signal_no_plugin': case 'no_object_cache_dropin': case 'site_url_home_mismatch': case 'wp_debug_display': case 'debug_log_found': return '

    ' . esc_html($this->issue_preview($issue)) . '

    ' . $advice; case 'large_autoloaded_options': return '

    ' . esc_html__('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') . '

    ' . esc_html__('This is usually not something visitors can see directly, but it can add hidden weight behind the scenes.', 'aifq-site-review') . '

    ' . esc_html__('This usually needs a careful review of plugin data, not a quick delete-all cleanup.', 'aifq-site-review') . '

    ' . $advice . $technical_detail; case 'large_images': return '

    ' . esc_html__('Some images on this site are quite large. Large image files can make pages load slower, especially on mobile connections.', 'aifq-site-review') . '

    ' . $advice . $technical_detail; case 'search_visibility_disabled': return '

    ' . esc_html__('WordPress is currently asking search engines not to index this site.', 'aifq-site-review') . '

    ' . esc_html__('This is normal for staging or private sites, but it can seriously hurt visibility if the site is meant to be public.', 'aifq-site-review') . '

    ' . $advice; default: return '

    ' . esc_html($this->issue_preview($issue)) . '

    ' . $advice; } } 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 '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 caching plugin, WP_CACHE constant or cache drop-in. Your hosting provider may still have server-level 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 caching signal exists, but no active caching 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 caching and performance attention.', '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 short_action_title($issue) { switch ($issue['key'] ?? 'generic') { case 'development_settings_active': return __('Clean up development settings.', 'aifq-site-review'); case 'complex_plugin_setup': return __('Review the plugin setup with your developer.', 'aifq-site-review'); case 'woo_background_tasks': return __('Review WooCommerce background tasks.', 'aifq-site-review'); case 'multi_cache': return __('Bring caching back to one solution.', 'aifq-site-review'); case 'multi_seo': return __('Choose one SEO plugin as the main solution.', 'aifq-site-review'); case 'core_update': case 'plugin_updates': case 'theme_update': return __('Update the pending items in a controlled way.', 'aifq-site-review'); case 'heavy_plugins': return __('Review whether each heavier plugin still has a clear purpose.', 'aifq-site-review'); case 'many_plugins': return __('Review whether all active plugins are still needed and not overlapping with another plugin.', 'aifq-site-review'); default: return $this->display_issue_recommendation($issue); } } private function short_action_reason($issue) { switch ($issue['key'] ?? 'generic') { case 'development_settings_active': return __('Debugging settings and an error log are active on this live site.', 'aifq-site-review'); case 'complex_plugin_setup': return __('Several plugin-complexity signals are present together.', 'aifq-site-review'); case 'woo_background_tasks': return __('Failed and delayed WooCommerce scheduled actions are present together.', 'aifq-site-review'); case 'multi_cache': return __('Multiple caching or optimization plugins are active right now.', 'aifq-site-review'); case 'multi_seo': return __('Multiple SEO plugins are active right now.', 'aifq-site-review'); case 'core_update': case 'plugin_updates': case 'theme_update': return __('Create a backup first, especially on larger websites or webshops.', 'aifq-site-review'); default: return $this->display_issue_title($issue); } } 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', ]; return in_array($issue['key'] ?? '', $high_confidence_keys, true) ? 'high' : 'medium'; } private function issue_time_estimate($issue) { switch ($issue['key'] ?? 'generic') { case 'multi_cache': case 'no_cache_layer': case 'no_object_cache_dropin': case 'site_url_home_mismatch': case 'wp_debug_display': case 'savequeries_enabled': case 'development_settings_active': case 'complex_plugin_setup': case 'woo_background_tasks': case 'default_admin_username': case 'public_user_registration': case 'woo_no_mail_delivery_plugin': case 'woo_failed_scheduled_actions': case 'woo_scheduled_actions_backlog': case 'multiple_page_builders': case 'woo_multi_cache': case 'multi_seo': case 'plain_permalinks': case 'search_visibility_disabled': return __('±5 minutes', 'aifq-site-review'); case 'old_wp_version': case 'core_update': case 'plugin_updates': case 'theme_update': return __('±10 minutes', 'aifq-site-review'); case 'old_php': case 'old_php_updates': case 'low_memory': case 'memory_plugin_conflict': return __('±10-20 minutes', 'aifq-site-review'); case 'large_images': return __('±10-30 minutes', 'aifq-site-review'); case 'no_backup': return __('±5-10 minutes', 'aifq-site-review'); default: return __('±2 minutes', 'aifq-site-review'); } } 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 __('Caching stores temporary versions of pages. It can help performance, but multiple caching plugins or leftover cache signals can be confusing.', '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 '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 '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'); default: return __('This can affect stability, security or speed.', 'aifq-site-review'); } } private function issue_if_ignored($issue) { if ($issue['severity'] === self::SEV_HIGH) { return __('The chance of delay, conflicts or extra maintenance stays higher while this remains unchanged.', 'aifq-site-review'); } if ($issue['severity'] === self::SEV_MEDIUM) { return __('Over time, this can make conflicts or extra maintenance more likely.', 'aifq-site-review'); } return __('This is usually not urgent, but cleaning it up can make the WordPress setup easier to understand.', '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 clear caching layer detected', 'aifq-site-review'); case 'no_cache_layer': return __('No clear caching layer detected', 'aifq-site-review'); case 'cache_signal_no_plugin': return __('Cache signal without active cache plugin', 'aifq-site-review'); case 'no_object_cache_dropin': return __('No persistent object cache detected', '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 '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 caching plugin 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 is not active in WordPress admin', 'aifq-site-review'); case 'site_url_home_mismatch': return __('WordPress Address and Site Address are different', '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 __('Check with your hosting provider first — server-level caching may already be active. If not, consider one caching solution that fits your hosting.', 'aifq-site-review'); case 'no_cache_layer': return __('Check whether your hosting already uses server-level caching. If not, consider one caching solution that fits your hosting.', 'aifq-site-review'); case 'cache_signal_no_plugin': return __('Check whether this comes from hosting-level caching, WP_CACHE or leftover cache files from an old cache plugin.', '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 '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 __('Consider one SEO plugin if search visibility matters for this website.', '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 __('Check what protection your hosting provider already includes. If needed, consider one reliable security plugin for basic monitoring. A security plugin is a helpful signal layer, not a guarantee against all threats.', '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-level caching may already be active. If not, use one WooCommerce-safe caching 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 __('Check whether HTTPS is configured correctly for WordPress admin and the public site.', '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 '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 __('Check whether reliable backups are available through your hosting, or use a separate backup solution.', '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 a server-level caching layer separate from normal page caching plugins.', '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 known_cache_plugins() { return [ 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache', 'wp-super-cache/wp-cache.php' => 'WP Super Cache', 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache', 'wp-rocket/wp-rocket.php' => 'WP Rocket', 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache', 'autoptimize/autoptimize.php' => 'Autoptimize', 'sg-cachepress/sg-cachepress.php' => 'SiteGround Optimizer', 'hummingbird-performance/wp-hummingbird.php'=> 'Hummingbird', 'breeze/breeze.php' => 'Breeze', 'swift-performance-lite/performance.php' => 'Swift Performance', 'nitropack/main.php' => 'NitroPack', 'perfmatters/perfmatters.php' => 'Perfmatters', 'flying-press/flying-press.php' => 'FlyingPress', ]; } private function known_seo_plugins() { return [ 'wordpress-seo/wp-seo.php' => 'Yoast SEO', 'seo-by-rank-math/rank-math.php' => 'Rank Math SEO', 'all-in-one-seo-pack/all_in_one_seo_pack.php' => 'All in One SEO', 'autodescription/autodescription.php' => 'The SEO Framework', 'wp-seopress/seopress.php' => 'SEOPress', 'slim-seo/slim-seo.php' => 'Slim SEO', 'squirrly-seo/squirrly.php' => 'Squirrly SEO', ]; } private function known_security_plugins() { return [ 'wordfence/wordfence.php' => 'Wordfence', 'better-wp-security/better-wp-security.php' => 'Solid Security', 'all-in-one-wp-security-and-firewall/wp-security.php' => 'All-In-One Security', 'sucuri-scanner/sucuri.php' => 'Sucuri Security', 'wp-cerber/wp-cerber.php' => 'WP Cerber', 'limit-login-attempts-reloaded/limit-login-attempts-reloaded.php' => 'Limit Login Attempts Reloaded', 'really-simple-ssl/rlrsssl-really-simple-ssl.php' => 'Really Simple Security', ]; } private function known_heavy_plugins() { return [ 'woocommerce/woocommerce.php' => 'WooCommerce', 'elementor/elementor.php' => 'Elementor', 'elementor-pro/elementor-pro.php' => 'Elementor Pro', 'sitepress-multilingual-cms/sitepress.php' => 'WPML', 'wpml-string-translation/plugin.php' => 'WPML String Translation', 'polylang/polylang.php' => 'Polylang', 'revslider/revslider.php' => 'Slider Revolution', 'js_composer/js_composer.php' => 'WPBakery', 'jetpack/jetpack.php' => 'Jetpack', 'advanced-custom-fields-pro/acf.php' => 'ACF Pro', ]; } private function known_backup_plugins() { return [ 'updraftplus/updraftplus.php' => 'UpdraftPlus', 'backwpup/backwpup.php' => 'BackWPup', 'duplicator/duplicator.php' => 'Duplicator', 'all-in-one-wp-migration/all-in-one-wp-migration.php' => 'All-in-One WP Migration', 'wp-staging/wp-staging.php' => 'WP STAGING', 'blogvault-real-time-backup/blogvault.php' => 'BlogVault', 'backupbuddy/backupbuddy.php' => 'BackupBuddy', 'boldgrid-backup/boldgrid-backup.php' => 'Total Upkeep', ]; } private function detect_plugins_by_list(array $active_plugins, array $known) { $found = []; foreach ($known as $file => $label) { if (in_array($file, $active_plugins, true)) $found[$file] = $label; } return $found; } private function detect_elementor_plugins(array $active_plugins) { $found = []; $keywords = ['elementor', 'essential-addons', 'premium-addons', 'happy-elementor', 'exclusive-addons']; $non_addon_slugs = ['elementor', 'elementor-pro']; foreach ($active_plugins as $file) { $slug = strtolower((string) strtok((string) $file, '/')); if (in_array($slug, $non_addon_slugs, true)) { continue; } foreach ($keywords as $kw) { if (strpos((string) $file, $kw) !== false) { $found[$file] = $this->pretty_plugin_name($file); break; } } } return $found; } private function detect_cache_plugin(array $active_plugins) { if (defined('WP_CACHE') && WP_CACHE === true) return true; foreach (array_keys($this->known_cache_plugins()) as $plugin) { if (in_array($plugin, $active_plugins, true)) return true; } return false; } private function plugin_names(array $plugins) { return array_values($plugins); } private function pretty_plugin_name($plugin_file) { return ucwords(str_replace(['-', '_'], ' ', dirname($plugin_file))); } // ------------------------------------------------------------------------- // Low-level data helpers // ------------------------------------------------------------------------- private function count_core_updates($core_updates) { if (!is_array($core_updates)) return 0; $count = 0; foreach ($core_updates as $update) { if (isset($update->response) && $update->response === 'upgrade') $count++; } return $count; } private function debug_log_path() { if (defined('WP_DEBUG_LOG') && is_string(WP_DEBUG_LOG) && WP_DEBUG_LOG !== '1') { return WP_DEBUG_LOG; } return WP_CONTENT_DIR . '/debug.log'; } private function debug_log_exists() { $path = $this->debug_log_path(); return is_string($path) && file_exists($path); } private function debug_log_size() { $path = $this->debug_log_path(); if (!is_string($path) || !file_exists($path)) { return 0; } $size = filesize($path); return $size === false ? 0 : (int) $size; } private function get_autoloaded_options_size() { $cached = get_transient('aifq_sr_autoloaded_options_size'); if ($cached !== false) { return (int) $cached; } global $wpdb; $autoload_values = ['yes', 'on', 'auto-on', 'auto']; $autoload_values = array_values(array_unique(array_filter(array_map('strval', (array) $autoload_values)))); if (empty($autoload_values)) { set_transient('aifq_sr_autoloaded_options_size', 0, 5 * MINUTE_IN_SECONDS); return 0; } $placeholders = implode(', ', array_fill(0, count($autoload_values), '%s')); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core options table name is supplied by WordPress; autoload values use placeholders. $sql = "SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options} WHERE autoload IN ($placeholders)"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Dynamic placeholder count is prepared immediately below; result is cached in a transient. $size = (int) $wpdb->get_var($wpdb->prepare($sql, ...$autoload_values)); set_transient('aifq_sr_autoloaded_options_size', $size, 5 * MINUTE_IN_SECONDS); return $size; } private function count_post_revisions() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Local diagnostic count only; no user data is exposed. return (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s", 'revision') ); } private function count_large_images() { $cached = get_transient('aifq_sr_large_images_count'); if ($cached !== false) { return (int) $cached; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Local diagnostic sample only; result is cached in a transient. $rows = $wpdb->get_col( $wpdb->prepare( "SELECT pm.meta_value FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = %s AND p.post_type = %s AND p.post_status = %s ORDER BY p.ID DESC LIMIT 200", '_wp_attachment_metadata', 'attachment', 'inherit' ) ); $large = 0; foreach ($rows as $row) { $meta = $this->safe_attachment_metadata($row); if (!is_array($meta)) continue; // Use stored filesize when available (WP 6.0+) if (isset($meta['filesize']) && (int) $meta['filesize'] > 1024 * 1024) { $large++; } elseif (!isset($meta['filesize'])) { // Fallback: very wide/tall images are typically > 1 MB $w = isset($meta['width']) ? (int) $meta['width'] : 0; $h = isset($meta['height']) ? (int) $meta['height'] : 0; if ($w > 2500 || $h > 2500) $large++; } } set_transient('aifq_sr_large_images_count', $large, 5 * MINUTE_IN_SECONDS); return $large; } // ------------------------------------------------------------------------- // 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; } } 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) { // Guard: previous scan must have a created_at timestamp. $prev_date = (string) ($previous_scan['created_at'] ?? ''); $curr_date = (string) ($current_scan['created_at'] ?? ''); if ($prev_date === '' || $curr_date === '') { return null; } // Guard: do not compare a scan with itself (double-click edge case). if ($prev_date === $curr_date) { return null; } // Guard: previous scan must be older than current scan. $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; } // Guard: previous scan needs at minimum a version and subscores to be useful. // We require version >= 1.1.0 to guarantee stable issue keys. $prev_version = (string) ($previous_scan['version'] ?? '0.0.0'); if (version_compare($prev_version, '1.1.0', '<')) { return null; } // Scores. $prev_subscores = (isset($previous_scan['subscores']) && is_array($previous_scan['subscores'])) ? $previous_scan['subscores'] : []; $curr_subscores = (isset($current_scan['subscores']) && is_array($current_scan['subscores'])) ? $current_scan['subscores'] : []; $previous_score = $prev_subscores ? $this->calculate_total_score($prev_subscores) : (int) ($previous_scan['score'] ?? 0); $current_score = $curr_subscores ? $this->calculate_total_score($curr_subscores) : (int) ($current_scan['score'] ?? 0); $score_delta = $current_score - $previous_score; // Area score changes — only for the four known categories. $categories = ['Performance', 'Security', 'Maintenance', 'Health']; $area_changes = []; foreach ($categories as $cat) { $prev_cat = isset($prev_subscores[$cat]) ? (int) $prev_subscores[$cat] : null; $curr_cat = isset($curr_subscores[$cat]) ? (int) $curr_subscores[$cat] : null; if ($prev_cat !== null && $curr_cat !== null) { $area_changes[$cat] = $curr_cat - $prev_cat; } else { $area_changes[$cat] = null; // Not comparable for this area. } } // Issue key sets — compare using stable keys. // Build key → full-object maps so titles can be retrieved safely. // IMPORTANT: resolved items use previous scan data; new items use current scan data. $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 issues: existed before, gone now — title from previous scan. $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 issues: exists now, was not there before — title from current scan. $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) ))); // Suggestion key sets. $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) { $k = (string) ($suggestion['key'] ?? ''); if ($k !== '') $prev_suggestions_by_key[$k] = $suggestion; } $curr_suggestions_by_key = []; foreach ($curr_suggestions as $suggestion) { $k = (string) ($suggestion['key'] ?? ''); if ($k !== '') $curr_suggestions_by_key[$k] = $suggestion; } // Resolved suggestions: existed before, gone now — title from previous scan. $resolved_suggestion_objs = array_values(array_intersect_key( $prev_suggestions_by_key, array_diff_key($prev_suggestions_by_key, $curr_suggestions_by_key) )); // New suggestions: exists now, was not there before — title from current scan. $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_score' => $previous_score, 'current_score' => $current_score, 'score_delta' => $score_delta, 'previous_date' => $prev_date, 'area_changes' => $area_changes, // Counts (kept for backward-compat with existing rendering). 'new_issues_count' => count($new_issue_objs), 'resolved_issues_count' => count($resolved_issue_objs), 'new_suggestions_count' => count($new_suggestion_objs), 'resolved_suggestions_count' => count($resolved_suggestion_objs), // Full item objects for "View changes" rendering. 'new_issues' => $new_issue_objs, 'resolved_issues' => $resolved_issue_objs, 'new_suggestions' => $new_suggestion_objs, 'resolved_suggestions' => $resolved_suggestion_objs, ]; } /** * Render the compact "Since last scan" comparison block. * * Outputs nothing if $diff is null. * Placed inside the score card, after the last-scan date line. * * @param array|null $diff Result of calculate_scan_diff(), or null. */ private function render_scan_diff($diff) { if (!is_array($diff) || empty($diff['is_comparable'])) { // Show a first-scan prompt so the block is not simply absent. ?>

    format_scan_date($diff['previous_date']); $area_changes = is_array($diff['area_changes']) ? $diff['area_changes'] : []; $new_issues = (int) ($diff['new_issues_count'] ?? 0); $res_issues = (int) ($diff['resolved_issues_count'] ?? 0); $new_sugg = (int) ($diff['new_suggestions_count'] ?? 0); $res_sugg = (int) ($diff['resolved_suggestions_count'] ?? 0); $has_any_change = ($delta !== 0 || $new_issues > 0 || $res_issues > 0 || $new_sugg > 0 || $res_sugg > 0); $change_sections = []; $section_sources = [ [ 'label' => __('Resolved attention points', 'aifq-site-review'), 'items' => (isset($diff['resolved_issues']) && is_array($diff['resolved_issues'])) ? $diff['resolved_issues'] : [], ], [ 'label' => __('New attention points', 'aifq-site-review'), 'items' => (isset($diff['new_issues']) && is_array($diff['new_issues'])) ? $diff['new_issues'] : [], ], [ 'label' => __('Opportunities that appear resolved', 'aifq-site-review'), 'items' => (isset($diff['resolved_suggestions']) && is_array($diff['resolved_suggestions'])) ? $diff['resolved_suggestions'] : [], ], [ 'label' => __('New opportunities', 'aifq-site-review'), 'items' => (isset($diff['new_suggestions']) && is_array($diff['new_suggestions'])) ? $diff['new_suggestions'] : [], ], ]; foreach ($section_sources as $section) { $titles = []; foreach ($section['items'] as $item) { if (!is_array($item)) { continue; } $title = $this->display_issue_title($item); if ($title !== '') { $titles[] = $title; } } if (!empty($titles)) { $change_sections[] = [ 'label' => $section['label'], 'titles' => array_slice($titles, 0, 5), 'more' => max(0, count($titles) - 5), ]; } } ?>

    0): ?>

    0) { $issue_parts[] = sprintf( /* translators: %d = number of new attention points */ _n('%d new attention point', '%d new attention points', $new_issues, 'aifq-site-review'), $new_issues ); } if ($res_issues > 0) { $issue_parts[] = sprintf( /* translators: %d = number of attention points that appear resolved */ _n('%d attention point appears resolved', '%d attention points appear resolved', $res_issues, 'aifq-site-review'), $res_issues ); } if ($new_sugg > 0) { $issue_parts[] = sprintf( /* translators: %d = number of new opportunities */ _n('%d new opportunity', '%d new opportunities', $new_sugg, 'aifq-site-review'), $new_sugg ); } if ($res_sugg > 0) { $issue_parts[] = sprintf( /* translators: %d = number of opportunities that appear resolved */ _n('%d opportunity appears resolved', '%d opportunities appear resolved', $res_sugg, 'aifq-site-review'), $res_sugg ); } if (!empty($issue_parts)): ?>

    $this->category_label('Performance'), 'Security' => $this->category_label('Security'), 'Maintenance' => $this->category_label('Maintenance'), 'Health' => $this->category_label('Health'), ]; foreach ($area_labels as $key => $label) { if (!array_key_exists($key, $area_changes) || $area_changes[$key] === null) { continue; } $change = (int) $area_changes[$key]; if ($change > 0) { $area_parts[] = $label . ' +' . $change; } elseif ($change < 0) { $area_parts[] = $label . ' ' . $change; } } if (!empty($area_parts)): ?>