add_query_arg( 'atb_health', $atb_buster, home_url( '/' ) ), 'rest' => add_query_arg( 'atb_health', $atb_buster, rest_url() ), ); $atb_failures = array(); $atb_checks = array(); foreach ( $atb_urls as $atb_label => $atb_url ) { $atb_start = microtime( true ); $atb_response = wp_remote_get( $atb_url, array( 'timeout' => self::PROBE_TIMEOUT, 'sslverify' => false, // Loopback to ourselves; local/self-signed certs must not fail the probe. ) ); $atb_time_ms = (int) round( ( microtime( true ) - $atb_start ) * 1000 ); if ( is_wp_error( $atb_response ) ) { $atb_failures[] = 'health probe (' . $atb_label . '): ' . $atb_response->get_error_message(); $atb_checks[ $atb_label ] = array( 'status' => 0, 'ok' => false, 'reason' => $atb_response->get_error_message(), 'time_ms' => $atb_time_ms, ); continue; } $atb_status = (int) wp_remote_retrieve_response_code( $atb_response ); $atb_verdict = self::evaluate( $atb_status, (string) wp_remote_retrieve_body( $atb_response ) ); if ( ! $atb_verdict['ok'] ) { $atb_failures[] = 'health probe (' . $atb_label . '): ' . $atb_verdict['reason']; } $atb_checks[ $atb_label ] = array( 'status' => $atb_status, 'ok' => $atb_verdict['ok'], 'reason' => $atb_verdict['reason'], 'time_ms' => $atb_time_ms, ); } return array( 'passed' => empty( $atb_failures ), 'failures' => $atb_failures, 'checks' => $atb_checks, ); } /** * Baseline check BEFORE a risky change (v1.5.2, review F2). The * recover operations promise "health-checked, restored if the site * broke" — a promise that silently inverts on a site that cannot * reach itself for reasons unrelated to the change (HTTP basic auth * or an access gate on staging, a WAF challenge, a host that blocks * loopback requests, a hostname the server cannot resolve): every * post-change probe fails, every change is "rolled back", and the * agent is told the update broke the site. Same stance as the * single-file-plugin refusal — without a working safety net the * operation refuses, honestly, with a machine code. * * @return array|null { reason, error, checks } to refuse with, or null * when the site answers its own probe. */ public static function baseline_refusal() { $atb_probe = self::run(); if ( $atb_probe['passed'] ) { return null; } return array( 'reason' => 'loopback_unavailable', 'error' => 'the site cannot reach itself over HTTP before any change was made (' . implode( '; ', $atb_probe['failures'] ) . '), so the post-change health check and ' . 'the automatic restore cannot work here and the operation is refused. Common causes: ' . 'HTTP basic auth or an access gate on this site, a firewall/WAF challenge, a host that ' . 'blocks loopback requests, or a hostname the server cannot resolve. Ask the site owner ' . 'to allow loopback requests (Site Health reports the same problem); agent-toolbelt/' . 'check-site-response shows the per-check details.', 'checks' => $atb_probe['checks'], ); } /** * Pure verdict on one probe response: 2xx and no fatal marker = ok. * * @param int $atb_code HTTP status code (0 when unavailable). * @param string $atb_body Response body. * @return array { ok: bool, reason: string } */ public static function evaluate( $atb_code, $atb_body ) { if ( $atb_code < 200 || $atb_code >= 300 ) { return array( 'ok' => false, 'reason' => 'unexpected HTTP status ' . $atb_code ); } foreach ( self::FATAL_MARKERS as $atb_marker ) { if ( false !== stripos( $atb_body, $atb_marker ) ) { return array( 'ok' => false, 'reason' => 'fatal-error marker found in response body' ); } } return array( 'ok' => true, 'reason' => '' ); } } }