Skip to content

Commit

Permalink
Merge pull request #163 from woocommerce/24-04/implement-incompatible…
Browse files Browse the repository at this point in the history
…-archive-check

Add `woo:validate-zip <path>` command
  • Loading branch information
rcstr committed May 15, 2024
2 parents 0358dca + 3c72124 commit 56d838c
Show file tree
Hide file tree
Showing 10 changed files with 440 additions and 70 deletions.
34 changes: 34 additions & 0 deletions docs/dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
### How to run tests?

Run `make tests` to run all tests.

Or, run any of the commands below to run tests:

```bash
make phpcs
make phpstan
make phpunit
make phan
```

### How to run tests with Xdebug:

To run tests with Xdebug, you can use the following commands:

```bash
make phpunit DEBUG=1
```

### How to update snapshots:

To update snapshots, run the following command:

```bash
make phpunit ARGS='-d --update-snapshots'
```

You can also combine both:

```bash
make phpunit ARGS='-d --update-snapshots' DEBUG=1
```
8 changes: 4 additions & 4 deletions src/src/Commands/CreateRunCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ public function execute( InputInterface $input, OutputInterface $output ) {
*/
if ( ! file_exists( $options['zip'] ) ) {
$output->writeln( sprintf(
"<error>Error: The specified zip file '%s' does not exist.</error>" .
"<error>Error: The specified ZIP file '%s' does not exist.</error>" .
"<info>\nTo run the command, use one of the following options:" .
"\n1. Provide the zip file name without an argument to infer from the slug or ID:" .
"\n1. Provide the ZIP file name without an argument to infer from the slug or ID:" .
"\n run:security my-extension --zip" .
"\n\n2. Provide the zip path as a parameter:" .
"\n\n2. Provide the ZIP path as a parameter:" .
"\n run:security my-extension --zip=/some/path/my-extension.zip</info>",
$options['zip']
) );
Expand Down Expand Up @@ -266,7 +266,7 @@ public function execute( InputInterface $input, OutputInterface $output ) {
'zip',
null,
InputOption::VALUE_OPTIONAL,
'(Optional) Run the test using a local zip file of the plugin. Useful for running the tests before publishing it to the Marketplace.'
'(Optional) Run the test using a local ZIP file of the plugin. Useful for running the tests before publishing it to the Marketplace.'
);

// JSON Response.
Expand Down
48 changes: 48 additions & 0 deletions src/src/Commands/WooValidateZipCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace QIT_CLI\Commands;

use QIT_CLI\Woo\ZipValidator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class WooValidateZipCommand extends Command {
protected static $defaultName = 'woo:validate-zip'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase

private $zip_validator;

public function __construct( ZipValidator $zip_validator ) {
$this->zip_validator = $zip_validator;

parent::__construct();
}

protected function configure() {
$this
->setDescription( 'Validate a local ZIP file\'s content.' )
->setHelp( 'If invalid content or wrong format is found in ZIP file, an error will be shown.' )
->addArgument( 'path', InputArgument::REQUIRED, 'The ZIP file path' );
}

protected function execute( InputInterface $input, OutputInterface $output ): int {
try {
$zip_file = $input->getArgument( 'path' );

$this->zip_validator->validate_zip( $zip_file );

$output->writeln( '<info>ZIP file content is valid.</info>' );
} catch ( \UnexpectedValueException $e ) {
$output->writeln( sprintf( '<comment>%s</comment>', $e->getMessage() ) );

return Command::FAILURE;
} catch ( \Exception $e ) {
$output->writeln( sprintf( '<error>An error occurred while validating the provided file. Error: %s</error>', $e->getMessage() ) );

return Command::FAILURE;
}

return Command::SUCCESS;
}
}
2 changes: 1 addition & 1 deletion src/src/Environment/EnvironmentDownloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public function maybe_download( string $env_name ): void {
unlink( $temp_zip_path );
switch ( $res ) {
case ZipArchive::ER_NOZIP:
$error = 'Not a zip archive.';
$error = 'Not a ZIP archive.';
break;
case ZipArchive::ER_INCONS:
$error = 'Consistency check failed.';
Expand Down
4 changes: 2 additions & 2 deletions src/src/Upload.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ protected function check_zip_consistency( string $filepath, OutputInterface $out

// Early bail: Tolerable inconsistency.
if ( $opened === 21 ) {
$output->writeln( '<comment>Zip file failed consistency check. We will proceed with the upload, as macOS Archive Utility is known to generate zip files that are non-compliant with the Zip specification.</comment>' );
$output->writeln( '<comment>ZIP file failed consistency check. We will proceed with the upload, as macOS Archive Utility is known to generate ZIP files that are non-compliant with the ZIP specification.</comment>' );

return;
}

if ( $opened !== true ) {
throw new \RuntimeException( 'This is not a valid zip file.' );
throw new \RuntimeException( 'This is not a valid ZIP file.' );
}
}
}
125 changes: 125 additions & 0 deletions src/src/Woo/ZipValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

namespace QIT_CLI\Woo;

use ZipArchive;

class ZipValidator {

/**
* @var string[]
*/
protected $system_files = [
// https://github.com/github/gitignore/blob/main/Global/Windows.gitignore.
'Thumbs.db',
'Thumbs.db:encryptable',
'Desktop.ini',
'desktop.ini',
'ehthumbs.db',
'ehthumbs_vista.db',
'$RECYCLE.BIN/',
// https://github.com/github/gitignore/blob/main/Global/Linux.gitignore.
'~',
'.directory',
// https://github.com/github/gitignore/blob/main/Global/macOS.gitignore.
'.DS_Store',
'.AppleDouble',
'.LSOverride',
'.Spotlight-V100',
'.Trashes',
'.fseventsd',
];

/**
* @param string $filepath
*
* @return \ZipArchive
* @throws \RuntimeException If the file is not a valid zip file.
* @throws \UnexpectedValueException If the zip file is inconsistent.
*/
private function open_file( string $filepath ) {
$zip = new ZipArchive();
$opened = $zip->open( $filepath, ZipArchive::CHECKCONS );

if ( $opened === 21 ) {
throw new \UnexpectedValueException( 'ZIP file is inconsistent. ZIP files generated by the Archive Utility from macOS may be the cause.' );
}

if ( $opened !== true ) {
throw new \RuntimeException( 'This is not a valid ZIP file.' );
}

return $zip;
}

/**
* @param string $filepath
*
* @return void
*/
public function validate_zip( string $filepath ) {
$zip = $this->open_file( $filepath );

// Example (foo => foo/).
$plugin_slug = $this->extract_slug_from_filepath( $filepath );
$parent_dir = strtolower( trim( trim( $plugin_slug ), '/' ) . '/' );

$found_parent_directory = false;

$left = 0;
$right = $zip->numFiles - 1; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

while ( $left <= $right ) {
foreach ( [ $left, $right ] as $i ) {
$info = $zip->statIndex( $i );

if ( ! $info ) {
throw new \RuntimeException( 'Cannot read ZIP index.', 400 );
}

if ( str_contains( $info['name'], '/vendor' ) || str_contains( $info['name'], '/node_modules' ) ) {
continue;
}

if ( $this->is_file_invalid( $info['name'] ) ) {
throw new \RuntimeException( sprintf( 'Invalid (%s) file/folder inside the provided ZIP file', $info['name'] ), 400 );
}

if ( ! $found_parent_directory && str_starts_with( strtolower( $info['name'] ), $parent_dir ) ) {
$found_parent_directory = true;
}
}

++$left;
--$right;
}

$zip->close();
}

/**
* @param string $filepath
*
* @return string
*/
private function extract_slug_from_filepath( string $filepath ): string {
$filename = basename( $filepath );

return substr( $filename, 0, strpos( $filename, '.' ) );
}

/**
* @param string $name
*
* @return bool
*/
protected function is_file_invalid( string $name ): bool {
foreach ( $this->system_files as $system_file ) {
if ( str_ends_with( $name, $system_file ) ) {
return true;
}
}

return false;
}
}
2 changes: 2 additions & 0 deletions src/src/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use QIT_CLI\Commands\Tags\ListTestTagsCommand;
use QIT_CLI\Commands\Tags\UploadTestTagsCommand;
use QIT_CLI\Commands\WooExtensionsCommand;
use QIT_CLI\Commands\WooValidateZipCommand;
use QIT_CLI\Config;
use QIT_CLI\Diagnosis;
use QIT_CLI\Environment\EnvironmentDanglingCleanup;
Expand Down Expand Up @@ -174,6 +175,7 @@ public function filter( $in, $out, &$consumed, $closing ): int {
$application->add( $container->make( DevModeCommand::class ) );
$application->add( $container->make( ConfigDirCommand::class ) );
$application->add( $container->make( ConnectCommand::class ) );
$application->add( $container->make( WooValidateZipCommand::class ) );

// Environment commands.
try {
Expand Down
86 changes: 86 additions & 0 deletions src/tests/Helpers/ZipBuilderHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace QIT_CLI_Tests\Helpers;

use RuntimeException;
use ZipArchive;

class ZipBuilderHelper {
private $filename;
private $files = array();
/**
* @var string
*/
private $filepath;

/**
* @param string $filename The name of the zip file to be created.
*/
public function __construct( $filename ) {
$this->filename = $filename;
$this->filepath = __DIR__ . "/../$this->filename";
}

/**
* Adds a new file to the archive.
*
* @param string $source The file to be added.
* @param string $target The target path in the archive.
*
* @return self The current ZipBuilder instance.
*/
public function with_file( $source, $target ): self {
$this->files[] = array(
'source' => $source,
'target' => $target,
);

return $this;
}

/**
* Builds the zip archive.
*
* @return string The zip file path.
* @throws RuntimeException If the zip file could not be created.
*
*/
public function build(): string {
$zip = new ZipArchive();

if ( $zip->open( $this->get_file_path(), ZipArchive::CREATE ) !== true ) {
throw new RuntimeException( 'Could not create ZIP file.' );
}

foreach ( $this->files as $file ) {
if ( ! $zip->addFile( $file['source'], $file['target'] ) ) {
throw new RuntimeException( 'Could not add file to zip: ' . $file['source'] );
}
}

$zip->close();

clearstatcache();

if ( ! file_exists( $this->get_file_path() ) ) {
throw new RuntimeException( 'ZIP file was not created.' );
}

return $this->get_file_path();
}

function corrupt() {
$data = file_get_contents( $this->get_file_path() );
if ( ! $data ) {
throw new \RuntimeException( 'Could not read ZIP file.' );
}

$data = substr_replace( $data, "", 20, 4 ); // Removing 4 bytes from offset 20

file_put_contents( $this->get_file_path(), $data );
}

public function get_file_path(): string {
return $this->filepath;
}
}

0 comments on commit 56d838c

Please sign in to comment.