$kind, 'technical' => self::short( $kind . ( '' !== $technical ? ': ' . $technical : '' ) ), ) ); } /** * The request to WPPilot itself failed in transport (DNS, refused, * timeout, TLS), or the action's time budget ran out first. * * @param string $wp_code The WP_Error code to return. * @param \WP_Error $transport What wp_remote_*() returned. */ public static function from_transport( string $wp_code, \WP_Error $transport ): \WP_Error { $kind = Backend_Client::BUDGET_EXHAUSTED === $transport->get_error_code() ? self::TIMED_OUT : self::BACKEND_UNREACHABLE; return self::error( $wp_code, $kind, $transport->get_error_code() . ' ' . $transport->get_error_message() ); } /** * WPPilot answered the connect request with something other than success. * * @param int $http_code The HTTP status. * @param mixed $data The decoded body, if it was JSON. */ public static function from_connect_response( int $http_code, $data ): \WP_Error { $error = is_array( $data ) && isset( $data['error'] ) && is_string( $data['error'] ) ? $data['error'] : ''; $reason = is_array( $data ) && isset( $data['reason'] ) && is_string( $data['reason'] ) ? $data['reason'] : ''; $code = is_array( $data ) && isset( $data['code'] ) && is_string( $data['code'] ) ? $data['code'] : ''; if ( 'handshake_failed' === $error ) { $kind = in_array( $code, self::BACKEND_KINDS, true ) ? $code : self::classify_handshake_reason( $reason ); } else { $kind = self::BACKEND_ERROR; } return self::error( 'wppilot_connect_failed', $kind, trim( 'HTTP ' . $http_code . ' ' . $error . ' ' . $reason ) ); } /** * WPPilot answered a handoff request with something other than success. * * @param int $http_code The HTTP status. */ public static function from_handoff_response( int $http_code ): \WP_Error { $kind = 401 === $http_code ? self::PAIRING_REJECTED : self::BACKEND_ERROR; return self::error( 'wppilot_handoff_failed', $kind, 'HTTP ' . $http_code ); } /** * Classify a handshake failure from a backend that predates its `code` * field, from the technical reason it did send. These are fixed strings * the backend's own code produces (HTTP statuses, Node fetch errors), not * language anybody typed. * * @param string $reason The backend's technical reason. */ public static function classify_handshake_reason( string $reason ): string { if ( preg_match( '/HTTP (\d{3})/', $reason, $m ) ) { $status = (int) $m[1]; if ( 401 === $status || 403 === $status ) { return self::SITE_BLOCKED; } return 404 === $status ? self::REST_UNAVAILABLE : self::SITE_ERROR; } if ( str_contains( $reason, 'same origin' ) ) { return self::URL_MISMATCH; } if ( str_contains( $reason, 'malformed' ) || str_contains( $reason, 'mismatch' ) || str_contains( $reason, 'JSON' ) ) { return self::RESPONSE_ALTERED; } return self::SITE_UNREACHABLE; } /** * The support line of a failure, or '' when it carries none. * * @param \WP_Error $error A failure from this class, or any other. */ public static function technical_of( \WP_Error $error ): string { $data = $error->get_error_data(); if ( is_array( $data ) && isset( $data['technical'] ) && is_string( $data['technical'] ) ) { return $data['technical']; } return self::short( (string) $error->get_error_code() ); } /** * Keep a technical line short and single-line. * * @param string $text The raw detail. */ private static function short( string $text ): string { $text = trim( (string) preg_replace( '/\s+/', ' ', $text ) ); return strlen( $text ) > 200 ? substr( $text, 0, 197 ) . '...' : $text; } }