* 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( $from ) );
$from = trailingslashit( $from );
$to = trailingslashit( $to );
if ( ! $wp_filesystem->exists( $to ) && ! $wp_filesystem->mkdir( $to ) ) {
'mkdir_destination_failed_copy_dir',
__( 'Could not create the destination directory.' ),
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 ( str_starts_with( $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 ) ) {
* Moves a directory from one location to another.
* Recursively invalidates OPcache on success.
* If the renaming failed, falls back to copy_dir().
* Assumes that WP_Filesystem() has already been called and setup.
* This function is not designed to merge directories, copy_dir() should be used instead.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @param string $from Source directory.
* @param string $to Destination directory.
* @param bool $overwrite Optional. Whether to overwrite the destination directory if it exists.
* @return true|WP_Error True on success, WP_Error on failure.
function move_dir( $from, $to, $overwrite = false ) {
if ( trailingslashit( strtolower( $from ) ) === trailingslashit( strtolower( $to ) ) ) {
return new WP_Error( 'source_destination_same_move_dir', __( 'The source and destination are the same.' ) );
if ( $wp_filesystem->exists( $to ) ) {
return new WP_Error( 'destination_already_exists_move_dir', __( 'The destination folder already exists.' ), $to );
} elseif ( ! $wp_filesystem->delete( $to, true ) ) {
// Can't overwrite if the destination couldn't be deleted.
return new WP_Error( 'destination_not_deleted_move_dir', __( 'The destination directory already exists and could not be removed.' ) );
if ( $wp_filesystem->move( $from, $to ) ) {
* When using an environment with shared folders,
* there is a delay in updating the filesystem's cache.
* This is a known issue in environments with a VirtualBox provider.
* A 200ms delay gives time for the filesystem to update its cache,
* prevents "Operation not permitted", and "No such file or directory" warnings.
* This delay is used in other projects, including Composer.
* @link https://github.com/composer/composer/blob/2.5.1/src/Composer/Util/Platform.php#L228-L233
wp_opcache_invalidate_directory( $to );
// Fall back to a recursive copy.
if ( ! $wp_filesystem->is_dir( $to ) ) {
if ( ! $wp_filesystem->mkdir( $to, FS_CHMOD_DIR ) ) {
return new WP_Error( 'mkdir_failed_move_dir', __( 'Could not create directory.' ), $to );
$result = copy_dir( $from, $to, array( basename( $to ) ) );
// Clear the source directory.
if ( true === $result ) {
$wp_filesystem->delete( $from, true );
* 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 ); // 30 seconds.
if ( ! defined( 'FS_TIMEOUT' ) ) {
define( 'FS_TIMEOUT', 30 ); // 30 seconds.
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://developer.wordpress.org/advanced-administration/wordpress/wp-config/#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.
* Filters the filesystem method to use.
* @param string $method Filesystem method to return.
* @param array $args An array of connection details for the method.
* @param string $context Full path to the directory that is tested for being writable.
* @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable.
return apply_filters( 'filesystem_method', $method, $args, $context, $allow_relaxed_file_ownership );
* Displays a form to the user to request for their FTP/SSH details in order
* to connect to the filesystem.
* All chosen/entered details are saved, excluding the password.
* Hostnames may be in the form of hostname:portnumber (eg: wordpress.org:2467)
* to specify an alternate FTP/SSH port.
* Plugins may override this form by returning true|false via the {@see 'request_filesystem_credentials'} filter.
* @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
* @global string $pagenow The filename of the current screen.
* @param string $form_post The URL to post the form to.
* @param string $type Optional. Chosen type of filesystem. Default empty.
* @param bool|WP_Error $error Optional. Whether the current request has failed
* to connect, or an error object. Default false.
* @param string $context Optional. Full path to the directory that is tested
* for being writable. Default empty.
* @param array $extra_fields Optional. Extra `POST` fields to be checked
* for inclusion in the post. Default null.
* @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
* @return bool|array True if no filesystem credentials are required,
* false if they are required but have not been provided,
* array of credentials if they are required and have been provided.
function request_filesystem_credentials( $form_post, $type = '', $error = false, $context = '', $extra_fields = null, $allow_relaxed_file_ownership = false ) {
* Filters the filesystem credentials.
* Returning anything other than an empty string will effectively short-circuit
* output of the filesystem credentials form, returning that value instead.
* A filter should return true if no filesystem credentials are required, false if they are required but have not been
* provided, or an array of credentials if they are required and have been provided.
* @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
* @param mixed $credentials Credentials to return instead. Default empty string.
* @param string $form_post The URL to post the form to.
* @param string $type Chosen type of filesystem.
* @param bool|WP_Error $error Whether the current request has failed to connect,
* @param string $context Full path to the directory that is tested for
* @param array $extra_fields Extra POST fields.
* @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable.
$req_cred = apply_filters( 'request_filesystem_credentials', '', $form_post, $type, $error, $context, $extra_fields, $allow_relaxed_file_ownership );
if ( '' !== $req_cred ) {
$type = get_filesystem_method( array(), $context, $allow_relaxed_file_ownership );
if ( 'direct' === $type ) {
if ( is_null( $extra_fields ) ) {
$extra_fields = array( 'version', 'locale' );
$credentials = get_option(
$submitted_form = wp_unslash( $_POST );
// Verify nonce, or unset submitted form field values on failure.
if ( ! isset( $_POST['_fs_nonce'] ) || ! wp_verify_nonce( $_POST['_fs_nonce'], 'filesystem-credentials' ) ) {
$submitted_form['hostname'],
$submitted_form['username'],
$submitted_form['password'],
$submitted_form['public_key'],
$submitted_form['private_key'],
$submitted_form['connection_type']
'hostname' => 'FTP_HOST',
'username' => 'FTP_USER',
'password' => 'FTP_PASS',
'public_key' => 'FTP_PUBKEY',
'private_key' => 'FTP_PRIKEY',
* If defined, set it to that. Else, if POST'd, set it to that. If not, set it to an empty string.
* Otherwise, keep it as it previously was (saved details in option).
foreach ( $ftp_constants as $key => $constant ) {
if ( defined( $constant ) ) {
$credentials[ $key ] = constant( $constant );
} elseif ( ! empty( $submitted_form[ $key ] ) ) {
$credentials[ $key ] = $submitted_form[ $key ];
} elseif ( ! isset( $credentials[ $key ] ) ) {
$credentials[ $key ] = '';
// Sanitize the hostname, some people might pass in odd data.
$credentials['hostname'] = preg_replace( '|\w+://|', '', $credentials['hostname'] ); // Strip any schemes off.
if ( strpos( $credentials['hostname'], ':' ) ) {
list( $credentials['hostname'], $credentials['port'] ) = explode( ':', $credentials['hostname'], 2 );
if ( ! is_numeric( $credentials['port'] ) ) {
unset( $credentials['port'] );
unset( $credentials['port'] );
if ( ( defined( 'FTP_SSH' ) && FTP_SSH ) || ( defined( 'FS_METHOD' ) && 'ssh2' === FS_METHOD ) ) {
$credentials['connection_type'] = 'ssh';
} elseif ( ( defined( 'FTP_SSL' ) && FTP_SSL ) && 'ftpext' === $type ) { // Only the FTP Extension understands SSL.
$credentials['connection_type'] = 'ftps';
} elseif ( ! empty( $submitted_form['connection_type'] ) ) {
$credentials['connection_type'] = $submitted_form['connection_type'];
} elseif ( ! isset( $credentials['connection_type'] ) ) { // All else fails (and it's not defaulted to something else saved), default to FTP.
$credentials['connection_type'] = 'ftp';
&& ( ! empty( $credentials['hostname'] ) && ! empty( $credentials['username'] ) && ! empty( $credentials['password'] )
|| 'ssh' === $credentials['connection_type'] && ! empty( $credentials['public_key'] ) && ! empty( $credentials['private_key'] )
$stored_credentials = $credentials;
if ( ! empty( $stored_credentials['port'] ) ) { // Save port as part of hostname to simplify above code.
$stored_credentials['hostname'] .= ':' . $stored_credentials['port'];
$stored_credentials['password'],
$stored_credentials['port'],
$stored_credentials['private_key'],
$stored_credentials['public_key']
if ( ! wp_installing() ) {
update_option( 'ftp_credentials', $stored_credentials, false );
$hostname = isset( $credentials['hostname'] ) ? $credentials['hostname'] : '';
$username = isset( $credentials['username'] ) ? $credentials['username'] : '';