diff --git a/features/core-download.feature b/features/core-download.feature index 1c5b6ad0..154eb897 100644 --- a/features/core-download.feature +++ b/features/core-download.feature @@ -556,3 +556,40 @@ Feature: Download WordPress Success: """ + Scenario: Extracts provided tar.gz files + Given an empty directory + + When I run `wp core download https://downloads.wordpress.org/release/wordpress-7.0.tar.gz --force` + Then the {RUN_DIR} directory should contain: + """ + index.php + license.txt + """ + + Scenario: Extracts provided zip files + Given an empty directory + + When I run `wp core download https://downloads.wordpress.org/release/wordpress-7.0.zip --force` + Then the {RUN_DIR} directory should contain: + """ + index.php + license.txt + """ + + Scenario: Error when downloading an unsupported archive format + Given an empty directory + And that HTTP requests to http://example.com/unsupported.txt will respond with: + """ + HTTP/1.1 200 OK + Content-Type: text/plain + + This is not a zip or tarball file. + """ + + When I try `wp core download http://example.com/unsupported.txt --force` + Then STDERR should contain: + """ + Error: Unsupported archive format. The downloaded file is not a valid zip or tar.gz archive. + """ + And the return code should be 1 + diff --git a/src/Core_Command.php b/src/Core_Command.php index 25003af9..454d8ce7 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -297,11 +297,11 @@ public function download( $args, $assoc_args ) { } if ( ! $cache_file || $bad_cache ) { - $temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . ".{$extension}"; + $temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . '.tmp'; register_shutdown_function( - function () use ( $temp ) { + function () use ( &$temp ) { if ( file_exists( $temp ) ) { - unlink( $temp ); + @unlink( $temp ); } } ); @@ -322,6 +322,69 @@ function () use ( $temp ) { WP_CLI::error( "Couldn't access download URL (HTTP code {$response->status_code})." ); } + if ( ! file_exists( $temp ) || ! is_readable( $temp ) ) { + WP_CLI::error( "Downloaded file could not be written to or read from disk: {$temp}" ); + } + + $extension = ''; + $mime = function_exists( 'mime_content_type' ) ? mime_content_type( $temp ) : ''; + if ( 'application/zip' === $mime || 'application/x-zip-compressed' === $mime ) { + $extension = 'zip'; + } elseif ( 'application/x-gzip' === $mime || 'application/gzip' === $mime ) { + $extension = 'tar.gz'; + } else { + // Fallback to magic bytes. + $handle = @fopen( $temp, 'rb' ); + if ( $handle ) { + $bytes = fread( $handle, 2 ); + fclose( $handle ); + if ( 'PK' === $bytes ) { + $extension = 'zip'; + } elseif ( "\x1f\x8b" === $bytes ) { + $extension = 'tar.gz'; + } + } + } + + // Deep validation for tar.gz archives: verify it's a valid, readable gzip stream. + if ( 'tar.gz' === $extension && function_exists( 'gzopen' ) ) { + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Silence potential gzopen warnings on corrupt streams. + $gz = @gzopen( $temp, 'rb' ); + if ( $gz ) { + $header = gzread( $gz, 262 ); + gzclose( $gz ); + if ( ! is_string( $header ) || strlen( $header ) < 1 ) { + $extension = ''; + } + } else { + $extension = ''; + } + } + + if ( ! in_array( $extension, [ 'zip', 'tar.gz' ], true ) ) { + WP_CLI::error( 'Unsupported archive format. The downloaded file is not a valid zip or tar.gz archive.' ); + } + + $actual_temp = substr( $temp, 0, -4 ) . ".{$extension}"; + if ( ! rename( $temp, $actual_temp ) ) { + // Fallback to copy + unlink. + if ( copy( $temp, $actual_temp ) ) { + $old_temp = $temp; + $temp = $actual_temp; + if ( ! unlink( $old_temp ) ) { + register_shutdown_function( + function () use ( $old_temp ) { + if ( file_exists( $old_temp ) ) { + unlink( $old_temp ); + } + } + ); + } + } + } else { + $temp = $actual_temp; + } + if ( 'nightly' !== $version ) { unset( $options['filename'] ); /** @var \WpOrg\Requests\Response $md5_response */