diff -r 34716fd837a4 -r be944660c56a wp/wp-includes/functions.php --- a/wp/wp-includes/functions.php Tue Dec 15 15:52:01 2020 +0100 +++ b/wp/wp-includes/functions.php Wed Sep 21 18:19:35 2022 +0200 @@ -467,7 +467,7 @@ } foreach ( $quant as $unit => $mag ) { - if ( doubleval( $bytes ) >= $mag ) { + if ( (float) $bytes >= $mag ) { return number_format_i18n( $bytes / $mag, $decimals ) . ' ' . $unit; } } @@ -830,14 +830,16 @@ * @since 1.5.0 * @since 5.3.0 The `$content` parameter was made optional, and the `$post` parameter was * updated to accept a post ID or a WP_Post object. + * @since 5.6.0 The `$content` parameter is no longer optional, but passing `null` to skip it + * is still supported. * * @global wpdb $wpdb WordPress database abstraction object. * - * @param string $content Post content. If `null`, the `post_content` field from `$post` is used. + * @param string|null $content Post content. If `null`, the `post_content` field from `$post` is used. * @param int|WP_Post $post Post ID or post object. - * @return null|bool Returns false if post is not found. - */ -function do_enclose( $content = null, $post ) { + * @return void|false Void on success, false if the post is not found. + */ +function do_enclose( $content, $post ) { global $wpdb; // @todo Tidy this code and make the debug code optional. @@ -897,6 +899,8 @@ $post_links = apply_filters( 'enclosure_links', $post_links, $post->ID ); foreach ( (array) $post_links as $url ) { + $url = strip_fragment_from_url( $url ); + if ( '' !== $url && ! $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = 'enclosure' AND meta_value LIKE %s", $post->ID, $wpdb->esc_like( $url ) . '%' ) ) ) { $headers = wp_get_http_headers( $url ); @@ -934,7 +938,7 @@ * * @param string $url URL to retrieve HTTP headers from. * @param bool $deprecated Not Used. - * @return bool|string False on failure, headers on success. + * @return string|false Headers on success, false on failure. */ function wp_get_http_headers( $url, $deprecated = false ) { if ( ! empty( $deprecated ) ) { @@ -1155,8 +1159,8 @@ * * @since 1.5.0 * - * @param string|array $key Query key or keys to remove. - * @param bool|string $query Optional. When false uses the current URL. Default false. + * @param string|string[] $key Query key or keys to remove. + * @param false|string $query Optional. When false uses the current URL. Default false. * @return string New URL query string. */ function remove_query_arg( $key, $query = false ) { @@ -1174,7 +1178,7 @@ * * @since 4.4.0 * - * @return string[] An array of parameters to remove from the URL. + * @return string[] An array of query variable names to remove from the URL. */ function wp_removable_query_args() { $removable_query_args = array( @@ -1182,6 +1186,7 @@ 'activated', 'admin_email_remind_later', 'approved', + 'core-major-auto-updates-saved', 'deactivate', 'delete_count', 'deleted', @@ -1191,6 +1196,7 @@ 'error', 'hotkeys_highlight_first', 'hotkeys_highlight_last', + 'ids', 'locked', 'message', 'same', @@ -1207,11 +1213,11 @@ ); /** - * Filters the list of query variables to remove. + * Filters the list of query variable names to remove. * * @since 4.2.0 * - * @param string[] $removable_query_args An array of query variables to remove from a URL. + * @param string[] $removable_query_args An array of query variable names to remove from a URL. */ return apply_filters( 'removable_query_args', $removable_query_args ); } @@ -1562,7 +1568,13 @@ * Fires once the given feed is loaded. * * The dynamic portion of the hook name, `$feed`, refers to the feed template name. - * Possible values include: 'rdf', 'rss', 'rss2', and 'atom'. + * + * Possible hook names include: + * + * - `do_feed_atom` + * - `do_feed_rdf` + * - `do_feed_rss` + * - `do_feed_rss2` * * @since 2.1.0 * @since 4.4.0 The `$feed` parameter was added. @@ -1634,7 +1646,8 @@ * * @since 2.1.0 * @since 5.3.0 Remove the "Disallow: /" output if search engine visiblity is - * discouraged in favor of robots meta HTML tag in wp_no_robots(). + * discouraged in favor of robots meta HTML tag via wp_robots_no_robots() + * filter callback. */ function do_robots() { header( 'Content-Type: text/plain; charset=utf-8' ); @@ -1745,7 +1758,7 @@ */ $wp_tables = $wpdb->tables(); foreach ( $wp_tables as $table ) { - // The existence of custom user tables shouldn't suggest an insane state or prevent a clean installation. + // The existence of custom user tables shouldn't suggest an unwise state or prevent a clean installation. if ( defined( 'CUSTOM_USER_TABLE' ) && CUSTOM_USER_TABLE == $table ) { continue; } @@ -1753,11 +1766,15 @@ continue; } - if ( ! $wpdb->get_results( "DESCRIBE $table;" ) ) { + $described_table = $wpdb->get_results( "DESCRIBE $table;" ); + if ( + ( ! $described_table && empty( $wpdb->last_error ) ) || + ( is_array( $described_table ) && 0 === count( $described_table ) ) + ) { continue; } - // One or more tables exist. We are insane. + // One or more tables exist. This is not good. wp_load_translations_early(); @@ -2468,6 +2485,10 @@ $filename = sanitize_file_name( $filename ); $ext2 = null; + // Initialize vars used in the wp_unique_filename filter. + $number = ''; + $alt_filenames = array(); + // Separate the filename into a name and extension. $ext = pathinfo( $filename, PATHINFO_EXTENSION ); $name = pathinfo( $filename, PATHINFO_BASENAME ); @@ -2488,8 +2509,7 @@ if ( $unique_filename_callback && is_callable( $unique_filename_callback ) ) { $filename = call_user_func( $unique_filename_callback, $dir, $name, $ext ); } else { - $number = ''; - $fname = pathinfo( $filename, PATHINFO_FILENAME ); + $fname = pathinfo( $filename, PATHINFO_FILENAME ); // Always append a number to file names that can potentially match image sub-size file names. if ( $fname && preg_match( '/-(?:\d+x\d+|scaled|rotated)$/', $fname ) ) { @@ -2499,37 +2519,54 @@ $filename = str_replace( "{$fname}{$ext}", "{$fname}-{$number}{$ext}", $filename ); } - // Change '.ext' to lower case. - if ( $ext && strtolower( $ext ) != $ext ) { - $ext2 = strtolower( $ext ); - $filename2 = preg_replace( '|' . preg_quote( $ext ) . '$|', $ext2, $filename ); - - // Check for both lower and upper case extension or image sub-sizes may be overwritten. - while ( file_exists( $dir . "/{$filename}" ) || file_exists( $dir . "/{$filename2}" ) ) { - $new_number = (int) $number + 1; - $filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename ); - $filename2 = str_replace( array( "-{$number}{$ext2}", "{$number}{$ext2}" ), "-{$new_number}{$ext2}", $filename2 ); - $number = $new_number; + // Get the mime type. Uploaded files were already checked with wp_check_filetype_and_ext() + // in _wp_handle_upload(). Using wp_check_filetype() would be sufficient here. + $file_type = wp_check_filetype( $filename ); + $mime_type = $file_type['type']; + + $is_image = ( ! empty( $mime_type ) && 0 === strpos( $mime_type, 'image/' ) ); + $upload_dir = wp_get_upload_dir(); + $lc_filename = null; + + $lc_ext = strtolower( $ext ); + $_dir = trailingslashit( $dir ); + + // If the extension is uppercase add an alternate file name with lowercase extension. Both need to be tested + // for uniqueness as the extension will be changed to lowercase for better compatibility with different filesystems. + // Fixes an inconsistency in WP < 2.9 where uppercase extensions were allowed but image sub-sizes were created with + // lowercase extensions. + if ( $ext && $lc_ext !== $ext ) { + $lc_filename = preg_replace( '|' . preg_quote( $ext ) . '$|', $lc_ext, $filename ); + } + + // Increment the number added to the file name if there are any files in $dir whose names match one of the + // possible name variations. + while ( file_exists( $_dir . $filename ) || ( $lc_filename && file_exists( $_dir . $lc_filename ) ) ) { + $new_number = (int) $number + 1; + + if ( $lc_filename ) { + $lc_filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $lc_filename ); } - $filename = $filename2; - } else { - while ( file_exists( $dir . "/{$filename}" ) ) { - $new_number = (int) $number + 1; - - if ( '' === "{$number}{$ext}" ) { - $filename = "{$filename}-{$new_number}"; - } else { - $filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename ); - } - - $number = $new_number; + if ( '' === "{$number}{$ext}" ) { + $filename = "{$filename}-{$new_number}"; + } else { + $filename = str_replace( array( "-{$number}{$ext}", "{$number}{$ext}" ), "-{$new_number}{$ext}", $filename ); } + + $number = $new_number; + } + + // Change the extension to lowercase if needed. + if ( $lc_filename ) { + $filename = $lc_filename; } // Prevent collisions with existing file names that contain dimension-like strings // (whether they are subsizes or originals uploaded prior to #42437). - $upload_dir = wp_get_upload_dir(); + + $files = array(); + $count = 10000; // The (resized) image files would have name and extension, and will be in the uploads dir. if ( $name && $ext && @is_dir( $dir ) && false !== strpos( $dir, $upload_dir['basedir'] ) ) { @@ -2559,18 +2596,77 @@ } if ( ! empty( $files ) ) { - // The extension case may have changed above. - $new_ext = ! empty( $ext2 ) ? $ext2 : $ext; + $count = count( $files ); // Ensure this never goes into infinite loop // as it uses pathinfo() and regex in the check, but string replacement for the changes. - $count = count( $files ); - $i = 0; + $i = 0; while ( $i <= $count && _wp_check_existing_file_names( $filename, $files ) ) { $new_number = (int) $number + 1; - $filename = str_replace( array( "-{$number}{$new_ext}", "{$number}{$new_ext}" ), "-{$new_number}{$new_ext}", $filename ); - $number = $new_number; + + // If $ext is uppercase it was replaced with the lowercase version after the previous loop. + $filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $filename ); + + $number = $new_number; + $i++; + } + } + } + + // Check if an image will be converted after uploading or some existing images sub-sizes file names may conflict + // when regenerated. If yes, ensure the new file name will be unique and will produce unique sub-sizes. + if ( $is_image ) { + $output_formats = apply_filters( 'image_editor_output_format', array(), $_dir . $filename, $mime_type ); + $alt_types = array(); + + if ( ! empty( $output_formats[ $mime_type ] ) ) { + // The image will be converted to this format/mime type. + $alt_mime_type = $output_formats[ $mime_type ]; + + // Other types of images whose names may conflict if their sub-sizes are regenerated. + $alt_types = array_keys( array_intersect( $output_formats, array( $mime_type, $alt_mime_type ) ) ); + $alt_types[] = $alt_mime_type; + } elseif ( ! empty( $output_formats ) ) { + $alt_types = array_keys( array_intersect( $output_formats, array( $mime_type ) ) ); + } + + // Remove duplicates and the original mime type. It will be added later if needed. + $alt_types = array_unique( array_diff( $alt_types, array( $mime_type ) ) ); + + foreach ( $alt_types as $alt_type ) { + $alt_ext = wp_get_default_extension_for_mime_type( $alt_type ); + + if ( ! $alt_ext ) { + continue; + } + + $alt_ext = ".{$alt_ext}"; + $alt_filename = preg_replace( '|' . preg_quote( $lc_ext ) . '$|', $alt_ext, $filename ); + + $alt_filenames[ $alt_ext ] = $alt_filename; + } + + if ( ! empty( $alt_filenames ) ) { + // Add the original filename. It needs to be checked again together with the alternate filenames + // when $number is incremented. + $alt_filenames[ $lc_ext ] = $filename; + + // Ensure no infinite loop. + $i = 0; + + while ( $i <= $count && _wp_check_alternate_file_names( $alt_filenames, $_dir, $files ) ) { + $new_number = (int) $number + 1; + + foreach ( $alt_filenames as $alt_ext => $alt_filename ) { + $alt_filenames[ $alt_ext ] = str_replace( array( "-{$number}{$alt_ext}", "{$number}{$alt_ext}" ), "-{$new_number}{$alt_ext}", $alt_filename ); + } + + // Also update the $number in (the output) $filename. + // If the extension was uppercase it was already replaced with the lowercase version. + $filename = str_replace( array( "-{$number}{$lc_ext}", "{$number}{$lc_ext}" ), "-{$new_number}{$lc_ext}", $filename ); + + $number = $new_number; $i++; } } @@ -2581,13 +2677,42 @@ * Filters the result when generating a unique file name. * * @since 4.5.0 + * @since 5.8.1 The `$alt_filenames` and `$number` parameters were added. * * @param string $filename Unique file name. * @param string $ext File extension, eg. ".png". * @param string $dir Directory path. * @param callable|null $unique_filename_callback Callback function that generates the unique file name. + * @param string[] $alt_filenames Array of alternate file names that were checked for collisions. + * @param int|string $number The highest number that was used to make the file name unique + * or an empty string if unused. */ - return apply_filters( 'wp_unique_filename', $filename, $ext, $dir, $unique_filename_callback ); + return apply_filters( 'wp_unique_filename', $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ); +} + +/** + * Helper function to test if each of an array of file names could conflict with existing files. + * + * @since 5.8.1 + * @access private + * + * @param string[] $filenames Array of file names to check. + * @param string $dir The directory containing the files. + * @param array $files An array of existing files in the directory. May be empty. + * @return bool True if the tested file name could match an existing file, false otherwise. + */ +function _wp_check_alternate_file_names( $filenames, $dir, $files ) { + foreach ( $filenames as $filename ) { + if ( file_exists( $dir . $filename ) ) { + return true; + } + + if ( ! empty( $files ) && _wp_check_existing_file_names( $filename, $files ) ) { + return true; + } + } + + return false; } /** @@ -2737,6 +2862,10 @@ // Compute the URL. $url = $upload['url'] . "/$filename"; + if ( is_multisite() ) { + clean_dirsize_cache( $new_file ); + } + /** This filter is documented in wp-admin/includes/file.php */ return apply_filters( 'wp_handle_upload', @@ -2770,6 +2899,26 @@ } /** + * Returns first matched extension for the mime-type, + * as mapped from wp_get_mime_types(). + * + * @since 5.8.1 + * + * @param string $mime_type + * + * @return string|false + */ +function wp_get_default_extension_for_mime_type( $mime_type ) { + $extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) ); + + if ( empty( $extensions[0] ) ) { + return false; + } + + return $extensions[0]; +} + +/** * Retrieve the file type from the file name. * * You can optionally define the mime array, if needed. @@ -2777,7 +2926,7 @@ * @since 2.0.4 * * @param string $filename File name or path. - * @param string[] $mimes Optional. Array of mime types keyed by their file extension regex. + * @param string[] $mimes Optional. Array of allowed mime types keyed by their file extension regex. * @return array { * Values for the extension and mime type. * @@ -2819,7 +2968,7 @@ * @param string $file Full path to the file. * @param string $filename The name of the file (may differ from $file due to $file being * in a tmp directory). - * @param string[] $mimes Optional. Array of mime types keyed by their file extension regex. + * @param string[] $mimes Optional. Array of allowed mime types keyed by their file extension regex. * @return array { * Values for the extension, mime type, and corrected filename. * @@ -2865,6 +3014,7 @@ 'image/gif' => 'gif', 'image/bmp' => 'bmp', 'image/tiff' => 'tif', + 'image/webp' => 'webp', ) ); @@ -2931,6 +3081,7 @@ array( 'text/plain', 'text/csv', + 'application/csv', 'text/richtext', 'text/tsv', 'text/vtt', @@ -2941,6 +3092,21 @@ $type = false; $ext = false; } + } elseif ( 'application/csv' === $real_mime ) { + // Special casing for CSV files. + if ( ! in_array( + $type, + array( + 'text/csv', + 'text/plain', + 'application/csv', + ), + true + ) + ) { + $type = false; + $ext = false; + } } elseif ( 'text/rtf' === $real_mime ) { // Special casing for RTF files. if ( ! in_array( @@ -2984,18 +3150,18 @@ * @since 3.0.0 * @since 5.1.0 The $real_mime parameter was added. * - * @param array $wp_check_filetype_and_ext { + * @param array $wp_check_filetype_and_ext { * Values for the extension, mime type, and corrected filename. * * @type string|false $ext File extension, or false if the file doesn't match a mime type. * @type string|false $type File mime type, or false if the file doesn't match a mime type. * @type string|false $proper_filename File name with its correct extension, or false if it cannot be determined. * } - * @param string $file Full path to the file. - * @param string $filename The name of the file (may differ from $file due to - * $file being in a tmp directory). - * @param string[] $mimes Array of mime types keyed by their file extension regex. - * @param string|bool $real_mime The actual mime type or false if the type cannot be determined. + * @param string $file Full path to the file. + * @param string $filename The name of the file (may differ from $file due to + * $file being in a tmp directory). + * @param string[] $mimes Array of mime types keyed by their file extension regex. + * @param string|false $real_mime The actual mime type or false if the type cannot be determined. */ return apply_filters( 'wp_check_filetype_and_ext', compact( 'ext', 'type', 'proper_filename' ), $file, $filename, $mimes, $real_mime ); } @@ -3006,6 +3172,7 @@ * This depends on exif_imagetype() or getimagesize() to determine real mime types. * * @since 4.7.1 + * @since 5.8.0 Added support for WebP images. * * @param string $file Full path to the file. * @return string|false The actual mime type or false if the type cannot be determined. @@ -3021,11 +3188,52 @@ $imagetype = exif_imagetype( $file ); $mime = ( $imagetype ) ? image_type_to_mime_type( $imagetype ) : false; } elseif ( function_exists( 'getimagesize' ) ) { - $imagesize = @getimagesize( $file ); - $mime = ( isset( $imagesize['mime'] ) ) ? $imagesize['mime'] : false; + // Don't silence errors when in debug mode, unless running unit tests. + if ( defined( 'WP_DEBUG' ) && WP_DEBUG + && ! defined( 'WP_RUN_CORE_TESTS' ) + ) { + // Not using wp_getimagesize() here to avoid an infinite loop. + $imagesize = getimagesize( $file ); + } else { + // phpcs:ignore WordPress.PHP.NoSilencedErrors + $imagesize = @getimagesize( $file ); + } + + $mime = ( isset( $imagesize['mime'] ) ) ? $imagesize['mime'] : false; } else { $mime = false; } + + if ( false !== $mime ) { + return $mime; + } + + $handle = fopen( $file, 'rb' ); + if ( false === $handle ) { + return false; + } + + $magic = fread( $handle, 12 ); + if ( false === $magic ) { + return false; + } + + /* + * Add WebP fallback detection when image library doesn't support WebP. + * Note: detection values come from LibWebP, see + * https://github.com/webmproject/libwebp/blob/master/imageio/image_dec.c#L30 + */ + $magic = bin2hex( $magic ); + if ( + // RIFF. + ( 0 === strpos( $magic, '52494646' ) ) && + // WEBP. + ( 16 === strpos( $magic, '57454250' ) ) + ) { + $mime = 'image/webp'; + } + + fclose( $handle ); } catch ( Exception $e ) { $mime = false; } @@ -3064,6 +3272,7 @@ 'png' => 'image/png', 'bmp' => 'image/bmp', 'tiff|tif' => 'image/tiff', + 'webp' => 'image/webp', 'ico' => 'image/x-icon', 'heic' => 'image/heic', // Video formats. @@ -3185,7 +3394,7 @@ return apply_filters( 'ext2type', array( - 'image' => array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'heic' ), + 'image' => array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'heic', 'webp' ), 'audio' => array( 'aac', 'ac3', 'aif', 'aiff', 'flac', 'm3a', 'm4a', 'm4b', 'mka', 'mp1', 'mp2', 'mp3', 'ogg', 'oga', 'ram', 'wav', 'wma' ), 'video' => array( '3g2', '3gp', '3gpp', 'asf', 'avi', 'divx', 'dv', 'flv', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mpv', 'ogm', 'ogv', 'qt', 'rm', 'vob', 'wmv' ), 'document' => array( 'doc', 'docx', 'docm', 'dotm', 'odt', 'pages', 'pdf', 'xps', 'oxps', 'rtf', 'wp', 'wpd', 'psd', 'xcf' ), @@ -3345,9 +3554,9 @@ * @param callable $function Callback function name. */ $function = apply_filters( 'wp_die_json_handler', '_json_wp_die_handler' ); - } elseif ( wp_is_jsonp_request() ) { + } elseif ( defined( 'REST_REQUEST' ) && REST_REQUEST && wp_is_jsonp_request() ) { /** - * Filters the callback for killing WordPress execution for JSONP requests. + * Filters the callback for killing WordPress execution for JSONP REST requests. * * @since 5.2.0 * @@ -3412,7 +3621,7 @@ array( $message ), wp_list_pluck( $parsed_args['additional_errors'], 'message' ) ); - $message = ""; + $message = ""; } $message = sprintf( @@ -3461,8 +3670,9 @@ <?php echo $title; ?> @@ -3472,13 +3682,14 @@ } body { background: #fff; + border: 1px solid #ccd0d4; color: #444; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; margin: 2em auto; padding: 1em 2em; max-width: 700px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .04); + box-shadow: 0 1px 1px rgba(0, 0, 0, .04); } h1 { border-bottom: 1px solid #dadada; @@ -3523,9 +3734,9 @@ outline: none; } .button { - background: #f7f7f7; - border: 1px solid #ccc; - color: #555; + background: #f3f5f6; + border: 1px solid #016087; + color: #016087; display: inline-block; text-decoration: none; font-size: 13px; @@ -3542,36 +3753,35 @@ -moz-box-sizing: border-box; box-sizing: border-box; - -webkit-box-shadow: 0 1px 0 #ccc; - box-shadow: 0 1px 0 #ccc; vertical-align: top; } .button.button-large { - height: 30px; - line-height: 2.15384615; - padding: 0 12px 2px; + line-height: 2.30769231; + min-height: 32px; + padding: 0 12px; } .button:hover, .button:focus { - background: #fafafa; - border-color: #999; - color: #23282d; + background: #f1f1f1; } .button:focus { - border-color: #5b9dd9; - -webkit-box-shadow: 0 0 3px rgba(0, 115, 170, 0.8); - box-shadow: 0 0 3px rgba(0, 115, 170, 0.8); - outline: none; + background: #f3f5f6; + border-color: #007cba; + -webkit-box-shadow: 0 0 0 1px #007cba; + box-shadow: 0 0 0 1px #007cba; + color: #016087; + outline: 2px solid transparent; + outline-offset: 0; } .button:active { - background: #eee; - border-color: #999; - -webkit-box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5); - box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5); + background: #f3f5f6; + border-color: #7e8993; + -webkit-box-shadow: none; + box-shadow: none; } true ); if ( isset( $data ) ) { $response['data'] = $data; } - wp_send_json( $response, $status_code ); + wp_send_json( $response, $status_code, $options ); } /** @@ -4126,11 +4340,13 @@ * @since 3.5.0 * @since 4.1.0 The `$data` parameter is now processed if a WP_Error object is passed in. * @since 4.7.0 The `$status_code` parameter was added. - * - * @param mixed $data Data to encode as JSON, then print and die. - * @param int $status_code The HTTP status code to output. - */ -function wp_send_json_error( $data = null, $status_code = null ) { + * @since 5.6.0 The `$options` parameter was added. + * + * @param mixed $data Optional. Data to encode as JSON, then print and die. Default null. + * @param int $status_code Optional. The HTTP status code to output. Default null. + * @param int $options Optional. Options to be passed to json_encode(). Default 0. + */ +function wp_send_json_error( $data = null, $status_code = null, $options = 0 ) { $response = array( 'success' => false ); if ( isset( $data ) ) { @@ -4151,7 +4367,7 @@ } } - wp_send_json( $response, $status_code ); + wp_send_json( $response, $status_code, $options ); } /** @@ -4397,7 +4613,7 @@ } /** - * Merge user defined arguments into defaults array. + * Merges user defined arguments into defaults array. * * This function is used throughout WordPress to allow for both string or array * to be merged into another array. @@ -4426,12 +4642,12 @@ } /** - * Cleans up an array, comma- or space-separated list of scalar values. + * Converts a comma- or space-separated list of scalar values to an array. * * @since 5.1.0 * * @param array|string $list List of values. - * @return array Sanitized array of values. + * @return array Array of values. */ function wp_parse_list( $list ) { if ( ! is_array( $list ) ) { @@ -4442,9 +4658,10 @@ } /** - * Clean up an array, comma- or space-separated list of IDs. + * Cleans up an array, comma- or space-separated list of IDs. * * @since 3.0.0 + * @since 5.1.0 Refactored to use wp_parse_list(). * * @param array|string $list List of IDs. * @return int[] Sanitized array of IDs. @@ -4456,9 +4673,10 @@ } /** - * Clean up an array, comma- or space-separated list of slugs. + * Cleans up an array, comma- or space-separated list of slugs. * * @since 4.7.0 + * @since 5.1.0 Refactored to use wp_parse_list(). * * @param array|string $list List of slugs. * @return string[] Sanitized array of slugs. @@ -4480,6 +4698,7 @@ */ function wp_array_slice_assoc( $array, $keys ) { $slice = array(); + foreach ( $keys as $key ) { if ( isset( $array[ $key ] ) ) { $slice[ $key ] = $array[ $key ]; @@ -4490,6 +4709,200 @@ } /** + * Accesses an array in depth based on a path of keys. + * + * It is the PHP equivalent of JavaScript's `lodash.get()` and mirroring it may help other components + * retain some symmetry between client and server implementations. + * + * Example usage: + * + * $array = array( + * 'a' => array( + * 'b' => array( + * 'c' => 1, + * ), + * ), + * ); + * _wp_array_get( $array, array( 'a', 'b', 'c' ) ); + * + * @internal + * + * @since 5.6.0 + * @access private + * + * @param array $array An array from which we want to retrieve some information. + * @param array $path An array of keys describing the path with which to retrieve information. + * @param mixed $default The return value if the path does not exist within the array, + * or if `$array` or `$path` are not arrays. + * @return mixed The value from the path specified. + */ +function _wp_array_get( $array, $path, $default = null ) { + // Confirm $path is valid. + if ( ! is_array( $path ) || 0 === count( $path ) ) { + return $default; + } + + foreach ( $path as $path_element ) { + if ( + ! is_array( $array ) || + ( ! is_string( $path_element ) && ! is_integer( $path_element ) && ! is_null( $path_element ) ) || + ! array_key_exists( $path_element, $array ) + ) { + return $default; + } + $array = $array[ $path_element ]; + } + + return $array; +} + +/** + * Sets an array in depth based on a path of keys. + * + * It is the PHP equivalent of JavaScript's `lodash.set()` and mirroring it may help other components + * retain some symmetry between client and server implementations. + * + * Example usage: + * + * $array = array(); + * _wp_array_set( $array, array( 'a', 'b', 'c', 1 ) ); + * + * $array becomes: + * array( + * 'a' => array( + * 'b' => array( + * 'c' => 1, + * ), + * ), + * ); + * + * @internal + * + * @since 5.8.0 + * @access private + * + * @param array $array An array that we want to mutate to include a specific value in a path. + * @param array $path An array of keys describing the path that we want to mutate. + * @param mixed $value The value that will be set. + */ +function _wp_array_set( &$array, $path, $value = null ) { + // Confirm $array is valid. + if ( ! is_array( $array ) ) { + return; + } + + // Confirm $path is valid. + if ( ! is_array( $path ) ) { + return; + } + + $path_length = count( $path ); + + if ( 0 === $path_length ) { + return; + } + + foreach ( $path as $path_element ) { + if ( + ! is_string( $path_element ) && ! is_integer( $path_element ) && + ! is_null( $path_element ) + ) { + return; + } + } + + for ( $i = 0; $i < $path_length - 1; ++$i ) { + $path_element = $path[ $i ]; + if ( + ! array_key_exists( $path_element, $array ) || + ! is_array( $array[ $path_element ] ) + ) { + $array[ $path_element ] = array(); + } + $array = &$array[ $path_element ]; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.VariableRedeclaration + } + + $array[ $path[ $i ] ] = $value; +} + +/** + * This function is trying to replicate what + * lodash's kebabCase (JS library) does in the client. + * + * The reason we need this function is that we do some processing + * in both the client and the server (e.g.: we generate + * preset classes from preset slugs) that needs to + * create the same output. + * + * We can't remove or update the client's library due to backward compatibility + * (some of the output of lodash's kebabCase is saved in the post content). + * We have to make the server behave like the client. + * + * Changes to this function should follow updates in the client + * with the same logic. + * + * @link https://github.com/lodash/lodash/blob/4.17/dist/lodash.js#L14369 + * @link https://github.com/lodash/lodash/blob/4.17/dist/lodash.js#L278 + * @link https://github.com/lodash-php/lodash-php/blob/master/src/String/kebabCase.php + * @link https://github.com/lodash-php/lodash-php/blob/master/src/internal/unicodeWords.php + * + * @param string $string The string to kebab-case. + * + * @return string kebab-cased-string. + */ +function _wp_to_kebab_case( $string ) { + //phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + // ignore the camelCase names for variables so the names are the same as lodash + // so comparing and porting new changes is easier. + + /* + * Some notable things we've removed compared to the lodash version are: + * + * - non-alphanumeric characters: rsAstralRange, rsEmoji, etc + * - the groups that processed the apostrophe, as it's removed before passing the string to preg_match: rsApos, rsOptContrLower, and rsOptContrUpper + * + */ + + /** Used to compose unicode character classes. */ + $rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff'; + $rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf'; + $rsPunctuationRange = '\\x{2000}-\\x{206f}'; + $rsSpaceRange = ' \\t\\x0b\\f\\xa0\\x{feff}\\n\\r\\x{2028}\\x{2029}\\x{1680}\\x{180e}\\x{2000}\\x{2001}\\x{2002}\\x{2003}\\x{2004}\\x{2005}\\x{2006}\\x{2007}\\x{2008}\\x{2009}\\x{200a}\\x{202f}\\x{205f}\\x{3000}'; + $rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde'; + $rsBreakRange = $rsNonCharRange . $rsPunctuationRange . $rsSpaceRange; + + /** Used to compose unicode capture groups. */ + $rsBreak = '[' . $rsBreakRange . ']'; + $rsDigits = '\\d+'; // The last lodash version in GitHub uses a single digit here and expands it when in use. + $rsLower = '[' . $rsLowerRange . ']'; + $rsMisc = '[^' . $rsBreakRange . $rsDigits . $rsLowerRange . $rsUpperRange . ']'; + $rsUpper = '[' . $rsUpperRange . ']'; + + /** Used to compose unicode regexes. */ + $rsMiscLower = '(?:' . $rsLower . '|' . $rsMisc . ')'; + $rsMiscUpper = '(?:' . $rsUpper . '|' . $rsMisc . ')'; + $rsOrdLower = '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])'; + $rsOrdUpper = '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])'; + + $regexp = '/' . implode( + '|', + array( + $rsUpper . '?' . $rsLower . '+' . '(?=' . implode( '|', array( $rsBreak, $rsUpper, '$' ) ) . ')', + $rsMiscUpper . '+' . '(?=' . implode( '|', array( $rsBreak, $rsUpper . $rsMiscLower, '$' ) ) . ')', + $rsUpper . '?' . $rsMiscLower . '+', + $rsUpper . '+', + $rsOrdUpper, + $rsOrdLower, + $rsDigits, + ) + ) . '/u'; + + preg_match_all( $regexp, str_replace( "'", '', $string ), $matches ); + return strtolower( implode( '-', $matches[0] ) ); + //phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase +} + +/** * Determines if the variable is a numeric-indexed array. * * @since 4.4.0 @@ -4504,24 +4917,36 @@ $keys = array_keys( $data ); $string_keys = array_filter( $keys, 'is_string' ); + return count( $string_keys ) === 0; } /** * Filters a list of objects, based on a set of key => value arguments. * + * Retrieves the objects from the list that match the given arguments. + * Key represents property name, and value represents property value. + * + * If an object has more properties than those specified in arguments, + * that will not disqualify it. When using the 'AND' operator, + * any missing properties will disqualify it. + * + * When using the `$field` argument, this function can also retrieve + * a particular field from all matching objects, whereas wp_list_filter() + * only does the filtering. + * * @since 3.0.0 * @since 4.7.0 Uses `WP_List_Util` class. * - * @param array $list An array of objects to filter + * @param array $list An array of objects to filter. * @param array $args Optional. An array of key => value arguments to match * against each object. Default empty array. - * @param string $operator Optional. The logical operation to perform. 'or' means - * only one element from the array needs to match; 'and' - * means all elements must match; 'not' means no elements may - * match. Default 'and'. - * @param bool|string $field A field from the object to place instead of the entire object. - * Default false. + * @param string $operator Optional. The logical operation to perform. 'AND' means + * all elements from the array must match. 'OR' means only + * one element needs to match. 'NOT' means no elements may + * match. Default 'AND'. + * @param bool|string $field Optional. A field from the object to place instead + * of the entire object. Default false. * @return array A list of objects or object fields. */ function wp_filter_object_list( $list, $args = array(), $operator = 'and', $field = false ) { @@ -4543,6 +4968,16 @@ /** * Filters a list of objects, based on a set of key => value arguments. * + * Retrieves the objects from the list that match the given arguments. + * Key represents property name, and value represents property value. + * + * If an object has more properties than those specified in arguments, + * that will not disqualify it. When using the 'AND' operator, + * any missing properties will disqualify it. + * + * If you want to retrieve a particular field from all matching objects, + * use wp_filter_object_list() instead. + * * @since 3.1.0 * @since 4.7.0 Uses `WP_List_Util` class. * @@ -4561,6 +4996,7 @@ } $util = new WP_List_Util( $list ); + return $util->filter( $args, $operator ); } @@ -4574,8 +5010,8 @@ * @since 4.0.0 $index_key parameter added. * @since 4.7.0 Uses `WP_List_Util` class. * - * @param array $list List of objects or arrays - * @param int|string $field Field from the object to place instead of the entire object + * @param array $list List of objects or arrays. + * @param int|string $field Field from the object to place instead of the entire object. * @param int|string $index_key Optional. Field from the object to use as keys for the new array. * Default null. * @return array Array of found values. If `$index_key` is set, an array of found values with keys @@ -4584,6 +5020,7 @@ */ function wp_list_pluck( $list, $field, $index_key = null ) { $util = new WP_List_Util( $list ); + return $util->pluck( $field, $index_key ); } @@ -4606,6 +5043,7 @@ } $util = new WP_List_Util( $list ); + return $util->sort( $orderby, $order, $preserve_keys ); } @@ -4717,7 +5155,7 @@ * @return int A non-negative integer. */ function absint( $maybeint ) { - return abs( intval( $maybeint ) ); + return abs( (int) $maybeint ); } /** @@ -5827,7 +6265,7 @@ } // Build the value. - $value = join( '/', $value ); + $value = implode( '/', $value ); $selected = ''; if ( $value === $selected_zone ) { $selected = 'selected="selected" '; @@ -5928,7 +6366,7 @@ } $structure[] = ''; - return join( "\n", $structure ); + return implode( "\n", $structure ); } /** @@ -6022,11 +6460,15 @@ // We don't need to write to the file, so just open for reading. $fp = fopen( $file, 'r' ); - // Pull only the first 8 KB of the file in. - $file_data = fread( $fp, 8 * KB_IN_BYTES ); - - // PHP will close file handle, but we are good citizens. - fclose( $fp ); + if ( $fp ) { + // Pull only the first 8 KB of the file in. + $file_data = fread( $fp, 8 * KB_IN_BYTES ); + + // PHP will close file handle, but we are good citizens. + fclose( $fp ); + } else { + $file_data = ''; + } // Make sure we catch CR-only line endings. $file_data = str_replace( "\r", "\n", $file_data ); @@ -6050,7 +6492,7 @@ } foreach ( $all_headers as $field => $regex ) { - if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) { + if ( preg_match( '/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) { $all_headers[ $field ] = _cleanup_header_comment( $match[1] ); } else { $all_headers[ $field ] = ''; @@ -6277,13 +6719,14 @@ * @since 4.3.0 Added 'webcal' to the protocols array. * @since 4.7.0 Added 'urn' to the protocols array. * @since 5.3.0 Added 'sms' to the protocols array. + * @since 5.6.0 Added 'irc6' and 'ircs' to the protocols array. * * @see wp_kses() * @see esc_url() * * @return string[] Array of allowed protocols. Defaults to an array containing 'http', 'https', - * 'ftp', 'ftps', 'mailto', 'news', 'irc', 'gopher', 'nntp', 'feed', 'telnet', - * 'mms', 'rtsp', 'sms', 'svn', 'tel', 'fax', 'xmpp', 'webcal', and 'urn'. + * 'ftp', 'ftps', 'mailto', 'news', 'irc', 'irc6', 'ircs', 'gopher', 'nntp', 'feed', + * 'telnet', 'mms', 'rtsp', 'sms', 'svn', 'tel', 'fax', 'xmpp', 'webcal', and 'urn'. * This covers all common link protocols, except for 'javascript' which should not * be allowed for untrusted users. */ @@ -6291,7 +6734,7 @@ static $protocols = array(); if ( empty( $protocols ) ) { - $protocols = array( 'http', 'https', 'ftp', 'ftps', 'mailto', 'news', 'irc', 'gopher', 'nntp', 'feed', 'telnet', 'mms', 'rtsp', 'sms', 'svn', 'tel', 'fax', 'xmpp', 'webcal', 'urn' ); + $protocols = array( 'http', 'https', 'ftp', 'ftps', 'mailto', 'news', 'irc', 'irc6', 'ircs', 'gopher', 'nntp', 'feed', 'telnet', 'mms', 'rtsp', 'sms', 'svn', 'tel', 'fax', 'xmpp', 'webcal', 'urn' ); } if ( ! did_action( 'wp_loaded' ) ) { @@ -6361,7 +6804,7 @@ } } if ( $pretty ) { - return join( ', ', array_reverse( $caller ) ); + return implode( ', ', array_reverse( $caller ) ); } else { return $caller; } @@ -6650,7 +7093,13 @@ static $overloaded = null; if ( is_null( $overloaded ) ) { - $overloaded = function_exists( 'mb_internal_encoding' ) && ( ini_get( 'mbstring.func_overload' ) & 2 ); // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated + if ( function_exists( 'mb_internal_encoding' ) + && ( (int) ini_get( 'mbstring.func_overload' ) & 2 ) // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated + ) { + $overloaded = true; + } else { + $overloaded = false; + } } if ( false === $overloaded ) { @@ -6821,9 +7270,9 @@ * * @param string $context Optional. Context in which the function is called. Accepts either 'admin', * 'image', or an arbitrary other context. If an arbitrary context is passed, - * the similarly arbitrary {@see '{$context}_memory_limit'} filter will be + * the similarly arbitrary {@see '$context_memory_limit'} filter will be * invoked. Default 'admin'. - * @return bool|int|string The limit that was set or false on failure. + * @return int|string|false The limit that was set or false on failure. */ function wp_raise_memory_limit( $context = 'admin' ) { // Exit early if the limit cannot be changed. @@ -7070,18 +7519,18 @@ * @since 4.9.0 * * @param array $email_change_email { - * Used to build wp_mail(). + * Used to build wp_mail(). * - * @type string $to The intended recipient. - * @type string $subject The subject of the email. - * @type string $message The content of the email. - * The following strings have a special meaning and will get replaced dynamically: - * - ###OLD_EMAIL### The old site admin email address. - * - ###NEW_EMAIL### The new site admin email address. - * - ###SITENAME### The name of the site. - * - ###SITEURL### The URL to the site. - * @type string $headers Headers. - * } + * @type string $to The intended recipient. + * @type string $subject The subject of the email. + * @type string $message The content of the email. + * The following strings have a special meaning and will get replaced dynamically: + * - ###OLD_EMAIL### The old site admin email address. + * - ###NEW_EMAIL### The new site admin email address. + * - ###SITENAME### The name of the site. + * - ###SITEURL### The URL to the site. + * @type string $headers Headers. + * } * @param string $old_email The old site admin email address. * @param string $new_email The new site admin email address. */ @@ -7239,6 +7688,8 @@ * Filters the directory used to store personal data export files. * * @since 4.9.6 + * @since 5.5.0 Exports now use relative paths, so changes to the directory + * via this filter should be reflected on the server. * * @param string $exports_dir Exports directory. */ @@ -7262,6 +7713,8 @@ * Filters the URL of the directory used to store personal data export files. * * @since 4.9.6 + * @since 5.5.0 Exports now use relative paths, so changes to the directory URL + * via this filter should be reflected on the server. * * @param string $exports_url Exports directory URL. */ @@ -7301,7 +7754,7 @@ } require_once ABSPATH . 'wp-admin/includes/file.php'; - $export_files = list_files( $exports_dir, 100, array( 'index.html' ) ); + $export_files = list_files( $exports_dir, 100, array( 'index.php' ) ); /** * Filters the lifetime, in seconds, of a personal data export file. @@ -7474,7 +7927,7 @@ echo '

'; printf( - '%2$s %3$s', + '%2$s %3$s', esc_url( $direct_update_url ), __( 'Update PHP' ), /* translators: Accessibility text. */ @@ -7484,6 +7937,91 @@ } /** + * Gets the URL to learn more about updating the site to use HTTPS. + * + * This URL can be overridden by specifying an environment variable `WP_UPDATE_HTTPS_URL` or by using the + * {@see 'wp_update_https_url'} filter. Providing an empty string is not allowed and will result in the + * default URL being used. Furthermore the page the URL links to should preferably be localized in the + * site language. + * + * @since 5.7.0 + * + * @return string URL to learn more about updating to HTTPS. + */ +function wp_get_update_https_url() { + $default_url = wp_get_default_update_https_url(); + + $update_url = $default_url; + if ( false !== getenv( 'WP_UPDATE_HTTPS_URL' ) ) { + $update_url = getenv( 'WP_UPDATE_HTTPS_URL' ); + } + + /** + * Filters the URL to learn more about updating the HTTPS version the site is running on. + * + * Providing an empty string is not allowed and will result in the default URL being used. Furthermore + * the page the URL links to should preferably be localized in the site language. + * + * @since 5.7.0 + * + * @param string $update_url URL to learn more about updating HTTPS. + */ + $update_url = apply_filters( 'wp_update_https_url', $update_url ); + if ( empty( $update_url ) ) { + $update_url = $default_url; + } + + return $update_url; +} + +/** + * Gets the default URL to learn more about updating the site to use HTTPS. + * + * Do not use this function to retrieve this URL. Instead, use {@see wp_get_update_https_url()} when relying on the URL. + * This function does not allow modifying the returned URL, and is only used to compare the actually used URL with the + * default one. + * + * @since 5.7.0 + * @access private + * + * @return string Default URL to learn more about updating to HTTPS. + */ +function wp_get_default_update_https_url() { + /* translators: Documentation explaining HTTPS and why it should be used. */ + return __( 'https://wordpress.org/support/article/why-should-i-use-https/' ); +} + +/** + * Gets the URL for directly updating the site to use HTTPS. + * + * A URL will only be returned if the `WP_DIRECT_UPDATE_HTTPS_URL` environment variable is specified or + * by using the {@see 'wp_direct_update_https_url'} filter. This allows hosts to send users directly to + * the page where they can update their site to use HTTPS. + * + * @since 5.7.0 + * + * @return string URL for directly updating to HTTPS or empty string. + */ +function wp_get_direct_update_https_url() { + $direct_update_url = ''; + + if ( false !== getenv( 'WP_DIRECT_UPDATE_HTTPS_URL' ) ) { + $direct_update_url = getenv( 'WP_DIRECT_UPDATE_HTTPS_URL' ); + } + + /** + * Filters the URL for directly updating the PHP version the site is running on from the host. + * + * @since 5.7.0 + * + * @param string $direct_update_url URL for directly updating PHP. + */ + $direct_update_url = apply_filters( 'wp_direct_update_https_url', $direct_update_url ); + + return $direct_update_url; +} + +/** * Get the size of a directory. * * A helper function that is used primarily to check whether @@ -7498,49 +8036,49 @@ * @return int|false|null Size in bytes if a valid directory. False if not. Null if timeout. */ function get_dirsize( $directory, $max_execution_time = null ) { - $dirsize = get_transient( 'dirsize_cache' ); - - if ( is_array( $dirsize ) && isset( $dirsize[ $directory ]['size'] ) ) { - return $dirsize[ $directory ]['size']; - } - - if ( ! is_array( $dirsize ) ) { - $dirsize = array(); - } // Exclude individual site directories from the total when checking the main site of a network, // as they are subdirectories and should not be counted. if ( is_multisite() && is_main_site() ) { - $dirsize[ $directory ]['size'] = recurse_dirsize( $directory, $directory . '/sites', $max_execution_time ); + $size = recurse_dirsize( $directory, $directory . '/sites', $max_execution_time ); } else { - $dirsize[ $directory ]['size'] = recurse_dirsize( $directory, null, $max_execution_time ); - } - - set_transient( 'dirsize_cache', $dirsize, HOUR_IN_SECONDS ); - return $dirsize[ $directory ]['size']; + $size = recurse_dirsize( $directory, null, $max_execution_time ); + } + + return $size; } /** * Get the size of a directory recursively. * - * Used by get_dirsize() to get a directory's size when it contains - * other directories. + * Used by get_dirsize() to get a directory size when it contains other directories. * * @since MU (3.0.0) - * @since 4.3.0 $exclude parameter added. - * @since 5.2.0 $max_execution_time parameter added. + * @since 4.3.0 The `$exclude` parameter was added. + * @since 5.2.0 The `$max_execution_time` parameter was added. + * @since 5.6.0 The `$directory_cache` parameter was added. * * @param string $directory Full path of a directory. * @param string|array $exclude Optional. Full path of a subdirectory to exclude from the total, * or array of paths. Expected without trailing slash(es). * @param int $max_execution_time Maximum time to run before giving up. In seconds. The timeout is global * and is measured from the moment WordPress started to load. + * @param array $directory_cache Optional. Array of cached directory paths. + * * @return int|false|null Size in bytes if a valid directory. False if not. Null if timeout. */ -function recurse_dirsize( $directory, $exclude = null, $max_execution_time = null ) { - $size = 0; - - $directory = untrailingslashit( $directory ); +function recurse_dirsize( $directory, $exclude = null, $max_execution_time = null, &$directory_cache = null ) { + $directory = untrailingslashit( $directory ); + $save_cache = false; + + if ( ! isset( $directory_cache ) ) { + $directory_cache = get_transient( 'dirsize_cache' ); + $save_cache = true; + } + + if ( isset( $directory_cache[ $directory ] ) && is_int( $directory_cache[ $directory ] ) ) { + return $directory_cache[ $directory ]; + } if ( ! file_exists( $directory ) || ! is_dir( $directory ) || ! is_readable( $directory ) ) { return false; @@ -7568,33 +8106,84 @@ } } - $handle = opendir( $directory ); - if ( $handle ) { - while ( ( $file = readdir( $handle ) ) !== false ) { - $path = $directory . '/' . $file; - if ( '.' !== $file && '..' !== $file ) { - if ( is_file( $path ) ) { - $size += filesize( $path ); - } elseif ( is_dir( $path ) ) { - $handlesize = recurse_dirsize( $path, $exclude, $max_execution_time ); - if ( $handlesize > 0 ) { - $size += $handlesize; + /** + * Filters the amount of storage space used by one directory and all its children, in megabytes. + * + * Return the actual used space to short-circuit the recursive PHP file size calculation + * and use something else, like a CDN API or native operating system tools for better performance. + * + * @since 5.6.0 + * + * @param int|false $space_used The amount of used space, in bytes. Default false. + */ + $size = apply_filters( 'pre_recurse_dirsize', false, $directory, $exclude, $max_execution_time, $directory_cache ); + + if ( false === $size ) { + $size = 0; + + $handle = opendir( $directory ); + if ( $handle ) { + while ( ( $file = readdir( $handle ) ) !== false ) { + $path = $directory . '/' . $file; + if ( '.' !== $file && '..' !== $file ) { + if ( is_file( $path ) ) { + $size += filesize( $path ); + } elseif ( is_dir( $path ) ) { + $handlesize = recurse_dirsize( $path, $exclude, $max_execution_time, $directory_cache ); + if ( $handlesize > 0 ) { + $size += $handlesize; + } + } + + if ( $max_execution_time > 0 && microtime( true ) - WP_START_TIMESTAMP > $max_execution_time ) { + // Time exceeded. Give up instead of risking a fatal timeout. + $size = null; + break; } } - - if ( $max_execution_time > 0 && microtime( true ) - WP_START_TIMESTAMP > $max_execution_time ) { - // Time exceeded. Give up instead of risking a fatal timeout. - $size = null; - break; - } } - } - closedir( $handle ); - } + closedir( $handle ); + } + } + + $directory_cache[ $directory ] = $size; + + // Only write the transient on the top level call and not on recursive calls. + if ( $save_cache ) { + set_transient( 'dirsize_cache', $directory_cache ); + } + return $size; } /** + * Cleans directory size cache used by recurse_dirsize(). + * + * Removes the current directory and all parent directories from the `dirsize_cache` transient. + * + * @since 5.6.0 + * + * @param string $path Full path of a directory or file. + */ +function clean_dirsize_cache( $path ) { + $directory_cache = get_transient( 'dirsize_cache' ); + + if ( empty( $directory_cache ) ) { + return; + } + + $path = untrailingslashit( $path ); + unset( $directory_cache[ $path ] ); + + while ( DIRECTORY_SEPARATOR !== $path && '.' !== $path && '..' !== $path ) { + $path = dirname( $path ); + unset( $directory_cache[ $path ] ); + } + + set_transient( 'dirsize_cache', $directory_cache ); +} + +/** * Checks compatibility with the current WordPress version. * * @since 5.2.0