if ( sodium_crypto_sign_verify_detached( $signature_raw, $file_hash, $key_raw ) ) {
reset_mbstring_encoding();
reset_mbstring_encoding();
'signature_verification_failed',
/* translators: %s: The filename of the package. */
__( 'The authenticity of %s could not be verified.' ),
'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
// Error data helpful for debugging:
'filename' => $filename_for_errors,
'signatures' => $signatures,
'hash' => bin2hex( $file_hash ),
'skipped_key' => $skipped_key,
'skipped_sig' => $skipped_signature,
'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ),
* Retrieves the list of signing keys trusted by WordPress.
* @return string[] Array of base64-encoded signing keys.
function wp_trusted_keys() {
if ( time() < 1617235200 ) {
// WordPress.org Key #1 - This key is only valid before April 1st, 2021.
$trusted_keys[] = 'fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0=';
// TODO: Add key #2 with longer expiration.
* Filters the valid signing keys used to verify the contents of files.
* @param string[] $trusted_keys The trusted keys that may sign packages.
return apply_filters( 'wp_trusted_keys', $trusted_keys );
* Determines whether the given file is a valid ZIP file.
* This function does not test to ensure that a file exists. Non-existent files
* are not valid ZIPs, so those will also return false.
* @param string $file Full path to the ZIP file.
* @return bool Whether the file is a valid ZIP file.
function wp_zip_file_is_valid( $file ) {
/** This filter is documented in wp-admin/includes/file.php */
if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
$archive = new ZipArchive();
$archive_is_valid = $archive->open( $file, ZipArchive::CHECKCONS );
if ( true === $archive_is_valid ) {
// Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file.
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
$archive = new PclZip( $file );
$archive_is_valid = is_array( $archive->properties() );
return $archive_is_valid;
* Unzips a specified ZIP file to a location on the filesystem via the WordPress
* Filesystem Abstraction.
* Assumes that WP_Filesystem() has already been called and set up. Does not extract
* a root-level __MACOSX directory, if present.
* Attempts to increase the PHP memory limit to 256M before uncompressing. However,
* the most memory required shouldn't be much larger than the archive itself.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @param string $file Full path and filename of ZIP archive.
* @param string $to Full path on the filesystem to extract archive to.
* @return true|WP_Error True on success, WP_Error on failure.
function unzip_file( $file, $to ) {
if ( ! $wp_filesystem || ! is_object( $wp_filesystem ) ) {
return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
// Unzip can use a lot of memory, but not this much hopefully.
wp_raise_memory_limit( 'admin' );
$to = trailingslashit( $to );
// Determine any parent directories needed (of the upgrade directory).
if ( ! $wp_filesystem->is_dir( $to ) ) { // Only do parents if no children exist.
$path = preg_split( '![/\\\]!', untrailingslashit( $to ) );
for ( $i = count( $path ); $i >= 0; $i-- ) {
if ( empty( $path[ $i ] ) ) {
$dir = implode( '/', array_slice( $path, 0, $i + 1 ) );
if ( preg_match( '!^[a-z]:$!i', $dir ) ) { // Skip it if it looks like a Windows Drive letter.
if ( ! $wp_filesystem->is_dir( $dir ) ) {
break; // A folder exists, therefore we don't need to check the levels below this.
* Filters whether to use ZipArchive to unzip archives.
* @param bool $ziparchive Whether to use ZipArchive. Default true.
if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
$result = _unzip_file_ziparchive( $file, $to, $needed_dirs );
if ( true === $result ) {
} elseif ( is_wp_error( $result ) ) {
if ( 'incompatible_archive' !== $result->get_error_code() ) {
// Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file.
return _unzip_file_pclzip( $file, $to, $needed_dirs );
* Attempts to unzip an archive using the ZipArchive class.
* This function should not be called directly, use `unzip_file()` instead.
* Assumes that WP_Filesystem() has already been called and set up.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @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 partial list of required folders needed to be created.
* @return true|WP_Error True on success, WP_Error on failure.
function _unzip_file_ziparchive( $file, $to, $needed_dirs = array() ) {
$zopen = $z->open( $file, ZIPARCHIVE::CHECKCONS );
return new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.' ), array( 'ziparchive_error' => $zopen ) );
for ( $i = 0; $i < $z->numFiles; $i++ ) {
$info = $z->statIndex( $i );
return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) );
if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory.
// Don't extract invalid files:
if ( 0 !== validate_file( $info['name'] ) ) {
$uncompressed_size += $info['size'];
$dirname = dirname( $info['name'] );
if ( str_ends_with( $info['name'], '/' ) ) {
$needed_dirs[] = $to . untrailingslashit( $info['name'] );
} elseif ( '.' !== $dirname ) {
$needed_dirs[] = $to . untrailingslashit( $dirname );
// 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.
* Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
$available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;
if ( $available_space && ( $required_space > $available_space ) ) {
__( 'Could not copy files. You may have run out of disk space.' ),
compact( 'uncompressed_size', 'available_space' )
$needed_dirs = array_unique( $needed_dirs );
foreach ( $needed_dirs as $dir ) {
// Check the parent folders of the folders all exist within the creation array.
if ( untrailingslashit( $to ) === $dir ) { // Skip over the working directory, we know this exists (or will exist).
if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it.
$parent_folder = dirname( $dir );
while ( ! empty( $parent_folder )
&& untrailingslashit( $to ) !== $parent_folder
&& ! in_array( $parent_folder, $needed_dirs, true )
$needed_dirs[] = $parent_folder;
$parent_folder = dirname( $parent_folder );
// Create those directories if need be:
foreach ( $needed_dirs as $_dir ) {
// Only check to see if the Dir exists upon creation failure. Less I/O this way.
if ( ! $wp_filesystem->mkdir( $_dir, FS_CHMOD_DIR ) && ! $wp_filesystem->is_dir( $_dir ) ) {
return new WP_Error( 'mkdir_failed_ziparchive', __( 'Could not create directory.' ), $_dir );
* Filters archive unzipping to override with a custom process.
* @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 );
// Ensure the ZIP file archive has been closed.
for ( $i = 0; $i < $z->numFiles; $i++ ) {
$info = $z->statIndex( $i );
return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) );
if ( str_ends_with( $info['name'], '/' ) ) { // Directory.
if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files.
// Don't extract invalid files:
if ( 0 !== validate_file( $info['name'] ) ) {
$contents = $z->getFromIndex( $i );
if ( false === $contents ) {
return new WP_Error( 'extract_failed_ziparchive', __( 'Could not extract file from archive.' ), $info['name'] );
if ( ! $wp_filesystem->put_contents( $to . $info['name'], $contents, FS_CHMOD_FILE ) ) {
return new WP_Error( 'copy_failed_ziparchive', __( 'Could not copy file.' ), $info['name'] );
* Filters the result of unzipping an archive.
* @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 );
* Attempts to unzip an archive using the PclZip library.
* This function should not be called directly, use `unzip_file()` instead.
* Assumes that WP_Filesystem() has already been called and set up.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @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 partial list of required folders needed to be created.
* @return true|WP_Error True on success, WP_Error on failure.
function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) {
mbstring_binary_safe_encoding();
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
$archive = new PclZip( $file );
$archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING );
reset_mbstring_encoding();
if ( ! is_array( $archive_files ) ) {
return new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.' ), $archive->errorInfo( true ) );
if ( 0 === count( $archive_files ) ) {
return new WP_Error( 'empty_archive_pclzip', __( 'Empty archive.' ) );
// Determine any children directories needed (From within the archive).
foreach ( $archive_files as $file ) {
if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory.
$uncompressed_size += $file['size'];
$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.
* Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
$available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;
if ( $available_space && ( $required_space > $available_space ) ) {
__( 'Could not copy files. You may have run out of disk space.' ),
compact( 'uncompressed_size', 'available_space' )
$needed_dirs = array_unique( $needed_dirs );
foreach ( $needed_dirs as $dir ) {
// Check the parent folders of the folders all exist within the creation array.
if ( untrailingslashit( $to ) === $dir ) { // Skip over the working directory, we know this exists (or will exist).
if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it.
$parent_folder = dirname( $dir );
while ( ! empty( $parent_folder )
&& untrailingslashit( $to ) !== $parent_folder
&& ! in_array( $parent_folder, $needed_dirs, true )
$needed_dirs[] = $parent_folder;
$parent_folder = dirname( $parent_folder );
// Create those directories if need be:
foreach ( $needed_dirs as $_dir ) {
// Only check to see if the dir exists upon creation failure. Less I/O this way.
if ( ! $wp_filesystem->mkdir( $_dir, FS_CHMOD_DIR ) && ! $wp_filesystem->is_dir( $_dir ) ) {
return new WP_Error( 'mkdir_failed_pclzip', __( 'Could not create directory.' ), $_dir );
/** This filter is documented in src/wp-admin/includes/file.php */
$pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space );
// Extract the files from the zip.
foreach ( $archive_files as $file ) {
if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files.
// Don't extract invalid files:
if ( 0 !== validate_file( $file['filename'] ) ) {
if ( ! $wp_filesystem->put_contents( $to . $file['filename'], $file['content'], FS_CHMOD_FILE ) ) {
return new WP_Error( 'copy_failed_pclzip', __( 'Could not copy file.' ), $file['filename'] );
/** This action is documented in src/wp-admin/includes/file.php */
$result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space );
* Copies a directory from one location to another via the WordPress Filesystem