__( 'WordPress + PHP environment', 'ai-o-optimiser' ), 'plugins' => __( 'Plugin + schema stack', 'ai-o-optimiser' ), 'cache' => __( 'Cache / CDN / server layer', 'ai-o-optimiser' ), 'guidance' => __( 'Robots / llms / sitemap ownership', 'ai-o-optimiser' ), 'security' => __( 'Security / WAF risk hints', 'ai-o-optimiser' ), 'content' => __( 'Content architecture / post types', 'ai-o-optimiser' ), 'storage' => __( 'Database / storage guardrails', 'ai-o-optimiser' ), ); } /** * Default report payload. * * @return array */ public static function empty_report() { return array( 'checked_at' => '', 'overall' => 'not_checked', 'source' => 'manual_chunks', 'chunks' => array(), 'version' => AI_O_Optimiser_VERSION, ); } /** * Read cached report. * * @return array */ public static function report() { $report = get_option( self::OPTION, array() ); if ( empty( $report ) || ! is_array( $report ) ) { return self::empty_report(); } $report = wp_parse_args( $report, self::empty_report() ); if ( empty( $report['chunks'] ) || ! is_array( $report['chunks'] ) ) { $report['chunks'] = array(); } return $report; } /** * Run one chunk and merge it into the cached report. * * @param string $chunk Chunk key. * @return array */ public static function run_chunk( $chunk ) { $chunk = sanitize_key( $chunk ); $chunks = self::chunks(); if ( ! isset( $chunks[ $chunk ] ) ) { return self::report(); } $method = 'check_' . $chunk; if ( ! is_callable( array( __CLASS__, $method ) ) ) { return self::report(); } $report = self::report(); $report['source'] = 'manual_chunks'; $report['chunks'][ $chunk ] = call_user_func( array( __CLASS__, $method ) ); $report['checked_at'] = current_time( 'mysql', true ); $report['overall'] = self::derive_overall( $report['chunks'] ); $report['version'] = AI_O_Optimiser_VERSION; update_option( self::OPTION, $report, false ); return $report; } /** * Run all defined chunks. Still called manually only. * * @return array */ public static function run_all_chunks() { $report = self::report(); $report['source'] = 'manual_chunks'; foreach ( array_keys( self::chunks() ) as $chunk ) { $method = 'check_' . $chunk; if ( is_callable( array( __CLASS__, $method ) ) ) { $report['chunks'][ $chunk ] = call_user_func( array( __CLASS__, $method ) ); } } $report['checked_at'] = current_time( 'mysql', true ); $report['overall'] = self::derive_overall( $report['chunks'] ); $report['version'] = AI_O_Optimiser_VERSION; update_option( self::OPTION, $report, false ); return $report; } /** * Admin handler for manual chunk execution. */ public function handle_run_chunk() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to run this check.', 'ai-o-optimiser' ) ); } check_admin_referer( 'ai_o_optimiser_run_environment_chunk' ); $chunk = isset( $_POST['chunk'] ) ? sanitize_key( wp_unslash( $_POST['chunk'] ) ) : ''; if ( 'all' === $chunk ) { self::run_all_chunks(); } else { self::run_chunk( $chunk ); } wp_safe_redirect( admin_url( 'admin.php?page=ai-o-optimiser-environment-check&updated=1' ) ); exit; } /** * Admin handler for pasted Site Health debug info. */ public function handle_analyse_paste() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to analyse this report.', 'ai-o-optimiser' ) ); } check_admin_referer( 'ai_o_optimiser_analyse_site_health_paste' ); $raw = isset( $_POST['site_health_debug'] ) ? wp_unslash( $_POST['site_health_debug'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitised before interpretation; raw text is not stored. self::analyse_pasted_debug_info( $raw ); wp_safe_redirect( admin_url( 'admin.php?page=ai-o-optimiser-environment-check&pasted=1' ) ); exit; } /** * Admin handler to clear stored interpreted report. */ public function handle_clear_report() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to clear this report.', 'ai-o-optimiser' ) ); } check_admin_referer( 'ai_o_optimiser_clear_environment_check' ); delete_option( self::OPTION ); wp_safe_redirect( admin_url( 'admin.php?page=ai-o-optimiser-environment-check&cleared=1' ) ); exit; } /** * Build a report from pasted Site Health Debug text. * * Raw pasted text is deliberately not stored. Only interpreted findings are saved. * * @param string $raw Raw pasted debug text. * @return array */ public static function analyse_pasted_debug_info( $raw ) { $raw = self::limit_text( self::normalise_line_endings( (string) $raw ), 350000 ); $parsed = self::parse_site_health_debug_text( $raw ); $items = self::findings_from_pasted_debug( $parsed ); $chunk = self::chunk_payload( 'pasted_site_health', __( 'Pasted WordPress Site Health Debug', 'ai-o-optimiser' ), $items, __( 'Source: manually pasted WordPress Site Health Debug text. Raw pasted content was not stored.', 'ai-o-optimiser' ) ); $report = self::empty_report(); $report['source'] = 'pasted_site_health'; $report['chunks'] = array( 'pasted_site_health' => $chunk ); $report['checked_at'] = current_time( 'mysql', true ); $report['overall'] = self::derive_overall( $report['chunks'] ); $report['version'] = AI_O_Optimiser_VERSION; update_option( self::OPTION, $report, false ); return $report; } /** * WordPress + PHP runtime chunk. * * @return array */ private static function check_environment() { $items = array(); $env_type = function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : 'production'; $php_memory = (string) ini_get( 'memory_limit' ); $php_max_execution = (string) ini_get( 'max_execution_time' ); $php_max_input = (string) ini_get( 'max_input_time' ); $php_post_max = (string) ini_get( 'post_max_size' ); $php_upload_max = (string) ini_get( 'upload_max_filesize' ); $php_max_input_vars = (string) ini_get( 'max_input_vars' ); $php_max_file_uploads = (string) ini_get( 'max_file_uploads' ); $php_timezone = (string) ini_get( 'date.timezone' ); $php_log_errors_raw = (string) ini_get( 'log_errors' ); $wp_memory = defined( 'WP_MEMORY_LIMIT' ) ? (string) WP_MEMORY_LIMIT : ''; $wp_max_memory = defined( 'WP_MAX_MEMORY_LIMIT' ) ? (string) WP_MAX_MEMORY_LIMIT : ''; $php_memory_bytes = self::memory_to_bytes( $php_memory ); $wp_debug = defined( 'WP_DEBUG' ) && WP_DEBUG; $wp_debug_display = defined( 'WP_DEBUG_DISPLAY' ) ? (bool) WP_DEBUG_DISPLAY : true; $php_display_raw = (string) ini_get( 'display_errors' ); $php_display = self::ini_flag_enabled( $php_display_raw ); $debug_display_active = $php_display || ( $wp_debug && $wp_debug_display ); $wp_cache = defined( 'WP_CACHE' ) && WP_CACHE; $items[] = self::item( __( 'WordPress version', 'ai-o-optimiser' ), 'pass', get_bloginfo( 'version' ), __( 'Record only. Compatibility should be reviewed after major WordPress upgrades.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'PHP version', 'ai-o-optimiser' ), version_compare( PHP_VERSION, '8.0', '>=' ) ? 'pass' : 'warning', PHP_VERSION, __( 'PHP 8+ is preferred for modern WordPress stacks and plugin-heavy sites.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'Environment type', 'ai-o-optimiser' ), 'production' === $env_type ? 'pass' : 'warning', $env_type, 'production' === $env_type ? __( 'Production environment reported.', 'ai-o-optimiser' ) : __( 'Do not treat local/staging audit results as final production crawlability evidence.', 'ai-o-optimiser' ) ); $debug_finding = sprintf( /* translators: 1: PHP display_errors state, 2: WP_DEBUG state, 3: WP_DEBUG_DISPLAY state. */ __( 'Observed PHP runtime display_errors: %1$s; WP_DEBUG: %2$s; WP_DEBUG_DISPLAY: %3$s', 'ai-o-optimiser' ), $php_display ? __( 'On', 'ai-o-optimiser' ) : __( 'Off', 'ai-o-optimiser' ), $wp_debug ? 'true' : 'false', $wp_debug_display ? 'true' : 'false' ); $items[] = self::item( __( 'Debug display', 'ai-o-optimiser' ), $debug_display_active ? 'warning' : 'pass', $debug_finding, $debug_display_active ? __( 'Public PHP/debug output can distort rendered-page audits and expose implementation details. Disable display output in the environment that owns PHP configuration; do not add duplicate wp-config.php settings solely to satisfy the audit. This finding is based on the runtime state visible to WordPress.', 'ai-o-optimiser' ) : __( 'Observed runtime display output is off for this request. A WP_DEBUG_DISPLAY constant set to true is not treated as a problem when WP_DEBUG is false and PHP display_errors is off. This reports the PHP state visible to WordPress, not a hosting control-panel setting.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'Page cache', 'ai-o-optimiser' ), $wp_cache ? 'context' : 'pass', $wp_cache ? __( 'WP_CACHE is enabled', 'ai-o-optimiser' ) : __( 'WP_CACHE is not enabled', 'ai-o-optimiser' ), $wp_cache ? __( 'Caching is normally desirable on production sites. AI-O Optimiser runtime crawler counts include only requests that reach WordPress, so page/CDN/server cache can make visit totals directional. Do not disable caching or exclude important public pages merely to increase tracker counts.', 'ai-o-optimiser' ) : __( 'No WordPress page-cache constant is enabled. External CDN, reverse-proxy or server caching may still exist.', 'ai-o-optimiser' ) ); $memory_parts = array(); $memory_parts[] = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'Observed PHP runtime memory_limit: %s', 'ai-o-optimiser' ), $php_memory ? $php_memory : __( 'Not reported', 'ai-o-optimiser' ) ); if ( $wp_memory ) { $memory_parts[] = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'WP_MEMORY_LIMIT: %s', 'ai-o-optimiser' ), $wp_memory ); } if ( $wp_max_memory ) { $memory_parts[] = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'WP_MAX_MEMORY_LIMIT: %s', 'ai-o-optimiser' ), $wp_max_memory ); } $memory_low = $php_memory_bytes > 0 && $php_memory_bytes < 67108864; $items[] = self::item( __( 'Memory availability', 'ai-o-optimiser' ), $memory_low ? 'warning' : 'pass', implode( '; ', $memory_parts ), $memory_low ? __( 'The PHP memory_limit observed by this WordPress request is low for large audits. Raise the active limit in the infrastructure or application layer that owns PHP configuration.', 'ai-o-optimiser' ) : __( 'The audit is judged against the PHP memory_limit observed by the current WordPress request. This is runtime evidence, not a claim about the value configured in a hosting control panel. A lower WordPress default constant does not by itself mean PHP has been reduced to that value.', 'ai-o-optimiser' ) ); $runtime_parts = array( sprintf( 'max_execution_time=%s', '' !== $php_max_execution ? $php_max_execution : __( 'Not reported', 'ai-o-optimiser' ) ), sprintf( 'max_input_time=%s', '' !== $php_max_input ? $php_max_input : __( 'Not reported', 'ai-o-optimiser' ) ), sprintf( 'post_max_size=%s', '' !== $php_post_max ? $php_post_max : __( 'Not reported', 'ai-o-optimiser' ) ), sprintf( 'upload_max_filesize=%s', '' !== $php_upload_max ? $php_upload_max : __( 'Not reported', 'ai-o-optimiser' ) ), sprintf( 'max_input_vars=%s', '' !== $php_max_input_vars ? $php_max_input_vars : __( 'Not reported', 'ai-o-optimiser' ) ), sprintf( 'max_file_uploads=%s', '' !== $php_max_file_uploads ? $php_max_file_uploads : __( 'Not reported', 'ai-o-optimiser' ) ), sprintf( 'date.timezone=%s', '' !== $php_timezone ? $php_timezone : __( 'Not set in PHP', 'ai-o-optimiser' ) ), sprintf( 'log_errors=%s', self::ini_flag_enabled( $php_log_errors_raw ) ? 'On' : 'Off' ), sprintf( 'display_errors=%s', $php_display ? 'On' : 'Off' ) ); $items[] = self::item( __( 'Observed PHP runtime settings', 'ai-o-optimiser' ), 'context', implode( '; ', $runtime_parts ), __( 'Source: the current PHP request as visible to WordPress via ini_get(). These values describe runtime behaviour, not the hosting provider control panel. If the panel shows different values, review inheritance, active overrides and configuration precedence before changing WordPress.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'Site timezone', 'ai-o-optimiser' ), 'pass', wp_timezone_string() ? wp_timezone_string() : 'UTC offset ' . get_option( 'gmt_offset' ), __( 'Used for audit recency and dashboard time labels.', 'ai-o-optimiser' ) ); return self::chunk_payload( 'environment', __( 'WordPress + PHP environment', 'ai-o-optimiser' ), $items ); } /** * Plugin and schema stack chunk. * * @return array */ private static function check_plugins() { $plugins = self::active_plugins_with_data(); $names = self::plugin_names_blob( $plugins ); $settings = wp_parse_args( get_option( 'ai_o_optimiser_settings', array() ), AI_O_Optimiser_Settings::defaults() ); $provider_mode = isset( $settings['llms_provider_mode'] ) ? sanitize_key( $settings['llms_provider_mode'] ) : 'auto'; $count = count( $plugins ); $items = array(); $items[] = self::item( __( 'Active plugin count', 'ai-o-optimiser' ), $count >= 35 ? 'warning' : 'pass', (string) $count, __( 'Plugin-heavy sites should run audits in chunks and treat compatibility warnings seriously.', 'ai-o-optimiser' ) ); $rank_math_active = self::has_plugin_named( $names, array( 'rank math seo' ) ); $rank_math_llms = self::rank_math_llms_module_active(); $items[] = self::item( __( 'Rank Math SEO', 'ai-o-optimiser' ), $rank_math_active ? 'context' : 'pass', $rank_math_active ? __( 'Detected', 'ai-o-optimiser' ) : __( 'Not detected', 'ai-o-optimiser' ), $rank_math_active ? __( 'If Rank Math owns schema or llms.txt, AI-O Optimiser should audit/fix only where needed and avoid duplicate ownership.', 'ai-o-optimiser' ) : __( 'No Rank Math-specific compatibility issue detected.', 'ai-o-optimiser' ) ); if ( $rank_math_active && ! empty( $settings['llms_generate_enabled'] ) ) { $ownership_status = in_array( $provider_mode, array( 'auto', 'rank_math' ), true ) ? 'suggestion' : 'warning'; $ownership_finding = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'Provider mode: %s', 'ai-o-optimiser' ), $provider_mode ); if ( 'act' === $provider_mode ) { $ownership_finding = $rank_math_llms ? __( 'Rank Math llms.txt module likely active; ACT is forced to own llms.txt', 'ai-o-optimiser' ) : __( 'Rank Math detected; ACT is forced to own llms.txt', 'ai-o-optimiser' ); } $items[] = self::item( __( 'llms.txt ownership', 'ai-o-optimiser' ), $ownership_status, $ownership_finding, __( 'Use one provider for /llms.txt. Auto or Rank Math-owned mode avoids ACT taking over Rank Math llms.txt while still allowing the ACT header fix/audit.', 'ai-o-optimiser' ) ); } $schema_engines = array(); if ( $rank_math_active ) { $schema_engines[] = 'Rank Math'; } if ( self::has_plugin_named( $names, array( 'yoast seo' ) ) ) { $schema_engines[] = 'Yoast SEO'; } if ( self::has_plugin_named( $names, array( 'schema pro', 'aioseo', 'all in one seo' ) ) ) { $schema_engines[] = 'Other SEO/schema plugin'; } $items[] = self::item( __( 'Schema engines', 'ai-o-optimiser' ), count( $schema_engines ) > 1 ? 'warning' : 'pass', $schema_engines ? implode( ', ', $schema_engines ) : __( 'No major SEO schema engine detected', 'ai-o-optimiser' ), count( $schema_engines ) > 1 ? __( 'Avoid broad managed schema fallbacks when another schema engine already outputs JSON-LD.', 'ai-o-optimiser' ) : __( 'Schema fallback risk appears manageable; still verify rendered JSON-LD before applying fallbacks.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'ACF / CPT tooling', 'ai-o-optimiser' ), self::has_plugin_named( $names, array( 'advanced custom fields', 'custom post type ui' ) ) ? 'suggestion' : 'pass', self::has_plugin_named( $names, array( 'advanced custom fields', 'custom post type ui' ) ) ? __( 'Detected', 'ai-o-optimiser' ) : __( 'Not detected', 'ai-o-optimiser' ), __( 'Custom fields and custom post types can contain important entity data; include pages, posts and public CPTs in Schema Dash review.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'Form plugins', 'ai-o-optimiser' ), self::has_plugin_named( $names, array( 'formidable', 'gravity forms', 'contact form 7', 'wpforms' ) ) ? 'suggestion' : 'pass', self::has_plugin_named( $names, array( 'formidable', 'gravity forms', 'contact form 7', 'wpforms' ) ) ? __( 'Detected', 'ai-o-optimiser' ) : __( 'Not detected', 'ai-o-optimiser' ), __( 'No direct conflict expected. Contact/conversion forms should not be crawled aggressively during public audits.', 'ai-o-optimiser' ) ); return self::chunk_payload( 'plugins', __( 'Plugin + schema stack', 'ai-o-optimiser' ), $items ); } /** * Cache / CDN / server chunk. * * @return array */ private static function check_cache() { $plugins = self::active_plugins_with_data(); $names = self::plugin_names_blob( $plugins ); $items = array(); $server = isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : ''; $advanced_cache = file_exists( WP_CONTENT_DIR . '/advanced-cache.php' ); $cache_detected = self::has_plugin_named( $names, array( 'wp rocket', 'litespeed cache', 'w3 total cache', 'wp super cache', 'breeze', 'siteground optimizer', 'cloudflare' ) ); $items[] = self::item( __( 'Cache plugin', 'ai-o-optimiser' ), $cache_detected ? 'context' : 'pass', $cache_detected ? __( 'Detected', 'ai-o-optimiser' ) : __( 'Not detected', 'ai-o-optimiser' ), $cache_detected ? __( 'Production page caching is normally beneficial. Treat AI-O Optimiser runtime visit counts as directional when cached responses can be served before WordPress runs. Use cache exclusions only as a temporary diagnostic when a specific resource is stale or incorrect, then restore caching and purge on change.', 'ai-o-optimiser' ) : __( 'No common cache plugin detected from active plugin names.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'advanced-cache.php drop-in', 'ai-o-optimiser' ), $advanced_cache ? 'context' : 'pass', $advanced_cache ? __( 'Present', 'ai-o-optimiser' ) : __( 'Not present', 'ai-o-optimiser' ), $advanced_cache ? __( 'This is a normal cache architecture signal, not an error. Cached responses can be served before AI-O Optimiser logging hooks, so crawler visit totals may undercount requests that never reach WordPress.', 'ai-o-optimiser' ) : __( 'No advanced-cache.php drop-in found.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'Server software', 'ai-o-optimiser' ), false !== stripos( $server, 'litespeed' ) ? 'context' : 'pass', $server ? $server : __( 'Not reported', 'ai-o-optimiser' ), false !== stripos( $server, 'litespeed' ) ? __( 'LiteSpeed/server cache can affect whether requests reach WordPress. This is interpretation context, not a reason to disable caching; compare runtime logs with live response or edge/server logs when exact visit counts matter.', 'ai-o-optimiser' ) : __( 'Record only.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'WP_CACHE constant', 'ai-o-optimiser' ), ( defined( 'WP_CACHE' ) && WP_CACHE ) ? 'context' : 'pass', ( defined( 'WP_CACHE' ) && WP_CACHE ) ? 'true' : 'false', ( defined( 'WP_CACHE' ) && WP_CACHE ) ? __( 'Enabled caching is normal on production. It can reduce WordPress-level crawler logging coverage when responses are served before PHP, but it should not be disabled solely for audit cosmetics.', 'ai-o-optimiser' ) : __( 'Not enabled at the WordPress constant layer; external caching may still exist.', 'ai-o-optimiser' ) ); return self::chunk_payload( 'cache', __( 'Cache / CDN / server layer', 'ai-o-optimiser' ), $items ); } /** * Guidance assets chunk. * * @return array */ private static function check_guidance() { $items = array(); $settings = wp_parse_args( get_option( 'ai_o_optimiser_settings', array() ), AI_O_Optimiser_Settings::defaults() ); $provider_mode = isset( $settings['llms_provider_mode'] ) ? sanitize_key( $settings['llms_provider_mode'] ) : 'auto'; $static_robots = file_exists( ABSPATH . 'robots.txt' ); $items[] = self::item( __( 'Static robots.txt file', 'ai-o-optimiser' ), $static_robots ? 'context' : 'pass', $static_robots ? __( 'Detected', 'ai-o-optimiser' ) : __( 'Not detected', 'ai-o-optimiser' ), $static_robots ? __( 'WordPress plugins cannot reliably override a physical robots.txt file. Audit the live file and edit the static file/server rule if needed.', 'ai-o-optimiser' ) : __( 'Robots.txt may be handled dynamically by WordPress or an SEO plugin.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'llms.txt provider mode', 'ai-o-optimiser' ), 'disabled' === $provider_mode ? 'warning' : 'suggestion', sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'Mode: %1$s; ACT generator: %2$s', 'ai-o-optimiser' ), $provider_mode, ! empty( $settings['llms_generate_enabled'] ) ? __( 'enabled', 'ai-o-optimiser' ) : __( 'disabled', 'ai-o-optimiser' ) ), __( 'Auto mode lets Rank Math own /llms.txt when its llms-txt module is active. Choose ACT ownership only when this plugin should serve the root file.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'llms.txt header fix', 'ai-o-optimiser' ), ! empty( $settings['llms_rank_math_header_fix'] ) ? 'pass' : 'warning', ! empty( $settings['llms_rank_math_header_fix'] ) ? __( 'Enabled', 'ai-o-optimiser' ) : __( 'Disabled', 'ai-o-optimiser' ), __( 'Recommended when an SEO plugin serves /llms.txt with a blocking X-Robots-Tag: noindex without nofollow is acceptable; nofollow is not.', 'ai-o-optimiser' ) ); if ( ! empty( $settings['robots_txt_header_fix'] ) && $static_robots ) { $items[] = self::item( __( 'robots.txt header fix', 'ai-o-optimiser' ), 'warning', __( 'Enabled, but likely ineffective', 'ai-o-optimiser' ), __( 'A static robots.txt file was detected, so this WordPress-side fix never runs for that request. Edit the static file directly, or remove it so WordPress can serve robots.txt dynamically.', 'ai-o-optimiser' ) ); } else { $items[] = self::item( __( 'robots.txt header fix', 'ai-o-optimiser' ), ! empty( $settings['robots_txt_header_fix'] ) ? 'pass' : 'suggestion', ! empty( $settings['robots_txt_header_fix'] ) ? __( 'Enabled', 'ai-o-optimiser' ) : __( 'Disabled', 'ai-o-optimiser' ), __( 'Corrects a blocking X-Robots-Tag on /robots.txt itself, most commonly set by an SEO plugin.', 'ai-o-optimiser' ) ); } foreach ( array( 'robots.txt' => home_url( '/robots.txt' ), 'llms.txt' => home_url( '/llms.txt' ), 'sitemap_index.xml' => home_url( '/sitemap_index.xml' ), ) as $label => $url ) { $items[] = self::head_check_item( $label, $url ); } return self::chunk_payload( 'guidance', __( 'Robots / llms / sitemap ownership', 'ai-o-optimiser' ), $items ); } /** * Security / WAF chunk. * * @return array */ private static function check_security() { $plugins = self::active_plugins_with_data(); $names = self::plugin_names_blob( $plugins ); $items = array(); $wordfence = self::has_plugin_named( $names, array( 'wordfence' ) ); $security = self::has_plugin_named( $names, array( 'wordfence', 'sucuri', 'ithemes security', 'solid security', 'all-in-one security', 'malcare' ) ); $items[] = self::item( __( 'Security plugin / firewall', 'ai-o-optimiser' ), $security ? 'context' : 'pass', $security ? __( 'Detected', 'ai-o-optimiser' ) : __( 'Not detected', 'ai-o-optimiser' ), $security ? __( 'Security plugins can block test user-agents, loopback requests or repeated audit fetches. If checks fail, verify allow-lists and rate limits before assuming the site is inaccessible.', 'ai-o-optimiser' ) : __( 'No common security plugin detected by name.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'Wordfence-specific note', 'ai-o-optimiser' ), $wordfence ? 'context' : 'pass', $wordfence ? __( 'Detected', 'ai-o-optimiser' ) : __( 'Not detected', 'ai-o-optimiser' ), $wordfence ? __( 'Wordfence is generally compatible, but bot blocking/rate limiting can make simulated AI crawler checks look worse than real access.', 'ai-o-optimiser' ) : __( 'No Wordfence-specific issue detected.', 'ai-o-optimiser' ) ); $items[] = self::item( __( 'Public request tracking limitation', 'ai-o-optimiser' ), 'context', __( 'Always applicable', 'ai-o-optimiser' ), __( 'Any firewall, CDN or server rule that blocks requests before WordPress loads cannot be measured by this plugin directly.', 'ai-o-optimiser' ) ); return self::chunk_payload( 'security', __( 'Security / WAF risk hints', 'ai-o-optimiser' ), $items ); } /** * Content architecture chunk. * * @return array */ private static function check_content() { $items = array(); $post_types = get_post_types( array( 'public' => true ), 'objects' ); $names = array(); foreach ( $post_types as $post_type => $object ) { $names[] = $post_type . ' (' . ( isset( $object->labels->name ) ? $object->labels->name : $post_type ) . ')'; } $items[] = self::item( __( 'Public post types', 'ai-o-optimiser' ), count( $names ) > 2 ? 'suggestion' : 'pass', $names ? implode( ', ', $names ) : __( 'None detected', 'ai-o-optimiser' ), __( 'Schema Dash and AI review scope should include pages, posts and public custom post types that represent services, locations, menus, resources, events or people.', 'ai-o-optimiser' ) ); foreach ( array( 'page', 'post' ) as $type ) { $count = wp_count_posts( $type ); $published = is_object( $count ) && isset( $count->publish ) ? (int) $count->publish : 0; $items[] = self::item( sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'Published %s count', 'ai-o-optimiser' ), $type ), 'pass', (string) $published, __( 'Used only as a light scope indicator; it does not crawl every URL.', 'ai-o-optimiser' ) ); } $items[] = self::item( __( 'Audit scope recommendation', 'ai-o-optimiser' ), 'suggestion', __( 'Manual review required', 'ai-o-optimiser' ), __( 'Run full audits in batches on large sites: homepage, navigation templates, representative service/product/CPT pages, posts/resources, and discovery files.', 'ai-o-optimiser' ) ); return self::chunk_payload( 'content', __( 'Content architecture / post types', 'ai-o-optimiser' ), $items ); } /** * Create a HEAD-check finding. * * @param string $label Label. * @param string $url URL. * @return array */ private static function head_check_item( $label, $url ) { $response = wp_safe_remote_head( $url, array( 'timeout' => 6, 'redirection' => 3, 'headers' => array( 'User-Agent' => 'AI-O Optimiser Environment Check/' . AI_O_Optimiser_VERSION, 'Accept' => '*/*', ), ) ); if ( is_wp_error( $response ) ) { return self::item( $label, 'warning', __( 'Could not fetch', 'ai-o-optimiser' ), $response->get_error_message() ); } $code = (int) wp_remote_retrieve_response_code( $response ); $headers = self::normalise_headers( wp_remote_retrieve_headers( $response ) ); $x_robots = isset( $headers['x-robots-tag'] ) ? strtolower( $headers['x-robots-tag'] ) : ''; $content_type = isset( $headers['content-type'] ) ? strtolower( $headers['content-type'] ) : ''; $status = ( $code >= 200 && $code < 400 ) ? 'pass' : 'warning'; $finding = 'HTTP ' . $code; if ( $content_type ) { $finding .= '; ' . $content_type; } if ( $x_robots ) { $finding .= '; X-Robots-Tag: ' . $x_robots; } $recommendation = __( 'Verify the live payload body in the relevant audit before making content claims.', 'ai-o-optimiser' ); if ( 'llms.txt' === $label ) { if ( false !== strpos( $x_robots, 'nofollow' ) || preg_match( '/(^|[,\s])none([,\s]|$)/i', $x_robots ) ) { $status = 'warning'; $recommendation = __( 'Remove nofollow/none from llms.txt X-Robots-Tag. noindex is acceptable; nofollow undermines guidance-link discovery.', 'ai-o-optimiser' ); } elseif ( false !== strpos( $x_robots, 'noindex' ) ) { $recommendation = __( 'noindex on llms.txt is acceptable when nofollow is absent. Continue to verify body content and links.', 'ai-o-optimiser' ); } } return self::item( $label, $status, $finding, $recommendation ); } /** * Parse pasted Site Health text into sections/key-values. * * @param string $text Raw text. * @return array */ private static function parse_site_health_debug_text( $text ) { $sections = array(); $current = 'general'; $sections[ $current ] = array( 'raw' => array(), 'values' => array() ); $lines = explode( "\n", self::normalise_line_endings( $text ) ); foreach ( $lines as $line ) { $line = trim( wp_strip_all_tags( $line ) ); if ( '' === $line || '`' === $line ) { continue; } if ( preg_match( '/^###\s*(.*?)\s*###$/', $line, $m ) ) { $current = sanitize_key( $m[1] ); if ( ! isset( $sections[ $current ] ) ) { $sections[ $current ] = array( 'raw' => array(), 'values' => array() ); } continue; } $sections[ $current ]['raw'][] = self::redact_value( $line ); if ( preg_match( '/^([^:]{1,120}):\s*(.*)$/', $line, $m ) ) { $key = sanitize_key( $m[1] ); $value = self::redact_value( trim( $m[2] ) ); $sections[ $current ]['values'][ $key ] = $value; } } return $sections; } /** * Build findings from parsed pasted Site Health text. * * @param array $sections Parsed sections. * @return array */ private static function findings_from_pasted_debug( $sections ) { $items = array(); $core = isset( $sections['wp-core']['values'] ) ? $sections['wp-core']['values'] : array(); $server = isset( $sections['wp-server']['values'] ) ? $sections['wp-server']['values'] : array(); $constants = isset( $sections['wp-constants']['values'] ) ? $sections['wp-constants']['values'] : array(); $dropins = isset( $sections['wp-dropins']['values'] ) ? $sections['wp-dropins']['values'] : array(); $rank_math = isset( $sections['rank-math']['values'] ) ? $sections['rank-math']['values'] : array(); $active_plugins_raw = isset( $sections['wp-plugins-active']['raw'] ) ? implode( "\n", $sections['wp-plugins-active']['raw'] ) : ''; $plugin_blob = strtolower( $active_plugins_raw ); if ( empty( $sections ) || ( 1 === count( $sections ) && empty( $sections['general']['raw'] ) ) ) { return array( self::item( __( 'Pasted text', 'ai-o-optimiser' ), 'warning', __( 'No Site Health sections detected', 'ai-o-optimiser' ), __( 'Paste the copied output from Tools → Site Health → Info/Debug, including the section headings.', 'ai-o-optimiser' ) ) ); } $env_type = isset( $core['environment_type'] ) ? strtolower( $core['environment_type'] ) : ''; if ( $env_type ) { $items[] = self::item( __( 'Pasted environment type', 'ai-o-optimiser' ), 'production' === $env_type ? 'pass' : 'warning', $env_type, 'production' === $env_type ? __( 'Production environment reported.', 'ai-o-optimiser' ) : __( 'Do not treat local/staging debug data as final production crawlability evidence.', 'ai-o-optimiser' ) ); } if ( ! empty( $server['php_version'] ) ) { $php_version = preg_replace( '/\s+.*/', '', $server['php_version'] ); $items[] = self::item( __( 'Pasted PHP version', 'ai-o-optimiser' ), version_compare( $php_version, '8.0', '>=' ) ? 'pass' : 'warning', $server['php_version'], __( 'PHP 8+ is preferred for modern plugin-heavy stacks.', 'ai-o-optimiser' ) ); } if ( isset( $constants['wp_cache'] ) ) { $wp_cache = strtolower( $constants['wp_cache'] ); $items[] = self::item( __( 'Pasted WP_CACHE', 'ai-o-optimiser' ), 'true' === $wp_cache ? 'context' : 'pass', $constants['wp_cache'], 'true' === $wp_cache ? __( 'Caching is normally desirable. Runtime crawler visit counts can undercount responses served before WordPress, so treat them as directional rather than disabling caching.', 'ai-o-optimiser' ) : __( 'No WordPress cache constant is reported; external caching may still exist.', 'ai-o-optimiser' ) ); } if ( isset( $constants['wp_debug_display'] ) || isset( $constants['wp_debug'] ) || isset( $server['display_errors'] ) ) { $debug_display = isset( $constants['wp_debug_display'] ) ? strtolower( $constants['wp_debug_display'] ) : ''; $wp_debug = isset( $constants['wp_debug'] ) ? strtolower( $constants['wp_debug'] ) : ''; $php_display = isset( $server['display_errors'] ) ? self::ini_flag_enabled( $server['display_errors'] ) : false; $wp_display_active = 'true' === $wp_debug && ( '' === $debug_display || 'true' === $debug_display ); $active = $php_display || $wp_display_active; $finding = array(); if ( isset( $server['display_errors'] ) ) { $finding[] = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'Site Health PHP display_errors: %s', 'ai-o-optimiser' ), $server['display_errors'] ); } if ( isset( $constants['wp_debug'] ) ) { $finding[] = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'WP_DEBUG: %s', 'ai-o-optimiser' ), $constants['wp_debug'] ); } if ( isset( $constants['wp_debug_display'] ) ) { $finding[] = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'WP_DEBUG_DISPLAY: %s', 'ai-o-optimiser' ), $constants['wp_debug_display'] ); } $items[] = self::item( __( 'Pasted debug display', 'ai-o-optimiser' ), $active ? 'warning' : 'pass', implode( '; ', $finding ), $active ? __( 'Public debug output reported by Site Health should be disabled in the PHP/WordPress layer that owns it. Site Health reflects the runtime environment seen by WordPress, not necessarily a hosting control-panel field.', 'ai-o-optimiser' ) : __( 'Do not flag WP_DEBUG_DISPLAY by itself when WP_DEBUG is false and the PHP runtime reported by Site Health has display_errors off.', 'ai-o-optimiser' ) ); } if ( isset( $constants['wp_memory_limit'] ) || isset( $server['php_memory_limit'] ) ) { $php_memory = isset( $server['php_memory_limit'] ) ? $server['php_memory_limit'] : ''; $wp_memory = isset( $constants['wp_memory_limit'] ) ? $constants['wp_memory_limit'] : ''; $php_memory_bytes = $php_memory ? self::memory_to_bytes( $php_memory ) : 0; $low = $php_memory && $php_memory_bytes > 0 && $php_memory_bytes < 67108864; $finding = array(); if ( $php_memory ) { $finding[] = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'Site Health PHP memory_limit: %s', 'ai-o-optimiser' ), $php_memory ); } if ( $wp_memory ) { $finding[] = sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( 'WP_MEMORY_LIMIT: %s', 'ai-o-optimiser' ), $wp_memory ); } if ( ! $php_memory ) { $items[] = self::item( __( 'Pasted memory availability', 'ai-o-optimiser' ), 'context', implode( '; ', $finding ), __( 'The pasted Site Health data does not include a PHP runtime memory_limit, so memory availability is not assessed from WP_MEMORY_LIMIT alone. WP_MEMORY_LIMIT is a WordPress application-level constant and does not prove that PHP or the hosting platform is capped at that value. Use the live runtime check or a Site Health export that includes PHP memory_limit before recommending an infrastructure change.', 'ai-o-optimiser' ) ); } else { $items[] = self::item( __( 'Pasted memory availability', 'ai-o-optimiser' ), $low ? 'warning' : 'pass', implode( '; ', $finding ), $low ? __( 'The PHP memory_limit reported by Site Health appears low for large audits. Change the active infrastructure/application PHP owner rather than duplicating settings in wp-config.php.', 'ai-o-optimiser' ) : __( 'Memory is judged from the PHP runtime memory_limit reported by Site Health. A lower WP_MEMORY_LIMIT value is application-level context only and does not mean PHP has been reduced to that value.', 'ai-o-optimiser' ) ); } } if ( isset( $server['httpd_software'] ) ) { $items[] = self::item( __( 'Pasted server software', 'ai-o-optimiser' ), false !== stripos( $server['httpd_software'], 'litespeed' ) ? 'suggestion' : 'pass', $server['httpd_software'], false !== stripos( $server['httpd_software'], 'litespeed' ) ? __( 'LiteSpeed/cache layers may affect whether crawler hits reach WordPress logging.', 'ai-o-optimiser' ) : __( 'Record only.', 'ai-o-optimiser' ) ); } if ( isset( $server['static_robotstxt_file'] ) ) { $static = strtolower( $server['static_robotstxt_file'] ); $items[] = self::item( __( 'Pasted static robots.txt file', 'ai-o-optimiser' ), 'true' === $static ? 'warning' : 'pass', $server['static_robotstxt_file'], 'true' === $static ? __( 'A physical robots.txt file must be edited directly or by server tooling; plugins can only audit the live result.', 'ai-o-optimiser' ) : __( 'No static robots.txt file reported by Site Health.', 'ai-o-optimiser' ) ); } if ( isset( $dropins['advanced-cache_php'] ) || isset( $dropins['advanced-cachephp'] ) || false !== stripos( implode( '\n', isset( $sections['wp-dropins']['raw'] ) ? $sections['wp-dropins']['raw'] : array() ), 'advanced-cache.php' ) ) { $items[] = self::item( __( 'Pasted advanced-cache.php drop-in', 'ai-o-optimiser' ), 'context', __( 'Detected', 'ai-o-optimiser' ), __( 'This is cache architecture context, not a fault. Cached responses may bypass WordPress runtime logging, so visit totals can undercount edge/cache-served requests.', 'ai-o-optimiser' ) ); } $plugin_count = 0; if ( $active_plugins_raw ) { $plugin_count = substr_count( $active_plugins_raw, 'version:' ); $items[] = self::item( __( 'Pasted active plugin count', 'ai-o-optimiser' ), $plugin_count >= 35 ? 'warning' : 'pass', (string) $plugin_count, __( 'Plugin-heavy sites should use chunked audits and review compatibility notes before full scans.', 'ai-o-optimiser' ) ); } $rank_active = false !== strpos( $plugin_blob, 'rank math seo' ) || ! empty( $rank_math ); if ( $rank_active ) { $modules = isset( $rank_math['active_modules'] ) ? strtolower( $rank_math['active_modules'] ) : ''; $llms_module = false !== strpos( $modules, 'llms-txt' ); $items[] = self::item( __( 'Pasted Rank Math / llms.txt', 'ai-o-optimiser' ), $llms_module ? 'context' : 'suggestion', $llms_module ? __( 'Rank Math llms-txt module active', 'ai-o-optimiser' ) : __( 'Rank Math detected', 'ai-o-optimiser' ), $llms_module ? __( 'Prefer Rank Math-owned /llms.txt with ACT applying only the noindex-without-nofollow header fix, unless the user explicitly chooses ACT as provider.', 'ai-o-optimiser' ) : __( 'Review schema and llms.txt ownership before applying ACT fallbacks.', 'ai-o-optimiser' ) ); } if ( false !== strpos( $plugin_blob, 'wp rocket' ) || false !== strpos( $plugin_blob, 'litespeed cache' ) || false !== strpos( $plugin_blob, 'breeze' ) || false !== strpos( $plugin_blob, 'cloudflare' ) ) { $items[] = self::item( __( 'Pasted cache plugin', 'ai-o-optimiser' ), 'context', __( 'Detected', 'ai-o-optimiser' ), __( 'Keep normal production caching enabled. Use exclusions only to diagnose a specific stale/incorrect resource, then restore caching and purge on content or guidance changes. Runtime visit counts remain directional when cache layers can answer before WordPress.', 'ai-o-optimiser' ) ); } if ( false !== strpos( $plugin_blob, 'wordfence' ) ) { $items[] = self::item( __( 'Pasted Wordfence', 'ai-o-optimiser' ), 'suggestion', __( 'Detected', 'ai-o-optimiser' ), __( 'Wordfence is compatible in principle, but bot blocking/rate limiting can affect test user-agents and audit fetches.', 'ai-o-optimiser' ) ); } if ( false !== strpos( $plugin_blob, 'advanced custom fields' ) || false !== strpos( $plugin_blob, 'custom post type ui' ) ) { $items[] = self::item( __( 'Pasted custom fields / CPT tooling', 'ai-o-optimiser' ), 'suggestion', __( 'Detected', 'ai-o-optimiser' ), __( 'Ensure Schema Dash includes pages, posts and public custom post types with entity-relevant content.', 'ai-o-optimiser' ) ); } if ( empty( $items ) ) { $items[] = self::item( __( 'Pasted Site Health parse', 'ai-o-optimiser' ), 'suggestion', __( 'Parsed with no major flags', 'ai-o-optimiser' ), __( 'Review the source manually if expected plugin/server details are missing.', 'ai-o-optimiser' ) ); } return $items; } /** * Plugin-owned database and option storage guardrails. * * @return array */ private static function check_storage() { $storage = AI_O_Optimiser_DB::storage_report(); $total = isset( $storage['total_bytes'] ) ? (int) $storage['total_bytes'] : 0; $status = $total > 75 * MB_IN_BYTES ? 'warning' : ( $total > 25 * MB_IN_BYTES ? 'context' : 'pass' ); $items = array(); $aggregate_state = isset( $storage['aggregate_state'] ) && is_array( $storage['aggregate_state'] ) ? $storage['aggregate_state'] : array(); if ( empty( $aggregate_state['complete'] ) ) { $done = min( (int) ( $aggregate_state['max_id'] ?? 0 ), (int) ( $aggregate_state['last_id'] ?? 0 ) ); $total_rows = (int) ( $aggregate_state['max_id'] ?? 0 ); $items[] = self::item( 'context', __( 'Visibility storage migration', 'ai-o-optimiser' ), sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( '%1$s / %2$s historic rows aggregated', 'ai-o-optimiser' ), number_format_i18n( $done ), number_format_i18n( $total_rows ) ), __( 'Keep the plugin active while WordPress cron completes the one-time v1.17 migration. Detailed history is not pruned until the compact aggregate backfill finishes.', 'ai-o-optimiser' ) ); } $items[] = self::item( $status, __( 'Estimated AI-O Optimiser storage', 'ai-o-optimiser' ), size_format( $total, 2 ), $total > 75 * MB_IN_BYTES ? __( 'Review the largest table below. AI-O Optimiser has automatic guardrails, but this site is retaining a materially large amount of plugin data.', 'ai-o-optimiser' ) : __( 'Current plugin storage is within the normal guardrail range.', 'ai-o-optimiser' ) ); $labels = array( 'ai_o_optimiser_visibility_daily' => __( 'Visibility daily aggregates', 'ai-o-optimiser' ), 'ai_o_optimiser_crawler_log' => __( 'Detailed visibility request log', 'ai-o-optimiser' ), 'ai_o_optimiser_metadata_queue' => __( 'Metadata Queue', 'ai-o-optimiser' ), 'ai_o_optimiser_metadata_search_rows' => __( 'Search Insights imported rows', 'ai-o-optimiser' ), 'ai_o_optimiser_metadata_opportunities' => __( 'Search Opportunities', 'ai-o-optimiser' ), 'ai_o_optimiser_gsc_rows' => __( 'Content Opportunities Search Console rows', 'ai-o-optimiser' ), 'ai_o_optimiser_opportunity_workflow' => __( 'Content Opportunity workflow', 'ai-o-optimiser' ), ); foreach ( $labels as $key => $label ) { if ( empty( $storage['tables'][ $key ] ) ) { continue; } $table = $storage['tables'][ $key ]; $items[] = self::item( ( (int) $table['bytes'] > 25 * MB_IN_BYTES ? 'context' : 'pass' ), $label, sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ __( '%1$s rows · %2$s', 'ai-o-optimiser' ), number_format_i18n( (int) $table['rows'] ), size_format( (int) $table['bytes'], 2 ) ), 'ai_o_optimiser_visibility_daily' === $key ? __( 'Long-term dashboard history is stored as compact daily aggregates rather than one row per request.', 'ai-o-optimiser' ) : ( 'ai_o_optimiser_crawler_log' === $key ? __( 'Detailed diagnostics are short-lived and capped at 50,000 rows. Long-term reporting continues from the aggregate table.', 'ai-o-optimiser' ) : ( 'ai_o_optimiser_metadata_queue' === $key ? __( 'Queued and suggested work is preserved. Failures older than 90 days are retired and terminal history is capped at 5,000 rows; oldest terminal entries are retired first while queued/suggested work is never pruned. Retiring an old applied queue record removes its queue-based rollback history, not the metadata already written to the post.', 'ai-o-optimiser' ) : __( 'This dataset is bounded by replacement, uniqueness or configured import limits rather than indefinite append-only history.', 'ai-o-optimiser' ) )) ); } $items[] = self::item( 'pass', __( 'Schema Dash storage model', 'ai-o-optimiser' ), size_format( isset( $storage['schema_option_bytes'] ) ? (int) $storage['schema_option_bytes'] : 0, 2 ), __( 'Schema Dash does not append every audit to a database table. It keeps the latest audit (maximum 600 URLs), one temporary comparison baseline and eight compact history summaries.', 'ai-o-optimiser' ) ); return self::chunk_payload( 'storage', __( 'Database / storage guardrails', 'ai-o-optimiser' ), $items, __( 'Source: live WordPress database table counts and approximate MySQL table storage. Values can differ slightly from hosting-panel totals.', 'ai-o-optimiser' ) ); } public static function render_admin_page() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only screen state (filters, tab selection, pagination, redirect result notices). Nothing is written here, so there is no form submission to carry a nonce. if ( ! current_user_can( self::CAPABILITY ) ) { return; } $report = self::report(); $chunks = self::chunks(); ?>