diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-admin/includes/file.php --- a/wp/wp-admin/includes/file.php Thu Sep 29 08:06:27 2022 +0200 +++ b/wp/wp-admin/includes/file.php Fri Sep 05 18:40:08 2025 +0200 @@ -21,6 +21,7 @@ 'searchform.php' => __( 'Search Form' ), '404.php' => __( '404 Template' ), 'link.php' => __( 'Links Template' ), + 'theme.json' => __( 'Theme Styles & Block Settings' ), // Archives. 'index.php' => __( 'Main Index Template' ), 'archive.php' => __( 'Archives' ), @@ -126,13 +127,16 @@ * * @since 2.6.0 * @since 4.9.0 Added the `$exclusions` parameter. + * @since 6.3.0 Added the `$include_hidden` parameter. * - * @param string $folder Optional. Full path to folder. Default empty. - * @param int $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit). - * @param string[] $exclusions Optional. List of folders and files to skip. + * @param string $folder Optional. Full path to folder. Default empty. + * @param int $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit). + * @param string[] $exclusions Optional. List of folders and files to skip. + * @param bool $include_hidden Optional. Whether to include details of hidden ("." prefixed) files. + * Default false. * @return string[]|false Array of files on success, false on failure. */ -function list_files( $folder = '', $levels = 100, $exclusions = array() ) { +function list_files( $folder = '', $levels = 100, $exclusions = array(), $include_hidden = false ) { if ( empty( $folder ) ) { return false; } @@ -155,12 +159,12 @@ } // Skip hidden and excluded files. - if ( '.' === $file[0] || in_array( $file, $exclusions, true ) ) { + if ( ( ! $include_hidden && '.' === $file[0] ) || in_array( $file, $exclusions, true ) ) { continue; } if ( is_dir( $folder . $file ) ) { - $files2 = list_files( $folder . $file, $levels - 1 ); + $files2 = list_files( $folder . $file, $levels - 1, array(), $include_hidden ); if ( $files2 ) { $files = array_merge( $files, $files2 ); } else { @@ -309,7 +313,7 @@ Changing File Permissions for more information.' ), - __( 'https://wordpress.org/support/article/changing-file-permissions/' ) + __( 'https://developer.wordpress.org/advanced-administration/server/file-permissions/' ) ); ?>

@@ -338,7 +342,12 @@ <# } #> <# } #> <# if ( data.dismissible ) { #> - + <# } #> @@ -539,10 +548,12 @@ } // Make sure PHP process doesn't die before loopback requests complete. - set_time_limit( 300 ); + if ( function_exists( 'set_time_limit' ) ) { + set_time_limit( 5 * MINUTE_IN_SECONDS ); + } // Time to wait for loopback requests to finish. - $timeout = 100; + $timeout = 100; // 100 seconds. $needle_start = "###### wp_scraping_result_start:$scrape_key ######"; $needle_end = "###### wp_scraping_result_end:$scrape_key ######"; @@ -563,8 +574,10 @@ } if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) { - // Close any active session to prevent HTTP requests from timing out - // when attempting to connect back to the site. + /* + * Close any active session to prevent HTTP requests from timing out + * when attempting to connect back to the site. + */ session_write_close(); } @@ -643,7 +656,7 @@ /** * Returns a filename of a temporary unique file. * - * Please note that the calling function must unlink() this itself. + * Please note that the calling function must delete or move the file. * * The filename is based off the passed parameter or defaults to the current unix timestamp, * while the directory can either be passed as well, or by leaving it blank, default to a writable @@ -676,7 +689,25 @@ // Suffix some random data to avoid filename conflicts. $temp_filename .= '-' . wp_generate_password( 6, false ); $temp_filename .= '.tmp'; - $temp_filename = $dir . wp_unique_filename( $dir, $temp_filename ); + $temp_filename = wp_unique_filename( $dir, $temp_filename ); + + /* + * Filesystems typically have a limit of 255 characters for a filename. + * + * If the generated unique filename exceeds this, truncate the initial + * filename and try again. + * + * As it's possible that the truncated filename may exist, producing a + * suffix of "-1" or "-10" which could exceed the limit again, truncate + * it to 252 instead. + */ + $characters_over_limit = strlen( $temp_filename ) - 252; + if ( $characters_over_limit > 0 ) { + $filename = substr( $filename, 0, -$characters_over_limit ); + return wp_tempnam( $filename, $dir ); + } + + $temp_filename = $dir . $temp_filename; $fp = @fopen( $temp_filename, 'x' ); @@ -746,9 +777,9 @@ * An array of override parameters for this file, or boolean false if none are provided. * * @type callable $upload_error_handler Function to call when there is an error during the upload process. - * @see wp_handle_upload_error(). + * See {@see wp_handle_upload_error()}. * @type callable $unique_filename_callback Function to call when determining a unique file name for the file. - * @see wp_unique_filename(). + * See {@see wp_unique_filename()}. * @type string[] $upload_error_strings The strings that describe the error indicated in * `$_FILES[{form field}]['error']`. * @type bool $test_form Whether to test that the `$_POST['action']` parameter is as expected. @@ -814,7 +845,7 @@ * @since 5.7.0 * * @param array|false $overrides An array of override parameters for this file. Boolean false if none are - * provided. @see _wp_handle_upload(). + * provided. See {@see _wp_handle_upload()}. * @param array $file { * Reference to a single element from `$_FILES`. * @@ -882,7 +913,7 @@ // If you override this, you must provide $ext and $type!! $test_type = isset( $overrides['test_type'] ) ? $overrides['test_type'] : true; - $mimes = isset( $overrides['mimes'] ) ? $overrides['mimes'] : false; + $mimes = isset( $overrides['mimes'] ) ? $overrides['mimes'] : null; // A correct form post will pass this test. if ( $test_form && ( ! isset( $_POST['action'] ) || $_POST['action'] !== $action ) ) { @@ -989,7 +1020,7 @@ } if ( false === $move_new_file ) { - if ( 0 === strpos( $uploads['basedir'], ABSPATH ) ) { + if ( str_starts_with( $uploads['basedir'], ABSPATH ) ) { $error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir']; } else { $error_path = basename( $uploads['basedir'] ) . $uploads['subdir']; @@ -1058,7 +1089,7 @@ * @param array|false $overrides Optional. An associative array of names => values * to override default variables. Default false. * See _wp_handle_upload() for accepted values. - * @param string $time Optional. Time formatted in 'yyyy/mm'. Default null. + * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null. * @return array See _wp_handle_upload() for return value. */ function wp_handle_upload( &$file, $overrides = false, $time = null ) { @@ -1089,7 +1120,7 @@ * @param array|false $overrides Optional. An associative array of names => values * to override default variables. Default false. * See _wp_handle_upload() for accepted values. - * @param string $time Optional. Time formatted in 'yyyy/mm'. Default null. + * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null. * @return array See _wp_handle_upload() for return value. */ function wp_handle_sideload( &$file, $overrides = false, $time = null ) { @@ -1108,7 +1139,7 @@ /** * Downloads a URL to a local temporary file using the WordPress HTTP API. * - * Please note that the calling function must unlink() the file. + * Please note that the calling function must delete or move the file. * * @since 2.5.0 * @since 5.2.0 Signature Verification with SoftFail was added. @@ -1122,9 +1153,9 @@ * @return string|WP_Error Filename on success, WP_Error on failure. */ function download_url( $url, $timeout = 300, $signature_verification = false ) { - // WARNING: The file is not automatically deleted, the script must unlink() the file. + // WARNING: The file is not automatically deleted, the script must delete or move the file. if ( ! $url ) { - return new WP_Error( 'http_no_url', __( 'Invalid URL Provided.' ) ); + return new WP_Error( 'http_no_url', __( 'No URL Provided.' ) ); } $url_path = parse_url( $url, PHP_URL_PATH ); @@ -1183,12 +1214,12 @@ return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ), $data ); } - $content_disposition = wp_remote_retrieve_header( $response, 'content-disposition' ); + $content_disposition = wp_remote_retrieve_header( $response, 'Content-Disposition' ); if ( $content_disposition ) { $content_disposition = strtolower( $content_disposition ); - if ( 0 === strpos( $content_disposition, 'attachment; filename=' ) ) { + if ( str_starts_with( $content_disposition, 'attachment; filename=' ) ) { $tmpfname_disposition = sanitize_file_name( substr( $content_disposition, 21 ) ); } else { $tmpfname_disposition = ''; @@ -1210,7 +1241,7 @@ } } - $content_md5 = wp_remote_retrieve_header( $response, 'content-md5' ); + $content_md5 = wp_remote_retrieve_header( $response, 'Content-MD5' ); if ( $content_md5 ) { $md5_check = verify_file_md5( $tmpfname, $content_md5 ); @@ -1235,17 +1266,19 @@ $signature_verification = in_array( parse_url( $url, PHP_URL_HOST ), $signed_hostnames, true ); } - // Perform signature valiation if supported. + // Perform signature validation if supported. if ( $signature_verification ) { - $signature = wp_remote_retrieve_header( $response, 'x-content-signature' ); + $signature = wp_remote_retrieve_header( $response, 'X-Content-Signature' ); if ( ! $signature ) { - // Retrieve signatures from a file if the header wasn't included. - // WordPress.org stores signatures at $package_url.sig. + /* + * Retrieve signatures from a file if the header wasn't included. + * WordPress.org stores signatures at $package_url.sig. + */ $signature_url = false; - if ( is_string( $url_path ) && ( '.zip' === substr( $url_path, -4 ) || '.tar.gz' === substr( $url_path, -7 ) ) ) { + if ( is_string( $url_path ) && ( str_ends_with( $url_path, '.zip' ) || str_ends_with( $url_path, '.tar.gz' ) ) ) { $signature_url = str_replace( $url_path, $url_path . '.sig', $url ); } @@ -1369,14 +1402,16 @@ ); } - // Check for a edge-case affecting PHP Maths abilities. + // Check for an edge-case affecting PHP Maths abilities. if ( ! extension_loaded( 'sodium' ) && in_array( PHP_VERSION_ID, array( 70200, 70201, 70202 ), true ) && extension_loaded( 'opcache' ) ) { - // Sodium_Compat isn't compatible with PHP 7.2.0~7.2.2 due to a bug in the PHP Opcache extension, bail early as it'll fail. - // https://bugs.php.net/bug.php?id=75938 + /* + * Sodium_Compat isn't compatible with PHP 7.2.0~7.2.2 due to a bug in the PHP Opcache extension, bail early as it'll fail. + * https://bugs.php.net/bug.php?id=75938 + */ return new WP_Error( 'signature_verification_unsupported', sprintf( @@ -1385,7 +1420,7 @@ '' . esc_html( $filename_for_errors ) . '' ), array( - 'php' => phpversion(), + 'php' => PHP_VERSION, 'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ), ) ); @@ -1409,8 +1444,10 @@ // phpcs:enable } - // This cannot be performed in a reasonable amount of time. - // https://github.com/paragonie/sodium_compat#help-sodium_compat-is-slow-how-can-i-make-it-fast + /* + * This cannot be performed in a reasonable amount of time. + * https://github.com/paragonie/sodium_compat#help-sodium_compat-is-slow-how-can-i-make-it-fast + */ if ( ! $sodium_compat_is_fast ) { return new WP_Error( 'signature_verification_unsupported', @@ -1420,7 +1457,7 @@ '' . esc_html( $filename_for_errors ) . '' ), array( - 'php' => phpversion(), + 'php' => PHP_VERSION, 'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ), 'polyfill_is_fast' => false, 'max_execution_time' => ini_get( 'max_execution_time' ), @@ -1456,7 +1493,7 @@ // Ensure only valid-length signatures are considered. if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $signature_raw ) ) { - $skipped_signature++; + ++$skipped_signature; continue; } @@ -1465,7 +1502,7 @@ // Only pass valid public keys through. if ( SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES !== strlen( $key_raw ) ) { - $skipped_key++; + ++$skipped_key; continue; } @@ -1493,7 +1530,7 @@ 'hash' => bin2hex( $file_hash ), 'skipped_key' => $skipped_key, 'skipped_sig' => $skipped_signature, - 'php' => phpversion(), + 'php' => PHP_VERSION, 'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ), ) ); @@ -1527,6 +1564,37 @@ } /** + * 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. + * + * @since 6.4.4 + * + * @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 ) { + $archive->close(); + return true; + } + } + + // 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. * @@ -1635,10 +1703,11 @@ $info = $z->statIndex( $i ); if ( ! $info ) { + $z->close(); 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. + if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory. continue; } @@ -1651,7 +1720,7 @@ $dirname = dirname( $info['name'] ); - if ( '/' === substr( $info['name'], -1 ) ) { + if ( str_ends_with( $info['name'], '/' ) ) { // Directory. $needed_dirs[] = $to . untrailingslashit( $info['name'] ); } elseif ( '.' !== $dirname ) { @@ -1660,6 +1729,9 @@ } } + // 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. @@ -1668,7 +1740,8 @@ if ( wp_doing_cron() ) { $available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false; - if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space ) { + if ( $available_space && ( $required_space > $available_space ) ) { + $z->close(); return new WP_Error( 'disk_full_unzip_file', __( 'Could not copy files. You may have run out of disk space.' ), @@ -1685,7 +1758,7 @@ continue; } - if ( strpos( $dir, $to ) === false ) { // If the directory is not within the working directory, skip it. + if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it. continue; } @@ -1706,23 +1779,44 @@ 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 ) ) ); + $z->close(); + return new WP_Error( 'mkdir_failed_ziparchive', __( 'Could not create directory.' ), $_dir ); } } - unset( $needed_dirs ); + + /** + * Filters archive unzipping to override with a custom process. + * + * @since 6.4.0 + * + * @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 ); + + if ( null !== $pre ) { + // Ensure the ZIP file archive has been closed. + $z->close(); + + return $pre; + } for ( $i = 0; $i < $z->numFiles; $i++ ) { $info = $z->statIndex( $i ); if ( ! $info ) { + $z->close(); return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) ); } - if ( '/' === substr( $info['name'], -1 ) ) { // Directory. + if ( str_ends_with( $info['name'], '/' ) ) { // Directory. continue; } - if ( '__MACOSX/' === substr( $info['name'], 0, 9 ) ) { // Don't extract the OS X-created __MACOSX directory files. + if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files. continue; } @@ -1734,17 +1828,34 @@ $contents = $z->getFromIndex( $i ); if ( false === $contents ) { + $z->close(); 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 ) ) { + $z->close(); return new WP_Error( 'copy_failed_ziparchive', __( 'Could not copy file.' ), $info['name'] ); } } $z->close(); - return true; + /** + * Filters the result of unzipping an archive. + * + * @since 6.4.0 + * + * @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 ); + + unset( $needed_dirs ); + + return $result; } /** @@ -1792,7 +1903,7 @@ // 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. + if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory. continue; } @@ -1801,6 +1912,9 @@ $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. @@ -1809,7 +1923,7 @@ if ( wp_doing_cron() ) { $available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false; - if ( $available_space && ( $uncompressed_size * 2.1 ) > $available_space ) { + if ( $available_space && ( $required_space > $available_space ) ) { return new WP_Error( 'disk_full_unzip_file', __( 'Could not copy files. You may have run out of disk space.' ), @@ -1826,7 +1940,7 @@ continue; } - if ( strpos( $dir, $to ) === false ) { // If the directory is not within the working directory, skip it. + if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it. continue; } @@ -1847,10 +1961,16 @@ 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 ) ) ); + return new WP_Error( 'mkdir_failed_pclzip', __( 'Could not create directory.' ), $_dir ); } } - unset( $needed_dirs ); + + /** This filter is documented in src/wp-admin/includes/file.php */ + $pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space ); + + if ( null !== $pre ) { + return $pre; + } // Extract the files from the zip. foreach ( $archive_files as $file ) { @@ -1858,7 +1978,7 @@ continue; } - if ( '__MACOSX/' === substr( $file['filename'], 0, 9 ) ) { // Don't extract the OS X-created __MACOSX directory files. + if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files. continue; } @@ -1872,7 +1992,12 @@ } } - return true; + /** This action is documented in src/wp-admin/includes/file.php */ + $result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space ); + + unset( $needed_dirs ); + + return $result; } /** @@ -1896,12 +2021,20 @@ $dirlist = $wp_filesystem->dirlist( $from ); if ( false === $dirlist ) { - return new WP_Error( 'dirlist_failed_copy_dir', __( 'Directory listing failed.' ), basename( $to ) ); + 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 ) ) { + return new WP_Error( + 'mkdir_destination_failed_copy_dir', + __( 'Could not create the destination directory.' ), + basename( $to ) + ); + } + foreach ( (array) $dirlist as $filename => $fileinfo ) { if ( in_array( $filename, $skip_list, true ) ) { continue; @@ -1929,7 +2062,7 @@ $sub_skip_list = array(); foreach ( $skip_list as $skip_item ) { - if ( 0 === strpos( $skip_item, $filename . '/' ) ) { + if ( str_starts_with( $skip_item, $filename . '/' ) ) { $sub_skip_list[] = preg_replace( '!^' . preg_quote( $filename, '!' ) . '/!i', '', $skip_item ); } } @@ -1946,6 +2079,79 @@ } /** + * 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. + * + * @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 ( $wp_filesystem->exists( $to ) ) { + if ( ! $overwrite ) { + 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 + */ + 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. * * This function will include the chosen transport and attempt connecting. @@ -2007,10 +2213,10 @@ * to allow for per-transport overriding of the default. */ if ( ! defined( 'FS_CONNECT_TIMEOUT' ) ) { - define( 'FS_CONNECT_TIMEOUT', 30 ); + define( 'FS_CONNECT_TIMEOUT', 30 ); // 30 seconds. } if ( ! defined( 'FS_TIMEOUT' ) ) { - define( 'FS_TIMEOUT', 30 ); + define( 'FS_TIMEOUT', 30 ); // 30 seconds. } if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { @@ -2043,7 +2249,7 @@ * 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 + * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#wordpress-upgrade-constants * * Plugins may define a custom transport handler, See WP_Filesystem(). * @@ -2235,8 +2441,10 @@ '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). + /* + * 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 ); @@ -2302,11 +2510,17 @@ $connection_type = isset( $credentials['connection_type'] ) ? $credentials['connection_type'] : ''; if ( $error ) { - $error_string = __( 'Error: Could not connect to the server. Please verify the settings are correct.' ); + $error_string = __( 'Error: Could not connect to the server. Please verify the settings are correct.' ); if ( is_wp_error( $error ) ) { $error_string = esc_html( $error->get_error_message() ); } - echo '

' . $error_string . '

'; + wp_admin_notice( + $error_string, + array( + 'id' => 'message', + 'additional_classes' => array( 'error' ), + ) + ); } $types = array(); @@ -2389,10 +2603,11 @@
@@ -2438,16 +2653,18 @@ } } - // Make sure the `submit_button()` function is available during the REST API call - // from WP_Site_Health_Auto_Updates::test_check_wp_filesystem_method(). + /* + * Make sure the `submit_button()` function is available during the REST API call + * from WP_Site_Health_Auto_Updates::test_check_wp_filesystem_method(). + */ if ( ! function_exists( 'submit_button' ) ) { - require_once ABSPATH . '/wp-admin/includes/template.php'; + require_once ABSPATH . 'wp-admin/includes/template.php'; } ?>

- +

@@ -2555,3 +2772,64 @@ 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()' + ); + wp_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 = static 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 ); +}