Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ Spacefast adds a delivery step after Simply Static has generated and delivered
its files but before cleanup. The step hashes the generated bytes, declares a
full export as an exact snapshot through Spacefast's normal version API,
uploads only the files Spacefast requests, waits for activation, and records
whether that version became live. Partial, update, build, and 404-only exports
whether that version became live. Static WordPress exports are public by
default; an explicit `sf.jsonc` in the export is preserved. Partial, update,
build, and 404-only exports
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
Expand Down
3 changes: 3 additions & 0 deletions includes/class-spacefast-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,9 @@ public function upload_static_file( array $target, string $file_path ): array {
}

public static function content_type_for_file( string $file_path ): string {
if ( 'jsonc' === strtolower( (string) pathinfo( $file_path, PATHINFO_EXTENSION ) ) ) {
return 'application/json';
}
$filetype = wp_check_filetype( $file_path );
$type = is_array( $filetype ) ? (string) ( $filetype['type'] ?? '' ) : '';
return '' !== $type ? $type : 'application/octet-stream';
Expand Down
15 changes: 15 additions & 0 deletions includes/class-spacefast-static-publisher.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public static function step(
*/
public static function manifest( string $archive_dir ): array {
$root = self::archive_root( $archive_dir );
self::ensure_public_config( $root );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject empty exports before creating the config

When Simply Static produces an empty archive after a crawl or export failure, this call creates sf.jsonc before the existing empty-manifest check runs, so the export is treated as valid. A full export is submitted in snapshot mode by Spacefast_Simply_Static_Publish_Task::perform(), causing Spacefast to replace the live site with a version containing only the config file instead of preserving the current version and reporting that no files were generated.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the empty-export guard effective.

Because Line [126] creates sf.jsonc before the empty-manifest check, an empty archive produces a one-file manifest. step() can then publish a version containing only sf.jsonc instead of rejecting the export. Check for pre-existing export files before creating the default configuration, or preserve the original file count for the empty check. Add a regression test for an empty archive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/class-spacefast-static-publisher.php` at line 126, Update the export
flow around ensure_public_config() so the empty-export guard evaluates the
archive’s pre-existing file count before sf.jsonc is created, preserving
rejection of genuinely empty archives. Add a regression test covering an empty
archive and confirming step() does not publish it.

$files = array();
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS )
Expand Down Expand Up @@ -162,6 +163,20 @@ public static function reset(): void {
delete_option( self::OPTION );
}

private static function ensure_public_config( string $root ): void {
$config = $root . DIRECTORY_SEPARATOR . 'sf.jsonc';
if ( file_exists( $config ) || is_link( $config ) ) {
if ( ! is_file( $config ) || is_link( $config ) || ! is_readable( $config ) ) {
throw new RuntimeException( 'The generated Spacefast configuration is not a readable file.' );
}
return;
}
$written = file_put_contents( $config, "{\n \"access\": \"public\"\n}\n", LOCK_EX );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve deployed access policies during additive exports

When a site already has a restrictive sf.jsonc from a prior full export and a partial, update, build, or 404-only export omits that file, this unconditional default writes a public policy into the partial archive. Those exports are sent in additive mode, so the generated file replaces the deployed policy and unexpectedly makes the site public; preservation currently works only when every partial archive happens to include the custom config.

Useful? React with 👍 / 👎.

if ( false === $written ) {
throw new RuntimeException( 'The generated export could not be made public on Spacefast.' );
}
}

private static function archive_root( string $archive_dir ): string {
$root = realpath( $archive_dir );
if ( false === $root || ! is_dir( $root ) || ! is_readable( $root ) ) {
Expand Down
5 changes: 4 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.5
Stable tag: 0.5.6
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,9 @@ OAuth access is limited to the Team you authorize and the mode you choose. WordP

== Changelog ==

= 0.5.6 =
* Make static WordPress exports publicly readable by default while preserving an explicit sf.jsonc access policy.

= 0.5.5 =
* Declare and send each exported file's media type so managed hosting accepts binary uploads.

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.5
* Version: 0.5.6
* 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.5' );
define( 'SPACEFAST_WORDPRESS_VERSION', '0.5.6' );
define( 'SPACEFAST_WORDPRESS_FILE', __FILE__ );

require_once __DIR__ . '/includes/class-spacefast-settings.php';
Expand Down
2 changes: 1 addition & 1 deletion tests/acceptance/wordpress.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function spacefast_accept( bool $condition, string $message ): void {
);

$plugin = get_plugin_data( WP_PLUGIN_DIR . '/spacefast-wordpress/spacefast-wordpress.php', false, false );
spacefast_accept( '0.5.5' === $plugin['Version'], 'Unexpected plugin version.' );
spacefast_accept( '0.5.6' === $plugin['Version'], 'Unexpected plugin version.' );
spacefast_accept( 'https://github.com/spacefast/wordpress' === $plugin['UpdateURI'], 'Update URI is missing.' );

Spacefast_Settings::disconnect();
Expand Down
36 changes: 29 additions & 7 deletions tests/behavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -847,14 +847,28 @@ static function ( string $url ) use ( &$invalid_receipt_requests ): array {
file_put_contents( $archive . '/assets/app.js', 'console.log("spacefast")' );
$manifest = Spacefast_Static_Publisher::manifest( $archive );
check(
array( 'assets/app.js', 'index.html' ) === array_column( $manifest, 'path' ),
array( 'assets/app.js', 'index.html', 'sf.jsonc' ) === array_column( $manifest, 'path' ),
'builds a sorted snapshot manifest from generated files'
);
check(
hash_file( 'sha256', $archive . '/index.html' ) === $manifest[1]['sha256'],
'hashes the generated bytes instead of trusting exporter metadata'
);
check( 'text/javascript' === $manifest[0]['contentType'], 'declares generated asset media types in the publish manifest' );
check( 'application/json' === $manifest[2]['contentType'], 'declares the generated Spacefast config as JSON' );
check(
'public' === ( json_decode( file_get_contents( $archive . '/sf.jsonc' ), true )['access'] ?? '' ),
'makes a static WordPress export publicly readable by default'
);
$custom_archive = sys_get_temp_dir() . '/spacefast-wordpress-custom-config-' . uniqid();
mkdir( $custom_archive );
file_put_contents( $custom_archive . '/index.html', '<h1>Custom</h1>' );
file_put_contents( $custom_archive . '/sf.jsonc', '{"access":{"public":["/docs/**"]}}' );
Spacefast_Static_Publisher::manifest( $custom_archive );
check(
'{"access":{"public":["/docs/**"]}}' === file_get_contents( $custom_archive . '/sf.jsonc' ),
'preserves an explicit Spacefast access configuration'
);
check( 'image/png' === Spacefast_Client::content_type_for_file( '/tmp/preview.png' ), 'sends binary uploads with their media type' );
check(
'application/octet-stream' === Spacefast_Client::content_type_for_file( '/tmp/unknown.custom' ),
Expand Down Expand Up @@ -883,7 +897,7 @@ static function ( string $url, array $args ) use ( &$static_requests ): array {
'id' => 'upl_static',
'mode' => 'dist',
'expiresAt' => '2030-01-01T00:00:00.000Z',
'summary' => array( 'upload' => 1, 'reused' => 0, 'ignored' => 0 ),
'summary' => array( 'upload' => 1, 'reused' => 1, 'ignored' => 0 ),
'targets' => array(
array(
'path' => 'assets/app.js',
Expand All @@ -905,10 +919,10 @@ static function ( string $url, array $args ) use ( &$static_requests ): array {
'id' => 'upl_static',
'mode' => 'dist',
'expiresAt' => '2030-01-01T00:00:00.000Z',
'summary' => array( 'upload' => 2, 'reused' => 0, 'ignored' => 0 ),
'targets' => array(
array(
'path' => 'index.html',
'summary' => array( 'upload' => 2, 'reused' => 1, 'ignored' => 0 ),
'targets' => array(
array(
'path' => 'sf.jsonc',
'method' => 'PUT',
'url' => 'https://uploads.example.test/file',
'headers' => array( 'X-Upload' => 'signed' ),
Expand Down Expand Up @@ -945,9 +959,13 @@ static function ( string $url, string $method, array $headers, string $file ) us
'attributes the published version to its public WordPress site'
);
check(
str_ends_with( $static_requests[1][3], '/index.html' ),
str_ends_with( $static_requests[1][3], '/sf.jsonc' ),
'uploads only the file named by the opaque server target'
);
check(
'public' === ( json_decode( file_get_contents( $static_requests[1][3] ), true )['access'] ?? '' ),
'uploads the generated public-access declaration with the export'
);
check(
str_ends_with( $static_requests[3][3], '/assets/app.js' ),
'uploads the next opaque target after resuming'
Expand Down Expand Up @@ -1034,7 +1052,11 @@ public function run_static_export() { self::$runs++; return true; }
unlink( $archive . '/assets/app.js' );
rmdir( $archive . '/assets' );
unlink( $archive . '/index.html' );
unlink( $archive . '/sf.jsonc' );
rmdir( $archive );
unlink( $custom_archive . '/index.html' );
unlink( $custom_archive . '/sf.jsonc' );
rmdir( $custom_archive );
Spacefast_Static_Publisher::reset();

fwrite( STDOUT, "Spacefast WordPress behavior tests: PASS\n" );
Loading