From 47172a2c65792a7e050688134d2c6b9967fe048f Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Fri, 3 Feb 2023 01:50:16 +0000 Subject: [PATCH] Filesystem API: Add directory support to `WP_Filesystem_Direct::move()`. Introduces: - New function: `wp_opcache_invalidate_directory()`, to recursively call `wp_opcache_invalidate()` after overwriting .php files. - New function: `move_dir()`, similar to `copy_dir()` that uses `WP_Filesystem::move()` followed by `wp_opcache_invalidate_directory()`, and has a fallback to `copy_dir()`. Props: costdev, afragen, peterwilsoncc, sergeybiryukov, ironprogrammer, flixos90, bronsonquick, mukesh27, azaozz. Fixes #57375. Built from https://develop.svn.wordpress.org/trunk@55204 git-svn-id: http://core.svn.wordpress.org/trunk@54737 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- .../includes/class-wp-filesystem-direct.php | 17 ++- wp-admin/includes/file.php | 131 ++++++++++++++++++ wp-includes/version.php | 2 +- 3 files changed, 147 insertions(+), 3 deletions(-) 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.