true]) : []; $wp_reported_latest = (!empty($updates) && !empty($updates[0]->current)) ? $updates[0]->current : $wp_version; // Direct query to wp.org — can't be cloaked by site-side filters. $wp_org_latest = $this->wp_org_core_version(); // Detect known plugins that hide/manipulate version reporting. $cloakers = $this->detect_cloakers(); $f[] = $this->finding('wp_current', 'info', 'Currently installed', $wp_version, ''); // What WP reports as the latest $wp_behind = version_compare($wp_version, $wp_reported_latest, '<'); $f[] = $this->finding( 'wp_reported_latest', $wp_behind ? 'warn' : 'ok', 'Latest per WordPress', $wp_reported_latest, 'This is what core/get_core_updates() returns — may be filtered by plugins like WP Downgrade.' ); // What wp.org actually thinks if ($wp_org_latest === null) { $f[] = $this->finding('wp_org_latest', 'info', 'Latest per wp.org', 'could not fetch', 'Direct API call failed — re-run when online.'); } else { $actual_behind = version_compare($wp_version, $wp_org_latest, '<'); $hidden_gap = version_compare($wp_reported_latest, $wp_org_latest, '<'); $level = $hidden_gap ? 'bad' : ($actual_behind ? 'warn' : 'ok'); $detail = ''; if ($hidden_gap) { $detail = 'CLOAKED — WordPress says latest is ' . $wp_reported_latest . ' but wp.org reports ' . $wp_org_latest . '. Updates likely blocked by a plugin or constant.'; } elseif ($actual_behind) { $detail = 'Real update available → ' . admin_url('update-core.php'); } else { $detail = 'Up to date against the public WP.org API.'; } $f[] = $this->finding('wp_org_latest', $level, 'Latest per wp.org (direct)', $wp_org_latest, $detail); } // Flag cloaker plugins explicitly if ($cloakers) { $f[] = $this->finding( 'cloakers', 'warn', 'Update-cloaker plugins active', implode(', ', $cloakers), 'These plugins can hide or block core/plugin updates. Confirm with the client whether they\'re needed before disabling them.' ); } // WP Downgrade specifically pins a target via an option — surface its value $wpd_target = get_option('wp_downgrade_core_version', ''); if ($wpd_target) { $f[] = $this->finding( 'wp_downgrade_target', 'warn', 'WP Downgrade target', (string) $wpd_target, 'WordPress core is pinned to this version by WP Downgrade. Set wp_downgrade_core_version to empty to release the pin.' ); } // Database upgrade required? if (function_exists('wp_get_db_schema')) { $required = (int) get_option('db_version'); global $wp_db_version; if ($required > 0 && $wp_db_version > $required) { $f[] = $this->finding('db_upgrade', 'warn', 'Database upgrade', 'pending', 'WP code is at v' . $wp_db_version . ', database at v' . $required . '. Visit /wp-admin/upgrade.php.'); } else { $f[] = $this->finding('db_upgrade', 'ok', 'Database upgrade', 'not needed', ''); } } // Auto-updates configured? $core_auto = get_site_option('auto_update_core_major', null); if ($core_auto === null) $core_auto = defined('WP_AUTO_UPDATE_CORE') ? (string) WP_AUTO_UPDATE_CORE : 'minor'; $f[] = $this->finding( 'auto_update', 'info', 'Core auto-update policy', (string) $core_auto, 'Configured via Updates → Auto-updates or the WP_AUTO_UPDATE_CORE constant.' ); // Safe-mode update guidance — not an action, just a reminder. $real_behind = $wp_org_latest !== null && version_compare($wp_version, $wp_org_latest, '<'); if ($real_behind || $wp_behind) { $f[] = $this->finding( 'safe_mode_hint', 'info', 'Safe-update sequence', 'see detail', 'Deactivate non-essential plugins → run the update → smoke-load front-end + wp-admin → reactivate plugins.' ); } return $f; } /** Direct call to wp.org's version-check endpoint. Cached 1h via transient. */ private function wp_org_core_version(): ?string { $cached = get_transient('att_hc_wp_org_core_latest'); if ($cached !== false) return $cached === '' ? null : (string) $cached; $resp = wp_remote_get('https://api.wordpress.org/core/version-check/1.7/', ['timeout' => 5]); if (is_wp_error($resp) || (int) wp_remote_retrieve_response_code($resp) !== 200) { set_transient('att_hc_wp_org_core_latest', '', 5 * MINUTE_IN_SECONDS); return null; } $data = json_decode((string) wp_remote_retrieve_body($resp), true); $latest = isset($data['offers'][0]['version']) ? (string) $data['offers'][0]['version'] : null; set_transient('att_hc_wp_org_core_latest', $latest ?: '', HOUR_IN_SECONDS); return $latest; } /** Detect plugins that are known to hide or manipulate version reporting. */ private function detect_cloakers(): array { if (!function_exists('is_plugin_active')) require_once ABSPATH . 'wp-admin/includes/plugin.php'; $known = [ 'wp-downgrade/wp-downgrade.php' => 'WP Downgrade', 'wp-rollback/wp-rollback.php' => 'WP Rollback', 'easy-updates-manager/wpmudev_install.php' => 'Easy Updates Manager', 'stops-core-theme-and-plugin-updates/stops-core-theme-and-plugin-updates.php' => 'Stops Core/Theme/Plugin Updates', 'disable-updates-manager/disable-updates-manager.php' => 'Disable Updates Manager', 'companion-auto-update/companion-auto-update.php' => 'Companion Auto Update', ]; $found = []; foreach ($known as $file => $label) { if (is_plugin_active($file)) $found[] = $label; } return $found; } };