feature: Initial functional push

This commit is contained in:
Keith Solomon
2025-12-14 16:58:52 -06:00
parent 15445ad40e
commit 189b32ccff
14 changed files with 3365 additions and 0 deletions

View 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 );
}
}