From 2e0d59160e8757d8eb5694025092770d4453a2aa Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Sat, 2 Mar 2024 14:37:03 +0100 Subject: [PATCH] Handle PHP version incompatibility issues, test UnzipStepRunner --- composer.json | 3 +- src/WordPress/Blueprints/BlueprintParser.php | 7 +- .../Blueprints/Progress/ProgressEvent.php | 22 +++- src/WordPress/Blueprints/Progress/Tracker.php | 2 +- .../Blueprints/Resources/ResourceManager.php | 2 - .../Runner/Step/UnzipStepRunner.php | 17 ++-- .../Zip/ZipCentralDirectoryEntry.php | 72 ++++++++++---- .../Zip/ZipEndCentralDirectoryEntry.php | 31 ++++-- src/WordPress/Zip/ZipFileEntry.php | 51 +++++++--- src/WordPress/Zip/ZipStreamReader.php | 2 +- src/WordPress/Zip/functions.php | 18 ++-- .../Runner/Step/UnzipStepRunnerTest.php | 94 ++++++++++++++++++ tests/Blueprints/Runner/Step/test_zip.txt | 1 + tests/Blueprints/Runner/Step/test_zip.zip | Bin 0 -> 164 bytes tests/Unit/UnzipStep.php | 23 ----- 15 files changed, 249 insertions(+), 96 deletions(-) create mode 100644 tests/Blueprints/Runner/Step/UnzipStepRunnerTest.php create mode 100644 tests/Blueprints/Runner/Step/test_zip.txt create mode 100644 tests/Blueprints/Runner/Step/test_zip.zip delete mode 100644 tests/Unit/UnzipStep.php diff --git a/composer.json b/composer.json index 5ae966c2..335c8bf5 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "symfony/http-kernel": "*", "pimple/pimple": "*", "psr/simple-cache": "*", - "opis/json-schema": "*" + "opis/json-schema": "*", + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "*", diff --git a/src/WordPress/Blueprints/BlueprintParser.php b/src/WordPress/Blueprints/BlueprintParser.php index 76b98330..d102ccb4 100644 --- a/src/WordPress/Blueprints/BlueprintParser.php +++ b/src/WordPress/Blueprints/BlueprintParser.php @@ -36,10 +36,9 @@ public function parse($rawBlueprint) 'Unsupported $rawBlueprint type. Use a JSON string, a parsed JSON object, or a BlueprintBuilder instance.' ); } - // TODO Evaluate waring: missing function's return type - public function fromJson($json) - { - // TODO Evaluate warning: 'ext-json' is missing in composer.json + + public function fromJson($json): ?Blueprint + { return $this->fromObject(json_decode($json, false)); } diff --git a/src/WordPress/Blueprints/Progress/ProgressEvent.php b/src/WordPress/Blueprints/Progress/ProgressEvent.php index ba4c449f..11355033 100644 --- a/src/WordPress/Blueprints/Progress/ProgressEvent.php +++ b/src/WordPress/Blueprints/Progress/ProgressEvent.php @@ -8,11 +8,25 @@ * Custom event providing progress details. */ class ProgressEvent extends Event { + /** + * The progress percentage as a number between 0 and 100. + * + * @var float $progress + */ + public float $progress; + + /** + * The caption to display during progress, a string. + * + * @var ?string $caption + */ + public ?string $caption; + public function __construct( - /** The progress percentage as a number between 0 and 100. */ - public float $progress, - /** The caption to display during progress, a string. */ - public ?string $caption, + float $progress, + ?string $caption ) { + $this->caption = $caption; + $this->progress = $progress; } } diff --git a/src/WordPress/Blueprints/Progress/Tracker.php b/src/WordPress/Blueprints/Progress/Tracker.php index b68d6b3d..ed23b027 100644 --- a/src/WordPress/Blueprints/Progress/Tracker.php +++ b/src/WordPress/Blueprints/Progress/Tracker.php @@ -46,7 +46,7 @@ class Tracker { private $weight; private $subTrackers = []; - public readonly EventDispatcher $events; + public EventDispatcher $events; public function __construct( $options = [] ) { $this->weight = $options['weight'] ?? 1; diff --git a/src/WordPress/Blueprints/Resources/ResourceManager.php b/src/WordPress/Blueprints/Resources/ResourceManager.php index 13c7afd1..4bfffd87 100644 --- a/src/WordPress/Blueprints/Resources/ResourceManager.php +++ b/src/WordPress/Blueprints/Resources/ResourceManager.php @@ -35,7 +35,6 @@ public function getStream( $key ) { return $this->map[ $key ]; } - public function bufferToTemporaryFile( $resource, $callback, $suffix = null ) { $fp = $this->getStream( $resource ); $path = $this->fs->tempnam( sys_get_temp_dir(), 'resource', $suffix ); @@ -47,5 +46,4 @@ public function bufferToTemporaryFile( $resource, $callback, $suffix = null ) { $this->fs->remove( $path ); } } - } diff --git a/src/WordPress/Blueprints/Runner/Step/UnzipStepRunner.php b/src/WordPress/Blueprints/Runner/Step/UnzipStepRunner.php index 22d081b3..081ba247 100644 --- a/src/WordPress/Blueprints/Runner/Step/UnzipStepRunner.php +++ b/src/WordPress/Blueprints/Runner/Step/UnzipStepRunner.php @@ -8,15 +8,20 @@ class UnzipStepRunner extends BaseStepRunner { + /** + * Runs the Unzip Step + * + * @param UnzipStep $input Step. + * @param Tracker $progress_tracker Tracker. + * @return void + */ public function run( UnzipStep $input, - Tracker $progress = null + Tracker $progress_tracker ) { - $progress?->set( 10, 'Unzipping...' ); + $progress_tracker->set( 10, 'Unzipping...' ); - // @TODO: Expose a generic helper method for this, e.g. $this->getExecutionContext()->resolvePath($input->extractToPath); - $toPath = $this->getRuntime()->getDocumentRoot() . '/' . $input->extractToPath; - zip_extract_to( $this->getResource( $input->zipFile ), $toPath ); + $resolved_to_path = $this->getRuntime()->resolvePath( $input->extractToPath ); + zip_extract_to( $this->getResource( $input->zipFile ), $resolved_to_path ); } - } diff --git a/src/WordPress/Zip/ZipCentralDirectoryEntry.php b/src/WordPress/Zip/ZipCentralDirectoryEntry.php index 29f87d2d..0527dd7e 100644 --- a/src/WordPress/Zip/ZipCentralDirectoryEntry.php +++ b/src/WordPress/Zip/ZipCentralDirectoryEntry.php @@ -4,28 +4,62 @@ class ZipCentralDirectoryEntry { - public readonly bool $isDirectory; + public bool $isDirectory; + public int $firstByteAt; + public int $versionCreated; + public int $versionNeeded; + public int $generalPurpose; + public int $compressionMethod; + public int $lastModifiedTime; + public int $lastModifiedDate; + public int $crc; + public int $compressedSize; + public int $uncompressedSize; + public int $diskNumber; + public int $internalAttributes; + public int $externalAttributes; + public int $lastByteAt; + public string $path; + public string $extra; + public string $fileComment; public function __construct( - public readonly int $versionCreated, - public readonly int $versionNeeded, - public readonly int $generalPurpose, - public readonly int $compressionMethod, - public readonly int $lastModifiedTime, - public readonly int $lastModifiedDate, - public readonly int $crc, - public readonly int $compressedSize, - public readonly int $uncompressedSize, - public readonly int $diskNumber, - public readonly int $internalAttributes, - public readonly int $externalAttributes, - public readonly int $firstByteAt, - public readonly int $lastByteAt, - public readonly string $path, - public readonly string $extra, - public readonly string $fileComment, + int $versionCreated, + int $versionNeeded, + int $generalPurpose, + int $compressionMethod, + int $lastModifiedTime, + int $lastModifiedDate, + int $crc, + int $compressedSize, + int $uncompressedSize, + int $diskNumber, + int $internalAttributes, + int $externalAttributes, + int $firstByteAt, + int $lastByteAt, + string $path, + string $extra, + string $fileComment ) { - $this->isDirectory = $this->path[ - 1 ] === '/'; + $this->fileComment = $fileComment; + $this->extra = $extra; + $this->path = $path; + $this->lastByteAt = $lastByteAt; + $this->externalAttributes = $externalAttributes; + $this->internalAttributes = $internalAttributes; + $this->diskNumber = $diskNumber; + $this->uncompressedSize = $uncompressedSize; + $this->compressedSize = $compressedSize; + $this->crc = $crc; + $this->lastModifiedDate = $lastModifiedDate; + $this->lastModifiedTime = $lastModifiedTime; + $this->compressionMethod = $compressionMethod; + $this->generalPurpose = $generalPurpose; + $this->versionNeeded = $versionNeeded; + $this->versionCreated = $versionCreated; + $this->firstByteAt = $firstByteAt; + $this->isDirectory = $this->path[- 1] === '/'; } public function isFileEntry() { diff --git a/src/WordPress/Zip/ZipEndCentralDirectoryEntry.php b/src/WordPress/Zip/ZipEndCentralDirectoryEntry.php index e6d70228..ed1df1df 100644 --- a/src/WordPress/Zip/ZipEndCentralDirectoryEntry.php +++ b/src/WordPress/Zip/ZipEndCentralDirectoryEntry.php @@ -4,17 +4,32 @@ class ZipEndCentralDirectoryEntry { + public int $diskNumber; + public int $centralDirectoryStartDisk; + public int $numberCentralDirectoryRecordsOnThisDisk; + public int $numberCentralDirectoryRecords; + public int $centralDirectorySize; + public int $centralDirectoryOffset; + public string $comment; + public function __construct( - public readonly int $diskNumber, - public readonly int $centralDirectoryStartDisk, - public readonly int $numberCentralDirectoryRecordsOnThisDisk, - public readonly int $numberCentralDirectoryRecords, - public readonly int $centralDirectorySize, - public readonly int $centralDirectoryOffset, - public readonly string $comment + int $diskNumber, + int $centralDirectoryStartDisk, + int $numberCentralDirectoryRecordsOnThisDisk, + int $numberCentralDirectoryRecords, + int $centralDirectorySize, + int $centralDirectoryOffset, + string $comment ) { + $this->comment = $comment; + $this->centralDirectoryOffset = $centralDirectoryOffset; + $this->centralDirectorySize = $centralDirectorySize; + $this->numberCentralDirectoryRecords = $numberCentralDirectoryRecords; + $this->numberCentralDirectoryRecordsOnThisDisk = $numberCentralDirectoryRecordsOnThisDisk; + $this->centralDirectoryStartDisk = $centralDirectoryStartDisk; + $this->diskNumber = $diskNumber; } - + public function isFileEntry() { return false; } diff --git a/src/WordPress/Zip/ZipFileEntry.php b/src/WordPress/Zip/ZipFileEntry.php index 44e1cbbc..3b794525 100644 --- a/src/WordPress/Zip/ZipFileEntry.php +++ b/src/WordPress/Zip/ZipFileEntry.php @@ -3,26 +3,47 @@ namespace WordPress\Zip; class ZipFileEntry { - public readonly bool $isDirectory; + public bool $isDirectory; + public int $version; + public int $generalPurpose; + public int $compressionMethod; + public int $lastModifiedTime; + public int $lastModifiedDate; + public int $crc; + public int $compressedSize; + public int $uncompressedSize; + public string $path; + public string $extra; + public string $bytes; public function __construct( - public readonly int $version, - public readonly int $generalPurpose, - public readonly int $compressionMethod, - public readonly int $lastModifiedTime, - public readonly int $lastModifiedDate, - public readonly int $crc, - public readonly int $compressedSize, - public readonly int $uncompressedSize, - public readonly string $path, - public readonly string $extra, - public readonly string $bytes + int $version, + int $generalPurpose, + int $compressionMethod, + int $lastModifiedTime, + int $lastModifiedDate, + int $crc, + int $compressedSize, + int $uncompressedSize, + string $path, + string $extra, + string $bytes ) { - $this->isDirectory = $this->path[ - 1 ] === '/'; + $this->bytes = $bytes; + $this->extra = $extra; + $this->path = $path; + $this->uncompressedSize = $uncompressedSize; + $this->compressedSize = $compressedSize; + $this->crc = $crc; + $this->lastModifiedDate = $lastModifiedDate; + $this->lastModifiedTime = $lastModifiedTime; + $this->compressionMethod = $compressionMethod; + $this->generalPurpose = $generalPurpose; + $this->version = $version; + $this->isDirectory = $this->path[- 1] === '/'; } - + public function isFileEntry() { return true; } - } diff --git a/src/WordPress/Zip/ZipStreamReader.php b/src/WordPress/Zip/ZipStreamReader.php index 409e31f5..6d32671c 100644 --- a/src/WordPress/Zip/ZipStreamReader.php +++ b/src/WordPress/Zip/ZipStreamReader.php @@ -192,7 +192,7 @@ static protected function readEndCentralDirectoryEntry( $stream ): ZipEndCentral * * @return false|string */ - static protected function read_bytes( $stream, $length ): string|bool { + static protected function read_bytes( $stream, $length ) { if ( $length === 0 ) { return ''; } diff --git a/src/WordPress/Zip/functions.php b/src/WordPress/Zip/functions.php index 3c009533..4f02a0f2 100644 --- a/src/WordPress/Zip/functions.php +++ b/src/WordPress/Zip/functions.php @@ -2,17 +2,20 @@ namespace WordPress\Zip; +use Symfony\Component\Filesystem\Path; + function zip_read_entry( $fp ) { return ZipStreamReader::readEntry( $fp ); } -function zip_extract_to( $fp, $toPath ) { +function zip_extract_to( $fp, $to_path ) { while ( $entry = zip_read_entry( $fp ) ) { if ( ! $entry->isFileEntry() ) { continue; } - $path = $toPath . '/' . sanitize_path( $entry->path ); - $parent = dirname( $path ); + + $path = Path::canonicalize( $to_path . '/' . $entry->path ); + $parent = Path::getDirectory( $path ); if ( ! is_dir( $parent ) ) { mkdir( $parent, 0777, true ); } @@ -28,12 +31,3 @@ function zip_extract_to( $fp, $toPath ) { return feof( $fp ) ? 1 : 0; } - -// @TODO: Find a more reliable technique -function sanitize_path( $path ) { - if ( empty( $path ) ) { - return ''; - } - - return preg_replace( '#/\.+(/|$)#', '/', $path ); -} diff --git a/tests/Blueprints/Runner/Step/UnzipStepRunnerTest.php b/tests/Blueprints/Runner/Step/UnzipStepRunnerTest.php new file mode 100644 index 00000000..835717c0 --- /dev/null +++ b/tests/Blueprints/Runner/Step/UnzipStepRunnerTest.php @@ -0,0 +1,94 @@ +document_root = Path::makeAbsolute( 'test', sys_get_temp_dir() ); + $this->runtime = new Runtime( $this->document_root ); + + $this->resource_manager = $this->createStub( ResourceManager::class ); + + $this->step = new UnzipStepRunner(); + $this->step->setRuntime( $this->runtime ); + $this->step->setResourceManager( $this->resource_manager ); + + $this->file_system = new Filesystem(); + } + + /** + * @after + */ + public function after() { + $this->file_system->remove( $this->document_root ); + } + + public function testUnzipFileWhenUsingAbsolutePath(): void { + $zip = __DIR__ . '/test_zip.zip'; + $this->resource_manager->method( 'getStream' ) + ->willReturn( fopen( $zip, 'rb' ) ); + + $input = new UnzipStep(); + $input->setZipFile( $zip ); + $extracted_file_path = $this->runtime->resolvePath( 'dir/test_zip.txt' ); + $input->setExtractToPath( Path::getDirectory( $extracted_file_path ) ); + + $this->step->run( $input, new Tracker() ); + + $this->assertFileEquals( __DIR__ . '/test_zip.txt', $extracted_file_path ); + } + + public function testUnzipFileWhenUsingRelativePath(): void { + $zip = __DIR__ . '/test_zip.zip'; + $this->resource_manager->method( 'getStream' ) + ->willReturn( fopen( $zip, 'rb' ) ); + + $input = new UnzipStep(); + $input->setZipFile( $zip ); + $input->setExtractToPath( 'dir' ); + + $this->step->run( $input, new Tracker() ); + + $extracted_file_path = $this->runtime->resolvePath( 'dir/test_zip.txt' ); + $this->assertFileEquals( __DIR__ . '/test_zip.txt', $extracted_file_path ); + } +} diff --git a/tests/Blueprints/Runner/Step/test_zip.txt b/tests/Blueprints/Runner/Step/test_zip.txt new file mode 100644 index 00000000..3b124649 --- /dev/null +++ b/tests/Blueprints/Runner/Step/test_zip.txt @@ -0,0 +1 @@ +TEST \ No newline at end of file diff --git a/tests/Blueprints/Runner/Step/test_zip.zip b/tests/Blueprints/Runner/Step/test_zip.zip new file mode 100644 index 0000000000000000000000000000000000000000..cf95a4cc28254b1dcd76574dbd47940395833a9a GIT binary patch literal 164 zcmWIWW@Zs#U|`^2P|Hh;*fIIlJ2oJX1&DcoxFof>B)%%MK(C~tgu9TJkC7q3n~_PL z0hbOHpdJuV0F$WtxuBwq3=#}7Z;Un^$-l)2ql5BnmVx*I-mD<&85n^u4oDk-%>e)# C$s(fw literal 0 HcmV?d00001 diff --git a/tests/Unit/UnzipStep.php b/tests/Unit/UnzipStep.php deleted file mode 100644 index da49c96a..00000000 --- a/tests/Unit/UnzipStep.php +++ /dev/null @@ -1,23 +0,0 @@ -execute(); - - expect( file_exists( __DIR__ . "/test/TEST" ) )->toBeTrue(); - - fclose( $fp ); - } finally { - if ( file_exists( __DIR__ . "/test/TEST" ) ) { - unlink( __DIR__ . "/test/TEST" ); - } - if ( file_exists( __DIR__ . "/test" ) ) { - rmdir( __DIR__ . "/test" ); - } - } -} );