} 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 ( '__MACOSX/' === substr( $info['name'], 0, 9 ) ) { // 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 ( '/' === substr( $info['name'], -1 ) ) {
$needed_dirs[] = $to . untrailingslashit( $info['name'] );
} elseif ( '.' !== $dirname ) {
$needed_dirs[] = $to . untrailingslashit( $dirname );
* 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 = @disk_free_space( WP_CONTENT_DIR );
if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space ) {
return new WP_Error( 'disk_full_unzip_file', __( '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 ( strpos( $dir, $to ) === false ) { // 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.' ), substr( $_dir, strlen( $to ) ) );
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 ( '/' === substr( $info['name'], -1 ) ) { // Directory.
if ( '__MACOSX/' === substr( $info['name'], 0, 9 ) ) { // 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'] );
* 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 ( '__MACOSX/' === substr( $file['filename'], 0, 9 ) ) { // Skip the OS X-created __MACOSX directory.
$uncompressed_size += $file['size'];
$needed_dirs[] = $to . untrailingslashit( $file['folder'] ? $file['filename'] : dirname( $file['filename'] ) );
* 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 = @disk_free_space( WP_CONTENT_DIR );
if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space ) {
return new WP_Error( 'disk_full_unzip_file', __( '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 ( strpos( $dir, $to ) === false ) { // 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.' ), substr( $_dir, strlen( $to ) ) );
// Extract the files from the zip.
foreach ( $archive_files as $file ) {
if ( '__MACOSX/' === substr( $file['filename'], 0, 9 ) ) { // 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'] );
* Copies a directory from one location to another via the WordPress Filesystem
* Assumes that WP_Filesystem() has already been called and setup.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @param string $from Source directory.
* @param string $to Destination directory.
* @param string[] $skip_list An array of files/folders to skip copying.
* @return true|WP_Error True on success, WP_Error on failure.
function copy_dir( $from, $to, $skip_list = array() ) {
$dirlist = $wp_filesystem->dirlist( $from );
if ( false === $dirlist ) {
return new WP_Error( 'dirlist_failed_copy_dir', __( 'Directory listing failed.' ), basename( $to ) );
$from = trailingslashit( $from );
$to = trailingslashit( $to );
foreach ( (array) $dirlist as $filename => $fileinfo ) {
if ( in_array( $filename, $skip_list, true ) ) {
if ( 'f' === $fileinfo['type'] ) {
if ( ! $wp_filesystem->copy( $from . $filename, $to . $filename, true, FS_CHMOD_FILE ) ) {
// If copy failed, chmod file to 0644 and try again.
$wp_filesystem->chmod( $to . $filename, FS_CHMOD_FILE );
if ( ! $wp_filesystem->copy( $from . $filename, $to . $filename, true, FS_CHMOD_FILE ) ) {
return new WP_Error( 'copy_failed_copy_dir', __( 'Could not copy file.' ), $to . $filename );
wp_opcache_invalidate( $to . $filename );
} elseif ( 'd' === $fileinfo['type'] ) {
if ( ! $wp_filesystem->is_dir( $to . $filename ) ) {
if ( ! $wp_filesystem->mkdir( $to . $filename, FS_CHMOD_DIR ) ) {
return new WP_Error( 'mkdir_failed_copy_dir', __( 'Could not create directory.' ), $to . $filename );
// Generate the $sub_skip_list for the subdirectory as a sub-set of the existing $skip_list.
$sub_skip_list = array();
foreach ( $skip_list as $skip_item ) {
if ( 0 === strpos( $skip_item, $filename . '/' ) ) {
$sub_skip_list[] = preg_replace( '!^' . preg_quote( $filename, '!' ) . '/!i', '', $skip_item );
$result = copy_dir( $from . $filename, $to . $filename, $sub_skip_list );
if ( is_wp_error( $result ) ) {
* Initializes and connects the WordPress Filesystem Abstraction classes.
* This function will include the chosen transport and attempt connecting.
* Plugins may add extra transports, And force WordPress to use them by returning
* the filename via the {@see 'filesystem_method_file'} filter.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @param array|false $args Optional. Connection args, These are passed
* directly to the `WP_Filesystem_*()` classes.
* @param string|false $context Optional. Context for get_filesystem_method().
* @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
* @return bool|null True on success, false on failure,
* null if the filesystem method class file does not exist.
function WP_Filesystem( $args = false, $context = false, $allow_relaxed_file_ownership = false ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
$method = get_filesystem_method( $args, $context, $allow_relaxed_file_ownership );
if ( ! class_exists( "WP_Filesystem_$method" ) ) {
* Filters the path for a specific filesystem method class file.
* @see get_filesystem_method()
* @param string $path Path to the specific filesystem method class file.
* @param string $method The filesystem method to use.
$abstraction_file = apply_filters( 'filesystem_method_file', ABSPATH . 'wp-admin/includes/class-wp-filesystem-' . $method . '.php', $method );
if ( ! file_exists( $abstraction_file ) ) {
require_once $abstraction_file;
$method = "WP_Filesystem_$method";
$wp_filesystem = new $method( $args );
* Define the timeouts for the connections. Only available after the constructor is called
* to allow for per-transport overriding of the default.
if ( ! defined( 'FS_CONNECT_TIMEOUT' ) ) {
define( 'FS_CONNECT_TIMEOUT', 30 );
if ( ! defined( 'FS_TIMEOUT' ) ) {
define( 'FS_TIMEOUT', 30 );
if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
if ( ! $wp_filesystem->connect() ) {
return false; // There was an error connecting to the server.
// Set the permission constants if not already set.
if ( ! defined( 'FS_CHMOD_DIR' ) ) {
define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
if ( ! defined( 'FS_CHMOD_FILE' ) ) {
define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
* Determines which method to use for reading, writing, modifying, or deleting
* files on the filesystem.
* The priority of the transports are: Direct, SSH2, FTP PHP Extension, FTP Sockets
* (Via Sockets class, or `fsockopen()`). Valid values for these are: 'direct', 'ssh2',
* 'ftpext' or 'ftpsockets'.
* The return value can be overridden by defining the `FS_METHOD` constant in `wp-config.php`,
* or filtering via {@see 'filesystem_method'}.
* @link https://wordpress.org/support/article/editing-wp-config-php/#wordpress-upgrade-constants
* Plugins may define a custom transport handler, See WP_Filesystem().
* @global callable $_wp_filesystem_direct_method
* @param array $args Optional. Connection details. Default empty array.
* @param string $context Optional. Full path to the directory that is tested
* for being writable. Default empty.
* @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
* @return string The transport to use, see description for valid return values.
function get_filesystem_method( $args = array(), $context = '', $allow_relaxed_file_ownership = false ) {
// Please ensure that this is either 'direct', 'ssh2', 'ftpext', or 'ftpsockets'.
$method = defined( 'FS_METHOD' ) ? FS_METHOD : false;
$context = WP_CONTENT_DIR;
// If the directory doesn't exist (wp-content/languages) then use the parent directory as we'll create it.
if ( WP_LANG_DIR == $context && ! is_dir( $context ) ) {
$context = dirname( $context );
$context = trailingslashit( $context );
$temp_file_name = $context . 'temp-write-test-' . str_replace( '.', '-', uniqid( '', true ) );
$temp_handle = @fopen( $temp_file_name, 'w' );
// Attempt to determine the file owner of the WordPress files, and that of newly created files.
$temp_file_owner = false;
if ( function_exists( 'fileowner' ) ) {
$wp_file_owner = @fileowner( __FILE__ );
$temp_file_owner = @fileowner( $temp_file_name );
if ( false !== $wp_file_owner && $wp_file_owner === $temp_file_owner ) {
* WordPress is creating files as the same owner as the WordPress files,
* this means it's safe to modify & create new files via PHP.
$GLOBALS['_wp_filesystem_direct_method'] = 'file_owner';
} elseif ( $allow_relaxed_file_ownership ) {
* The $context directory is writable, and $allow_relaxed_file_ownership is set,
* this means we can modify files safely in this directory.
* This mode doesn't create new files, only alter existing ones.
$GLOBALS['_wp_filesystem_direct_method'] = 'relaxed_ownership';
@unlink( $temp_file_name );
if ( ! $method && isset( $args['connection_type'] ) && 'ssh' === $args['connection_type'] && extension_loaded( 'ssh2' ) ) {
if ( ! $method && extension_loaded( 'ftp' ) ) {
if ( ! $method && ( extension_loaded( 'sockets' ) || function_exists( 'fsockopen' ) ) ) {
$method = 'ftpsockets'; // Sockets: Socket extension; PHP Mode: FSockopen / fwrite / fread.