';
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') . '
' . $signal_items . '
' . 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.
?>