✨feature: Initial functional push
This commit is contained in:
222
includes/class-transport.php
Normal file
222
includes/class-transport.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace SiteSync;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles HTTP transport operations for SiteSync, including GET, POST, and media transfers.
|
||||
*/
|
||||
class Transport {
|
||||
/**
|
||||
* SiteSync settings instance.
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* Transport constructor.
|
||||
*
|
||||
* @param Settings $settings SiteSync settings instance.
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user