Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ are additive so they cannot accidentally delete omitted site files. After a
deletion, the plugin blocks partial publishing until a full export removes old
files safely. Upload transfer is paged and processed one file
per Simply Static background step so a large upload does not become one
unbounded WordPress request.
unbounded WordPress request. Transient upload failures retry the same file up to
three times, with delays of 2, 4, and 8 seconds between attempts. Streaming
uploads use the WordPress CA bundle, the `http_request_args` certificate and
timeout settings, and the configured WordPress proxy. TLS verification stays
enabled. Permanent failures stop with a recovery action; sanitized file, host,
error-code, and timing details appear under **Technical details**. Signed URLs
and authorization headers are never included in these diagnostics.

Simply Static remains responsible for crawling WordPress and rewriting URLs.
Spacefast never reads drafts, WordPress credentials, or the database. Dynamic
Expand Down
45 changes: 41 additions & 4 deletions includes/class-spacefast-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ private function curl_upload( string $url, string $method, array $headers, strin
if ( ( 'https' !== $scheme && ! $development ) || ( ! $validated && ! $development ) ) {
return self::upload_error( false, 'unsafe_upload_url', 'Spacefast returned an unsafe upload URL.' );
}
if ( ( new WP_Http() )->block_request( $url ) ) {
return self::upload_error( false, 'upload_blocked', 'WordPress blocks this upload destination. Ask your host to allow the Spacefast upload host, then retry publishing.' );
}
$size = filesize( $file_path );
$stream = fopen( $file_path, 'rb' );
if ( false === $size || false === $stream ) {
Expand All @@ -423,6 +426,16 @@ private function curl_upload( string $url, string $method, array $headers, strin
$curl_headers[] = 'Content-Type: ' . self::content_type_for_file( $file_path );
}
$curl_headers[] = 'Content-Length: ' . $size;
$http_args = apply_filters( 'http_request_args', array(
'method' => $method,
'headers' => $headers,
'body' => null,
'redirection' => 0,
'blocking' => true,
'timeout' => 120,
'sslverify' => true,
'sslcertificates' => ABSPATH . WPINC . '/certificates/ca-bundle.crt',
), $url );
$handle = curl_init( $url );
curl_setopt_array(
$handle,
Expand All @@ -438,19 +451,43 @@ private function curl_upload( string $url, string $method, array $headers, strin
? CURLPROTO_HTTP | CURLPROTO_HTTPS
: CURLPROTO_HTTPS,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 120,
CURLOPT_TIMEOUT => max( 1, min( 120, (int) $http_args['timeout'] ) ),
CURLOPT_CAINFO => $http_args['sslcertificates'],
CURLOPT_USERAGENT => 'Spacefast-WordPress/' . SPACEFAST_WORDPRESS_VERSION,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
),
);
$proxy = new WP_HTTP_Proxy();
if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
curl_setopt( $handle, CURLOPT_PROXY, $proxy->host() );
curl_setopt( $handle, CURLOPT_PROXYPORT, $proxy->port() );
if ( $proxy->use_authentication() ) {
curl_setopt( $handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
}
}
$result = curl_exec( $handle );
$status = (int) curl_getinfo( $handle, CURLINFO_RESPONSE_CODE );
$error = curl_error( $handle );
$errno = curl_errno( $handle );
$duration_ms = (int) round( curl_getinfo( $handle, CURLINFO_TOTAL_TIME ) * 1000 );
curl_close( $handle );
fclose( $stream );
if ( false === $result ) {
unset( $error );
return self::upload_error( true, 'network_error', 'The generated file could not be uploaded.' );
$retryable = in_array( $errno, array(
CURLE_COULDNT_RESOLVE_PROXY, CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_CONNECT,
CURLE_OPERATION_TIMEDOUT, CURLE_SEND_ERROR, CURLE_RECV_ERROR, CURLE_GOT_NOTHING, CURLE_PARTIAL_FILE,
), true );
$failure = self::upload_error(
$retryable,
'upload_curl_' . $errno,
in_array( $errno, array( CURLE_SSL_CACERT, CURLE_SSL_CACERT_BADFILE ), true )
? 'The upload certificate could not be verified. Ask your host to check the PHP certificate configuration, then retry publishing.'
: 'The file upload did not finish. Retry publishing. If it fails again, share the upload details below with your host.'
);
// curl_error can contain signed URLs or proxy credentials. The error-code description cannot.
$failure['data'] = array( 'curlCode' => $errno, 'reason' => curl_strerror( $errno ), 'host' => $host, 'durationMs' => $duration_ms );
return $failure;
}
return self::upload_response( $status );
}
Expand Down
17 changes: 9 additions & 8 deletions includes/class-spacefast-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -951,11 +951,12 @@ static function ( array $state ): array {
);
}

public static function static_publish_progress( string $status ): void {
public static function static_publish_progress( string $status, string $message = '' ): void {
if ( ! in_array( $status, array( 'uploading', 'finalizing' ), true ) ) return;
Spacefast_Sync_State::mutate(
static function ( array $state ) use ( $status ): array {
static function ( array $state ) use ( $status, $message ): array {
$state['last_status'] = $status;
$state['last_message'] = $message;
$state['last_attempt_at'] = time();
return $state;
}
Expand Down Expand Up @@ -1162,14 +1163,14 @@ public static function render_admin(): void {
<?php self::action_form( 'spacefast_wordpress_disconnect', __( 'Start over', 'spacefast-wordpress' ), 'secondary' ); ?>
<?php else : ?>
<div class="card" style="max-width:720px;padding:24px">
<h2 style="margin-top:0"><?php echo esc_html( $configured ? self::status_label( (string) $state['last_status'], $static_mode ) : __( 'Finishing setup', 'spacefast-wordpress' ) ); ?></h2>
<h2 style="margin-top:0"><?php echo esc_html( self::status_label( (string) $state['last_status'], $static_mode ) ); ?></h2>
<p><strong><?php echo esc_html( (string) $settings['space_name'] ); ?></strong> · <?php echo esc_html( (string) $settings['team_name'] ); ?></p>
<?php if ( $state['last_message'] ) : ?><div class="notice notice-error inline"><p><?php echo esc_html( (string) $state['last_message'] ); ?></p></div><?php endif; ?>
<?php if ( $settings['live_url'] ) : ?><p><a class="button button-secondary" href="<?php echo esc_url( (string) $settings['live_url'] ); ?>" target="_blank" rel="external noreferrer noopener"><?php esc_html_e( 'Open live site', 'spacefast-wordpress' ); ?></a></p><?php endif; ?>
<?php if ( $state['last_message'] ) : ?><div class="notice notice-<?php echo 'blocked' === $state['last_status'] ? 'error' : 'warning'; ?> inline"><p><?php echo esc_html( (string) $state['last_message'] ); ?></p></div><?php endif; ?>
<?php if ( $configured && $settings['live_url'] ) : ?><p><a class="button button-secondary" href="<?php echo esc_url( (string) $settings['live_url'] ); ?>" target="_blank" rel="external noreferrer noopener"><?php esc_html_e( 'Open live site', 'spacefast-wordpress' ); ?></a></p><?php endif; ?>
<?php if ( $state['last_success_at'] ) : ?><p class="description"><?php echo esc_html( sprintf( __( 'Last published %s', 'spacefast-wordpress' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $state['last_success_at'] ) ) ); ?></p><?php endif; ?>
<?php if ( max( 0, (int) $state['desired'] - (int) $state['delivered'] ) ) : ?><p><?php esc_html_e( 'Changes are waiting. Spacefast will combine them into the next publish.', 'spacefast-wordpress' ); ?></p><?php endif; ?>
<?php if ( 'pending' === $state['last_status'] && max( 0, (int) $state['desired'] - (int) $state['delivered'] ) ) : ?><p><?php esc_html_e( 'Changes are waiting. Spacefast will combine them into the next publish.', 'spacefast-wordpress' ); ?></p><?php endif; ?>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:16px">
<?php if ( $static_mode ) : ?><?php self::action_form( 'spacefast_wordpress_publish', $configured ? __( 'Publish now', 'spacefast-wordpress' ) : __( 'Retry publish', 'spacefast-wordpress' ), 'primary' ); ?>
<?php if ( $static_mode ) : ?><?php self::action_form( 'spacefast_wordpress_publish', 'blocked' === $state['last_status'] || ! $configured ? __( 'Retry publish', 'spacefast-wordpress' ) : __( 'Publish now', 'spacefast-wordpress' ), 'primary' ); ?>
<?php else : ?><?php self::action_form( 'spacefast_wordpress_build', $configured ? __( 'Build now', 'spacefast-wordpress' ) : __( 'Retry build', 'spacefast-wordpress' ), 'primary' ); ?><?php endif; ?>
</div>
</div>
Expand All @@ -1187,7 +1188,7 @@ public static function render_admin(): void {
<?php submit_button( __( 'Save and sync', 'spacefast-wordpress' ), 'secondary', 'submit', false ); ?>
</form>
<?php if ( $settings['last_settings_sync_at'] ) : ?><p class="description"><?php echo esc_html( sprintf( __( 'Settings last synced %s', 'spacefast-wordpress' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $settings['last_settings_sync_at'] ) ) ); ?></p><?php endif; ?>
<details style="max-width:720px;margin-top:12px"><summary><?php esc_html_e( 'Technical details', 'spacefast-wordpress' ); ?></summary><p><?php echo esc_html( $static_mode ? __( 'Version ID:', 'spacefast-wordpress' ) : __( 'Build ID:', 'spacefast-wordpress' ) ); ?> <code><?php echo esc_html( (string) ( $static_mode ? ( $state['last_version_id'] ?: '—' ) : ( $state['last_build_id'] ?: '—' ) ) ); ?></code></p></details>
<details style="max-width:720px;margin-top:12px"><summary><?php esc_html_e( 'Technical details', 'spacefast-wordpress' ); ?></summary><p><?php echo esc_html( $static_mode ? __( 'Version ID:', 'spacefast-wordpress' ) : __( 'Build ID:', 'spacefast-wordpress' ) ); ?> <code><?php echo esc_html( (string) ( $static_mode ? ( $state['last_version_id'] ?: '—' ) : ( $state['last_build_id'] ?: '—' ) ) ); ?></code></p><?php if ( $state['upload_diagnostic'] ) : ?><p><?php esc_html_e( 'Last upload error', 'spacefast-wordpress' ); ?></p><pre style="white-space:pre-wrap"><?php echo esc_html( wp_json_encode( $state['upload_diagnostic'], JSON_PRETTY_PRINT ) ); ?></pre><?php endif; ?></details>
<?php if ( $static_mode && ! self::simply_static_available() ) : ?>
<div class="notice notice-warning inline"><p><?php esc_html_e( 'Static WordPress needs the free Simply Static plugin.', 'spacefast-wordpress' ); ?> <?php echo wp_kses_post( self::simply_static_action_link() ); ?></p></div>
<?php endif; ?>
Expand Down
6 changes: 3 additions & 3 deletions includes/class-spacefast-simply-static-task.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ public function perform() {
$partial_export ? 'additive' : 'snapshot'
);
if ( ! $result['done'] ) {
Spacefast_Plugin::static_publish_progress( $result['status'] );
Spacefast_Plugin::static_publish_progress( $result['status'], $result['message'] ?? '' );
$this->save_status_message(
'finalizing' === $result['status']
$result['message'] ?? ( 'finalizing' === $result['status']
? __( 'Spacefast is activating the uploaded version', 'spacefast-wordpress' )
: sprintf(
/* translators: 1: uploaded files, 2: files requested by Spacefast. */
__( 'Publishing to Spacefast: %1$d of %2$d files', 'spacefast-wordpress' ),
$result['uploaded'],
$result['total']
)
) )
);
return false;
}
Expand Down
46 changes: 41 additions & 5 deletions includes/class-spacefast-static-publisher.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ final class Spacefast_Static_Publisher {
/**
* Advance one bounded step of a Simply Static publish.
*
* @return array{done:bool,version_id:string,uploaded:int,total:int,status:string}
* @return array{done:bool,version_id:string,uploaded:int,total:int,status:string,message?:string}
*/
public static function step(
string $archive_dir,
?Spacefast_Client $client = null,
string $publish_mode = 'snapshot'
string $publish_mode = 'snapshot',
?int $now = null
): array {
if ( ! in_array( $publish_mode, array( 'additive', 'snapshot' ), true ) ) {
throw new InvalidArgumentException( 'Static publish mode is invalid.' );
}
$now = $now ?? time();
$root = self::archive_root( $archive_dir );
$client = $client ?? new Spacefast_Client();
$state = get_option( self::OPTION, array() );
Expand Down Expand Up @@ -89,15 +91,37 @@ public static function step(
$next_target = max( 0, (int) ( $state['next_target'] ?? 0 ) );

if ( isset( $targets[ $next_target ] ) && is_array( $targets[ $next_target ] ) ) {
if ( (int) ( $state['retry_at'] ?? 0 ) > $now ) {
return self::retry_progress( $state );
}
$target = $targets[ $next_target ];
$path = self::target_file( $root, (string) ( $target['path'] ?? '' ) );
$result = $client->upload_static_file( $target, $path );
if ( ! $result['ok'] ) {
if ( in_array( $result['code'], array( 'upload_http_401', 'upload_http_403' ), true ) ) {
return self::resume( $state, $client );
}
Spacefast_Sync_State::mutate(
static function ( array $sync ) use ( $result, $target, $state ): array {
$sync['upload_diagnostic'] = array_merge( $result['data'], array(
'code' => $result['code'],
'file' => $target['path'],
'host' => (string) wp_parse_url( $target['url'], PHP_URL_HOST ),
'versionId' => $state['version_id'],
) );
return $sync;
}
);
$attempt = (int) ( $state['upload_attempts'] ?? 0 ) + 1;
if ( $result['retryable'] && $attempt < 4 ) {
$state['upload_attempts'] = $attempt;
$state['retry_at'] = $now + 2 ** $attempt;
update_option( self::OPTION, $state, false );
return self::retry_progress( $state );
}
throw new RuntimeException( $result['message'] );
}
unset( $state['upload_attempts'], $state['retry_at'] );
$state['next_target'] = $next_target + 1;
$state['uploaded'] = (int) ( $state['uploaded'] ?? 0 ) + 1;
update_option( self::OPTION, $state, false );
Expand All @@ -118,6 +142,18 @@ public static function step(
return self::begin_finalizing( $state );
}

/** @param array<string,mixed> $state */
private static function retry_progress( array $state ): array {
return array(
'done' => false,
'version_id' => $state['version_id'],
'uploaded' => $state['uploaded'],
'total' => $state['total'],
'status' => 'uploading',
'message' => 'Upload interrupted. Retrying automatically from the last completed file.',
);
}

/**
* @return array<int,array{path:string,size:int,sha256:string,contentType:string}>
*/
Expand Down Expand Up @@ -203,7 +239,7 @@ private static function target_file( string $root, string $path ): string {

/**
* @param array<string,mixed> $state State.
* @return array{done:bool,version_id:string,uploaded:int,total:int,status:string}
* @return array{done:bool,version_id:string,uploaded:int,total:int,status:string,message?:string}
*/
private static function resume( array $state, Spacefast_Client $client ): array {
$pages = (int) ( $state['pages'] ?? 0 ) + 1;
Expand Down Expand Up @@ -236,7 +272,7 @@ private static function resume( array $state, Spacefast_Client $client ): array

/**
* @param array<string,mixed> $state State.
* @return array{done:bool,version_id:string,uploaded:int,total:int,status:string}
* @return array{done:bool,version_id:string,uploaded:int,total:int,status:string,message?:string}
*/
private static function begin_finalizing( array $state ): array {
$state['phase'] = 'finalizing';
Expand All @@ -254,7 +290,7 @@ private static function begin_finalizing( array $state ): array {

/**
* @param array<string,mixed> $state State.
* @return array{done:bool,version_id:string,uploaded:int,total:int,status:string}
* @return array{done:bool,version_id:string,uploaded:int,total:int,status:string,message?:string}
*/
private static function poll_version( array $state, Spacefast_Client $client ): array {
$polls = (int) ( $state['polls'] ?? 0 ) + 1;
Expand Down
5 changes: 3 additions & 2 deletions includes/class-spacefast-sync-state.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ final class Spacefast_Sync_State {
const HOOK = 'spacefast_wordpress_deliver';

/**
* @return array{desired:int,delivered:int,event_id:string,reasons:array<int,string>,attempts:int,next_at:int,last_status:string,last_message:string,last_build_id:string,last_version_id:string,last_change_at:int,last_attempt_at:int,last_success_at:int,last_settings_sync_at:int,active_generation:int,settings_pending:bool}
* @return array{desired:int,delivered:int,event_id:string,reasons:array<int,string>,attempts:int,next_at:int,last_status:string,last_message:string,upload_diagnostic:?array,last_build_id:string,last_version_id:string,last_change_at:int,last_attempt_at:int,last_success_at:int,last_settings_sync_at:int,active_generation:int,settings_pending:bool}
*/
public static function defaults(): array {
return array(
Expand All @@ -19,6 +19,7 @@ public static function defaults(): array {
'next_at' => 0,
'last_status' => 'idle',
'last_message' => '',
'upload_diagnostic' => null,
'last_build_id' => '',
'last_version_id' => '',
'last_change_at' => 0,
Expand All @@ -31,7 +32,7 @@ public static function defaults(): array {
}

/**
* @return array{desired:int,delivered:int,event_id:string,reasons:array<int,string>,attempts:int,next_at:int,last_status:string,last_message:string,last_build_id:string,last_version_id:string,last_change_at:int,last_attempt_at:int,last_success_at:int,last_settings_sync_at:int,active_generation:int,settings_pending:bool}
* @return array{desired:int,delivered:int,event_id:string,reasons:array<int,string>,attempts:int,next_at:int,last_status:string,last_message:string,upload_diagnostic:?array,last_build_id:string,last_version_id:string,last_change_at:int,last_attempt_at:int,last_success_at:int,last_settings_sync_at:int,active_generation:int,settings_pending:bool}
*/
public static function get(): array {
$value = get_option( self::OPTION, array() );
Expand Down
7 changes: 6 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Contributors: spacefast
Tags: static site, headless cms, simply static, deployment
Requires at least: 6.5
Requires PHP: 8.1
Stable tag: 0.5.7
Stable tag: 0.5.8
License: GPLv2 or later

Publish a Simply Static export to Spacefast, or rebuild a headless site when WordPress content changes.
Expand Down Expand Up @@ -31,6 +31,11 @@ OAuth access is limited to the Team you authorize and the mode you choose. WordP

== Changelog ==

= 0.5.8 =
* Use WordPress certificate and proxy settings for streaming uploads.
* Retry transient file uploads with bounded backoff and preserve safe diagnostic details.
* Show the actual first-publish state and only offer the live site after successful publishing.

= 0.5.7 =
* Include public custom post type archive routes in Simply Static exports.

Expand Down
4 changes: 2 additions & 2 deletions spacefast-wordpress.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Spacefast
* Plugin URI: https://spacefast.com/
* Description: Publishes static WordPress exports or rebuilds a headless Spacefast site.
* Version: 0.5.7
* Version: 0.5.8
* Update URI: https://github.com/spacefast/wordpress
* Requires at least: 6.5
* Requires PHP: 8.1
Expand All @@ -14,7 +14,7 @@

defined( 'ABSPATH' ) || exit;

define( 'SPACEFAST_WORDPRESS_VERSION', '0.5.7' );
define( 'SPACEFAST_WORDPRESS_VERSION', '0.5.8' );
define( 'SPACEFAST_WORDPRESS_FILE', __FILE__ );

require_once __DIR__ . '/includes/class-spacefast-settings.php';
Expand Down
Loading
Loading