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
16 changes: 16 additions & 0 deletions src/class-ss-util.php
Original file line number Diff line number Diff line change
Expand Up @@ -2825,6 +2825,22 @@ public static function add_archive_status_message( $messages, $task_name, $messa
public static function abs_path_to_url( $path = '' ) {
$normalized_path = wp_normalize_path( $path );

// Uploads may live outside WP_CONTENT_DIR (or behind a symlink), so use
// WordPress's authoritative filesystem-to-URL mapping before checking the
// standard WordPress roots.
if ( function_exists( 'wp_upload_dir' ) ) {
$upload_dir = wp_upload_dir();
if ( is_array( $upload_dir ) && ! empty( $upload_dir['basedir'] ) && ! empty( $upload_dir['baseurl'] ) ) {
$normalized_upload_dir = wp_normalize_path( untrailingslashit( $upload_dir['basedir'] ) );

if ( $normalized_path === $normalized_upload_dir || 0 === strpos( $normalized_path, $normalized_upload_dir . '/' ) ) {
$url = untrailingslashit( $upload_dir['baseurl'] ) . substr( $normalized_path, strlen( $normalized_upload_dir ) );

return esc_url_raw( $url );
}
}
}

// Check if the path is within WP_CONTENT_DIR
if ( defined( 'WP_CONTENT_DIR' ) && defined( 'WP_CONTENT_URL' ) ) {
$normalized_content_dir = wp_normalize_path( untrailingslashit( WP_CONTENT_DIR ) );
Expand Down
37 changes: 31 additions & 6 deletions src/tasks/class-ss-create-zip-archive.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,7 @@ public function create_zip() {

do_action( 'ss_zip_file_created', $zip_archive );

$download_url = Util::abs_path_to_url( $zip_archive->zipname );

return $download_url;
return $this->get_zip_download_url( $zip_archive->zipname );
}

/**
Expand All @@ -215,7 +213,35 @@ private function create_empty_zip( $zip_filename ) {

do_action( 'ss_zip_file_created', (object) array( 'zipname' => $zip_filename ) );

return Util::abs_path_to_url( $zip_filename );
return $this->get_zip_download_url( $zip_filename );
}

/**
* Validate a completed ZIP and resolve its public download URL.
*
* @param string $zip_filename Full path to the zip file.
*
* @return string|WP_Error Download URL on success, WP_Error otherwise.
*/
private function get_zip_download_url( $zip_filename ) {
clearstatcache( true, $zip_filename );

if ( ! is_file( $zip_filename ) || ! is_readable( $zip_filename ) || filesize( $zip_filename ) < 22 ) {
return new \WP_Error(
'zip_file_missing',
__( 'The ZIP archive could not be found after it was created.', 'simply-static' )
);
}

$download_url = Util::abs_path_to_url( $zip_filename );
if ( empty( $download_url ) ) {
return new \WP_Error(
'zip_url_unavailable',
__( 'The ZIP archive was created, but its temporary files directory is not publicly accessible. Choose a directory inside the WordPress uploads directory and try again.', 'simply-static' )
);
}

return $download_url;
}

/**
Expand Down Expand Up @@ -489,7 +515,6 @@ private function create_zip_batched( $zip_filename, $archive_dir, $batch_offset,

do_action( 'ss_zip_file_created', (object) array( 'zipname' => $zip_filename ) );

$download_url = Util::abs_path_to_url( $zip_filename );
return $download_url;
return $this->get_zip_download_url( $zip_filename );
}
}
4 changes: 4 additions & 0 deletions tests/Unit/UtilSecurityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ public function test_absolute_paths_are_converted_only_inside_wordpress_roots():
WP_CONTENT_URL . '/simply-static/export.zip',
Util::abs_path_to_url( WP_CONTENT_DIR . '/simply-static/export.zip' )
);
self::assertSame(
WpEnv::$upload_dir['baseurl'] . '/simply-static/temp-files/export.zip',
Util::abs_path_to_url( WpEnv::$upload_dir['basedir'] . '/simply-static/temp-files/export.zip' )
);
self::assertSame(
'https://example.test/wp-content-backup/export.zip',
Util::abs_path_to_url( WP_CONTENT_DIR . '-backup/export.zip' )
Expand Down
31 changes: 31 additions & 0 deletions tests/Unit/ZipArchiveTaskTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,37 @@ public function test_perform_publishes_structured_download_link(): void {
self::assertStringEndsWith( '/activity-link-test.zip', $status['link']['url'] );
}

public function test_perform_maps_an_external_uploads_directory_to_its_public_url(): void {
file_put_contents( $this->archive_dir . 'index.html', '<h1>Archived</h1>' );

self::assertTrue( ( new Create_Zip_Archive_Task() )->perform() );

$zip_file = untrailingslashit( $this->archive_dir ) . '.zip';
$status = WpEnv::$options['simply-static']['archive_status_messages']['create_zip_archive'];
$relative = substr( wp_normalize_path( $zip_file ), strlen( wp_normalize_path( WpEnv::$upload_dir['basedir'] ) ) );

self::assertFileExists( $zip_file );
self::assertGreaterThanOrEqual( 22, filesize( $zip_file ) );
self::assertSame( WpEnv::$upload_dir['baseurl'] . $relative, $status['link']['url'] );
}

public function test_perform_does_not_report_success_when_the_zip_has_no_public_url(): void {
$private_temp_dir = sys_get_temp_dir() . '/simply-static-private-zip-' . uniqid( '', true );
$private_archive = trailingslashit( $private_temp_dir ) . 'site/';
wp_mkdir_p( $private_archive );
file_put_contents( $private_archive . 'index.html', '<h1>Archived</h1>' );

WpEnv::$options['simply-static']['temp_files_dir'] = $private_temp_dir;
Options::reinstance();

$result = ( new Create_Zip_Archive_Task() )->perform();

self::assertInstanceOf( \WP_Error::class, $result );
self::assertSame( 'zip_url_unavailable', $result->get_error_code() );
self::assertFileExists( untrailingslashit( $private_archive ) . '.zip' );
self::assertArrayNotHasKey( 'create_zip_archive', WpEnv::$options['simply-static']['archive_status_messages'] );
}

public function test_filtered_files_outside_archive_and_symlink_escapes_are_excluded(): void {
file_put_contents( $this->archive_dir . 'safe.txt', 'safe' );
$outside = WpEnv::$upload_dir['basedir'] . '/outside-secret.txt';
Expand Down
Loading