http ?? new HttpClient(); $response = $http->get( $endpoint ); } catch ( RuntimeException $e ) { $code = (int) $e->getCode(); $message = $e->getMessage(); // Map HttpClient error codes to user-friendly messages. switch ( $code ) { case HttpClient::ERR_DNS_FAILURE: throw new RuntimeException( $message, // Already formatted with hostname. $code, $e ); case HttpClient::ERR_CONNECTION_REFUSED: throw new RuntimeException( $message, $code, $e ); case HttpClient::ERR_TIMEOUT: throw new RuntimeException( __( 'The remote site took too long to respond. It may be overloaded or unreachable. Try again or increase the request timeout in Shiftora settings.', 'shiftora' ), $code, $e ); case HttpClient::ERR_SSL: throw new RuntimeException( __( 'SSL/TLS handshake failed. The remote site may require a TLS version or cipher this server does not support. Check the remote site\'s TLS configuration and this server\'s OpenSSL.', 'shiftora' ), $code, $e ); case HttpClient::ERR_REMOTE_NOT_FOUND: throw new RuntimeException( __( 'The remote site responded but Shiftora does not appear to be installed or active. Make sure Shiftora is installed and activated on the remote site.', 'shiftora' ), $code, $e ); case HttpClient::ERR_BLOCKED_EXTERNAL: throw new RuntimeException( $message, $code, $e ); default: throw new RuntimeException( sprintf( /* translators: %s: the raw error message from the HTTP layer */ __( 'Could not reach the remote site. %s', 'shiftora' ), $message ), $code ?: HttpClient::ERR_REMOTE_ERROR, $e ); } } $latency_ms = (int) round( ( hrtime( true ) - $start ) / 1_000_000 ); $payload = json_decode( $response['body'], true ); if ( ! is_array( $payload ) || empty( $payload['shiftora'] ) ) { throw new RuntimeException( __( 'Remote site responded but does not appear to have Shiftora installed. Make sure Shiftora is installed and activated on the remote site.', 'shiftora' ), HttpClient::ERR_REMOTE_NOT_FOUND ); } // A capability announced at pairing is not given up later. A ping that leaves one // out is a downgrade, because the next request would send the token in the clear. if ( PairingCapabilities::was_lowered( $safe_url, $payload ) ) { throw new RuntimeException( __( 'The remote site stopped advertising a security capability it announced when the sites were paired, so the request was refused. If Shiftora was rolled back there, delete this connection and pair the sites again.', 'shiftora' ), HttpClient::ERR_REMOTE_ERROR ); } // Where the peer's admin-ajax fallback lives, as it says it itself. HttpClient::remember_ajax_url( $safe_url, $payload['ajax_url'] ?? null ); // Read before the first request that carries a credential, so that request can // already name the connection by key id instead of sending the token. if ( true === ( $payload['accepts_key_id'] ?? false ) ) { HttpClient::expect_key_id( $safe_url ); } return [ 'url' => esc_url_raw( $remote_url ), 'site_url' => isset( $payload['site_url'] ) ? esc_url_raw( (string) $payload['site_url'] ) : esc_url_raw( $remote_url ), 'version' => isset( $payload['version'] ) ? sanitize_text_field( (string) $payload['version'] ) : '', 'latency_ms' => $latency_ms, // The version number, not a boolean: version 1 does not interoperate with the current one, which alone enforces owner-scoped writes. 'transfer_protocol' => PreflightChecker::advertised_protocol( $payload['transfer_protocol'] ?? null ), 'allow_pull' => isset( $payload['allow_pull'] ) ? (bool) $payload['allow_pull'] : true, ]; } // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped }