diff --git a/wp-admin/includes/class-wp-filesystem-direct.php b/wp-admin/includes/class-wp-filesystem-direct.php index e84dfed39f..e8be5506d2 100644 --- a/wp-admin/includes/class-wp-filesystem-direct.php +++ b/wp-admin/includes/class-wp-filesystem-direct.php @@ -316,7 +316,14 @@ class WP_Filesystem_Direct extends WP_Filesystem_Base { } /** - * Moves a file. + * Moves a file or directory. + * + * After moving files or directories, OPcache will need to be invalidated. + * + * If moving a directory fails, `copy_dir()` can be used for a recursive copy. + * + * Use `move_dir()` for moving directories with OPcache invalidation and a + * fallback to `copy_dir()`. * * @since 2.5.0 * @@ -331,12 +338,18 @@ class WP_Filesystem_Direct extends WP_Filesystem_Base { return false; } + if ( $overwrite && $this->exists( $destination ) && ! $this->delete( $destination, true ) ) { + // Can't overwrite if the destination couldn't be deleted. + return false; + } + // Try using rename first. if that fails (for example, source is read only) try copy. if ( @rename( $source, $destination ) ) { return true; } - if ( $this->copy( $source, $destination, $overwrite ) && $this->exists( $destination ) ) { + // Backward compatibility: Only fall back to `::copy()` for single files. + if ( $this->is_file( $source ) && $this->copy( $source, $destination, $overwrite ) && $this->exists( $destination ) ) { $this->delete( $source ); return true; diff --git a/wp-admin/includes/file.php b/wp-admin/includes/file.php index f609277826..62e9c0cb91 100644 --- a/wp-admin/includes/file.php +++ b/wp-admin/includes/file.php @@ -1946,6 +1946,75 @@ function copy_dir( $from, $to, $skip_list = array() ) { return true; } +/** + * 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. + * + * @since 6.2.0 + * + * @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. + * Default false. + * @return true|WP_Error True on success, WP_Error on failure. + */ +function move_dir( $from, $to, $overwrite = false ) { + global $wp_filesystem; + + if ( trailingslashit( strtolower( $from ) ) === trailingslashit( strtolower( $to ) ) ) { + return new WP_Error( + 'source_destination_same_move_dir', + __( 'The source and destination are the same.' ) + ); + } + + if ( ! $overwrite && $wp_filesystem->exists( $to ) ) { + return new WP_Error( 'destination_already_exists_move_dir', __( 'The destination folder already exists.' ), $to ); + } + + if ( $wp_filesystem->move( $from, $to, $overwrite ) ) { + /* + * 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 + */ + usleep( 200000 ); + wp_opcache_invalidate_directory( $to ); + + return true; + } + + // 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 ); + } + + return $result; +} + /** * Initializes and connects the WordPress Filesystem Abstraction classes. * @@ -2557,3 +2626,65 @@ function wp_opcache_invalidate( $filepath, $force = false ) { return false; } + +/** + * Attempts to clear the opcode cache for a directory of files. + * + * @since 6.2.0 + * + * @see wp_opcache_invalidate() + * @link https://www.php.net/manual/en/function.opcache-invalidate.php + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @param string $dir The path to the directory for which the opcode cache is to be cleared. + */ +function wp_opcache_invalidate_directory( $dir ) { + global $wp_filesystem; + + if ( ! is_string( $dir ) || '' === trim( $dir ) ) { + if ( WP_DEBUG ) { + $error_message = sprintf( + /* translators: %s: The function name. */ + __( '%s expects a non-empty string.' ), + 'wp_opcache_invalidate_directory()' + ); + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + trigger_error( $error_message ); + } + return; + } + + $dirlist = $wp_filesystem->dirlist( $dir, false, true ); + + if ( empty( $dirlist ) ) { + return; + } + + /* + * Recursively invalidate opcache of files in a directory. + * + * WP_Filesystem_*::dirlist() returns an array of file and directory information. + * + * This does not include a path to the file or directory. + * To invalidate files within sub-directories, recursion is needed + * to prepend an absolute path containing the sub-directory's name. + * + * @param array $dirlist Array of file/directory information from WP_Filesystem_Base::dirlist(), + * with sub-directories represented as nested arrays. + * @param string $path Absolute path to the directory. + */ + $invalidate_directory = function( $dirlist, $path ) use ( &$invalidate_directory ) { + $path = trailingslashit( $path ); + + foreach ( $dirlist as $name => $details ) { + if ( 'f' === $details['type'] ) { + wp_opcache_invalidate( $path . $name, true ); + } elseif ( is_array( $details['files'] ) && ! empty( $details['files'] ) ) { + $invalidate_directory( $details['files'], $path . $name ); + } + } + }; + + $invalidate_directory( $dirlist, $dir ); +} diff --git a/wp-includes/version.php b/wp-includes/version.php index 87997f5cfc..c63aa31fae 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.2-alpha-55203'; +$wp_version = '6.2-alpha-55204'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.