Skip to content

Commit

Permalink
Filesystem API: Introduce filters for before/after unzipping archives.
Browse files Browse the repository at this point in the history
This introduces the following new filters which wrap the process of unzipping an archive:
- `pre_unzip_file` - Filters archive unzipping to allow an override with a custom process.
- `unzip_file` - Filters the result of unzipping an archive.

Both filters pass the following:
- `string $file` - Full path and filename of ZIP archive.
- `string $to` - Full path on the filesystem to extract archive to.
- `string[] $needed_dirs` - A full list of required folders that need to be created.
- `float|false $required_space` - The space required to unzip the file and copy its contents, with a 10% buffer.

Props dfavor, azaozz, oglekler, afragen, costdev.
Fixes #37719.

git-svn-id: https://develop.svn.wordpress.org/trunk@56689 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
costdev authored and mikachan committed Sep 26, 2023
1 parent 0eae815 commit ef7fa1c
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 6 deletions.
63 changes: 57 additions & 6 deletions src/wp-admin/includes/file.php
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,9 @@ function _unzip_file_ziparchive( $file, $to, $needed_dirs = array() ) {
}
}

// Enough space to unzip the file and copy its contents, with a 10% buffer.
$required_space = $uncompressed_size * 2.1;

/*
* disk_free_space() could return false. Assume that any falsey value is an error.
* A disk that has zero free bytes has bigger problems.
Expand All @@ -1705,7 +1708,7 @@ function _unzip_file_ziparchive( $file, $to, $needed_dirs = array() ) {
if ( wp_doing_cron() ) {
$available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;

if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space ) {
if ( $available_space && ( $required_space > $available_space ) ) {
return new WP_Error(
'disk_full_unzip_file',
__( 'Could not copy files. You may have run out of disk space.' ),
Expand Down Expand Up @@ -1746,7 +1749,26 @@ function _unzip_file_ziparchive( $file, $to, $needed_dirs = array() ) {
return new WP_Error( 'mkdir_failed_ziparchive', __( 'Could not create directory.' ), $_dir );
}
}
unset( $needed_dirs );

/**
* Filters archive unzipping to override with a custom process.
*
* @since 6.4.0
*
* @param null|true|WP_Error $result The result of the override. True on success, otherwise WP Error. Default null.
* @param string $file Full path and filename of ZIP archive.
* @param string $to Full path on the filesystem to extract archive to.
* @param string[] $needed_dirs A full list of required folders that need to be created.
* @param float $required_space The space required to unzip the file and copy its contents, with a 10% buffer.
*/
$pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space );

if ( null !== $pre ) {
// Ensure the ZIP file archive has been closed.
$z->close();

return $pre;
}

for ( $i = 0; $i < $z->numFiles; $i++ ) {
$info = $z->statIndex( $i );
Expand Down Expand Up @@ -1781,7 +1803,22 @@ function _unzip_file_ziparchive( $file, $to, $needed_dirs = array() ) {

$z->close();

return true;
/**
* Filters the result of unzipping an archive.
*
* @since 6.4.0
*
* @param true|WP_Error $result The result of unzipping the archive. True on success, otherwise WP_Error. Default true.
* @param string $file Full path and filename of ZIP archive.
* @param string $to Full path on the filesystem the archive was extracted to.
* @param string[] $needed_dirs A full list of required folders that were created.
* @param float $required_space The space required to unzip the file and copy its contents, with a 10% buffer.
*/
$result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space );

unset( $needed_dirs );

return $result;
}

/**
Expand Down Expand Up @@ -1838,6 +1875,9 @@ function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) {
$needed_dirs[] = $to . untrailingslashit( $file['folder'] ? $file['filename'] : dirname( $file['filename'] ) );
}

// Enough space to unzip the file and copy its contents, with a 10% buffer.
$required_space = $uncompressed_size * 2.1;

/*
* disk_free_space() could return false. Assume that any falsey value is an error.
* A disk that has zero free bytes has bigger problems.
Expand All @@ -1846,7 +1886,7 @@ function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) {
if ( wp_doing_cron() ) {
$available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;

if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space ) {
if ( $available_space && ( $required_space > $available_space ) ) {
return new WP_Error(
'disk_full_unzip_file',
__( 'Could not copy files. You may have run out of disk space.' ),
Expand Down Expand Up @@ -1887,7 +1927,13 @@ function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) {
return new WP_Error( 'mkdir_failed_pclzip', __( 'Could not create directory.' ), $_dir );
}
}
unset( $needed_dirs );

/** This filter is documented in src/wp-admin/includes/file.php */
$pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space );

if ( null !== $pre ) {
return $pre;
}

// Extract the files from the zip.
foreach ( $archive_files as $file ) {
Expand All @@ -1909,7 +1955,12 @@ function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) {
}
}

return true;
/** This action is documented in src/wp-admin/includes/file.php */
$result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space );

unset( $needed_dirs );

return $result;
}

/**
Expand Down
76 changes: 76 additions & 0 deletions tests/phpunit/tests/filesystem/_unzipFilePclzip.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

/**
* Tests _unzip_file_pclzip().
*
* @group file.php
*
* @covers ::_unzip_file_pclzip
*/
class Tests_Filesystem_UnzipFilePclzip extends WP_UnitTestCase {

/**
* The test data directory.
*
* @var string $test_data_dir
*/
private static $test_data_dir;

/**
* Sets up the filesystem and test data directory property
* before any tests run.
*/
public static function set_up_before_class() {
parent::set_up_before_class();

require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();

self::$test_data_dir = DIR_TESTDATA . '/filesystem/';
}

/**
* Tests that _unzip_file_pclzip() applies "pre_unzip_file" filters.
*
* @ticket 37719
*/
public function test_should_apply_pre_unzip_file_filters() {
$filter = new MockAction();
add_filter( 'pre_unzip_file', array( $filter, 'filter' ) );

// Prepare test environment.
$unzip_destination = self::$test_data_dir . 'archive/';
mkdir( $unzip_destination );

_unzip_file_pclzip( self::$test_data_dir . 'archive.zip', $unzip_destination );

// Cleanup test environment.
$this->rmdir( $unzip_destination );
$this->delete_folders( $unzip_destination );

$this->assertSame( 1, $filter->get_call_count() );
}

/**
* Tests that _unzip_file_pclzip() applies "unzip_file" filters.
*
* @ticket 37719
*/
public function test_should_apply_unzip_file_filters() {
$filter = new MockAction();
add_filter( 'unzip_file', array( $filter, 'filter' ) );

// Prepare test environment.
$unzip_destination = self::$test_data_dir . 'archive/';
mkdir( $unzip_destination );

_unzip_file_pclzip( self::$test_data_dir . 'archive.zip', $unzip_destination );

// Cleanup test environment.
$this->rmdir( $unzip_destination );
$this->delete_folders( $unzip_destination );

$this->assertSame( 1, $filter->get_call_count() );
}

}
84 changes: 84 additions & 0 deletions tests/phpunit/tests/filesystem/_unzipFileZiparchive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/**
* Tests _unzip_file_ziparchive().
*
* @group file.php
*
* @covers ::_unzip_file_ziparchive
*/
class Tests_Filesystem_UnzipFileZiparchive extends WP_UnitTestCase {

/**
* The test data directory.
*
* @var string $test_data_dir
*/
private static $test_data_dir;

/**
* Sets up the filesystem and test data directory property
* before any tests run.
*/
public static function set_up_before_class() {
parent::set_up_before_class();

require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();

self::$test_data_dir = DIR_TESTDATA . '/filesystem/';
}

/**
* Tests that _unzip_file_ziparchive() applies "pre_unzip_file" filters.
*
* @ticket 37719
*/
public function test_should_apply_pre_unzip_file_filters() {
if ( ! class_exists( 'ZipArchive' ) ) {
$this->markTestSkipped( 'This test requires the ZipArchive class.' );
}

$filter = new MockAction();
add_filter( 'pre_unzip_file', array( $filter, 'filter' ) );

// Prepare test environment.
$unzip_destination = self::$test_data_dir . 'archive/';
mkdir( $unzip_destination );

_unzip_file_ziparchive( self::$test_data_dir . 'archive.zip', $unzip_destination );

// Cleanup test environment.
$this->rmdir( $unzip_destination );
$this->delete_folders( $unzip_destination );

$this->assertSame( 1, $filter->get_call_count() );
}

/**
* Tests that _unzip_file_ziparchive() applies "unzip_file" filters.
*
* @ticket 37719
*/
public function test_should_apply_unzip_file_filters() {
if ( ! class_exists( 'ZipArchive' ) ) {
$this->markTestSkipped( 'This test requires the ZipArchive class.' );
}

$filter = new MockAction();
add_filter( 'unzip_file', array( $filter, 'filter' ) );

// Prepare test environment.
$unzip_destination = self::$test_data_dir . 'archive/';
mkdir( $unzip_destination );

_unzip_file_ziparchive( self::$test_data_dir . 'archive.zip', $unzip_destination );

// Cleanup test environment.
$this->rmdir( $unzip_destination );
$this->delete_folders( $unzip_destination );

$this->assertSame( 1, $filter->get_call_count() );
}

}

0 comments on commit ef7fa1c

Please sign in to comment.