settings = $settings; } /** * Perform a GET request to the peer. * * @param string $path The API path to request from the peer. */ public function get( string $path ) { $url = $this->build_url( $path ); $headers = $this->build_headers( '' ); $response = wp_remote_get( $url, array( 'headers' => $headers, 'timeout' => $this->timeout( 30 ), ) ); return $this->parse_response( $response ); } /** * Perform a POST request with JSON body to the peer. * * @param string $path The API path to request from the peer. * @param array $body The data to send as the JSON body. */ public function post( string $path, array $body ) { $payload = wp_json_encode( $body ); $url = $this->build_url( $path ); $headers = array_merge( $this->build_headers( $payload ), array( 'Content-Type' => 'application/json' ) ); $response = wp_remote_post( $url, array( 'headers' => $headers, 'body' => $payload, 'timeout' => $this->timeout( 180 ), ) ); return $this->parse_response( $response ); } /** * Perform a POST with raw body (for media uploads) — scaffold for future use. * * @param string $path The API path to request from the peer. * @param mixed $body The raw body to send (e.g., file contents). * @param array $extraHeaders Additional headers to include in the request. */ public function post_stream( string $path, $body, array $extraHeaders = array() ) { $url = $this->build_url( $path ); $payload = $body; $headers = array_merge( $this->build_headers( is_string( $payload ) ? $payload : '' ), $extraHeaders ); $response = wp_remote_post( $url, array( 'headers' => $headers, 'body' => $payload, 'timeout' => $this->timeout( 60 ), ) ); return $this->parse_response( $response ); } /** * Fetch binary media from peer (no JSON decoding). * * @param string $externalId The external media ID to fetch. */ public function fetch_media_by_external_id( string $externalId ) { $path = '/wp-json/site-sync/v1/media?external_id=' . rawurlencode( $externalId ); $url = $this->build_url( $path ); $headers = $this->build_headers( '' ); $response = wp_remote_get( $url, array( 'headers' => $headers, 'timeout' => $this->timeout( 60 ), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); if ( $code !== 200 ) { return new WP_Error( 'site_sync_media_fetch_failed', sprintf( 'Media fetch failed with HTTP %d', $code ), array( 'body' => wp_remote_retrieve_body( $response ) ) ); } $headersOut = wp_remote_retrieve_headers( $response ); return array( 'body' => wp_remote_retrieve_body( $response ), 'headers' => $headersOut, 'checksum' => is_array( $headersOut ) && isset( $headersOut['x-site-sync-checksum'] ) ? $headersOut['x-site-sync-checksum'] : null, 'code' => $code, ); } /** * Run a handshake to verify connectivity and identity. */ public function handshake() { return $this->get( '/wp-json/site-sync/v1/handshake' ); } /** * Build the full URL to the peer for a given API path. * * @param string $path The API path to append to the peer URL. * @return string The full URL. */ private function build_url( string $path ): string { $peer = rtrim( $this->settings->get( 'peer_url', '' ), '/' ); $path = '/' . ltrim( $path, '/' ); return $peer . $path; } /** * Build the HTTP headers required for SiteSync authentication. * * @param string $body The request body to be signed. * @return array The array of headers. */ private function build_headers( string $body ): array { $siteKey = $this->settings->get( 'site_uuid' ); $secret = $this->settings->get( 'shared_key' ); $timestamp = (string) time(); $signature = Auth::build_signature( $siteKey, $timestamp, $body, $secret ); return array( 'X-Site-Sync-Key' => $siteKey, 'X-Site-Sync-Timestamp' => $timestamp, 'X-Site-Sync-Signature' => $signature, ); } /** * Parse the HTTP response and return decoded data or WP_Error on failure. * * @param mixed $response The response from wp_remote_get or wp_remote_post. * @return mixed|WP_Error Decoded response data, raw body, or WP_Error on error. */ private function parse_response( $response ) { if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( $code >= 200 && $code < 300 ) { return $data ?? $body; } return new WP_Error( 'site_sync_http_error', sprintf( 'HTTP %d from peer', $code ), array( 'body' => $body ) ); } /** * Get the HTTP timeout value, allowing for filtering. * * @param int $default The default timeout value in seconds. * @return int The filtered timeout value, minimum 5 seconds. */ private function timeout( int $default ): int { // phpcs:ignore $filtered = apply_filters( 'site_sync/http_timeout', $default ); $value = is_numeric( $filtered ) ? (int) $filtered : $default; return max( 5, $value ); } }