* @param array $file Reference to a single element of `$_FILES`.
* Call the function once for each uploaded file.
* @param array|false $overrides Optional. An associative array of names => values
* to override default variables. Default false.
* @param string $time Optional. Time formatted in 'yyyy/mm'. Default null.
* @return array On success, returns an associative array of file attributes.
* On failure, returns `$overrides['upload_error_handler']( &$file, $message )`
* or `array( 'error' => $message )`.
function wp_handle_upload( &$file, $overrides = false, $time = null ) {
* $_POST['action'] must be set and its value must equal $overrides['action']
$action = 'wp_handle_upload';
if ( isset( $overrides['action'] ) ) {
$action = $overrides['action'];
return _wp_handle_upload( $file, $overrides, $time, $action );
* Wrapper for _wp_handle_upload().
* Passes the {@see 'wp_handle_sideload'} action.
* @see _wp_handle_upload()
* @param array $file Reference to a single element of `$_FILES`.
* Call the function once for each uploaded file.
* @param array|false $overrides Optional. An associative array of names => values
* to override default variables. Default false.
* @param string $time Optional. Time formatted in 'yyyy/mm'. Default null.
* @return array On success, returns an associative array of file attributes.
* On failure, returns `$overrides['upload_error_handler']( &$file, $message )`
* or `array( 'error' => $message )`.
function wp_handle_sideload( &$file, $overrides = false, $time = null ) {
* $_POST['action'] must be set and its value must equal $overrides['action']
$action = 'wp_handle_sideload';
if ( isset( $overrides['action'] ) ) {
$action = $overrides['action'];
return _wp_handle_upload( $file, $overrides, $time, $action );
* Downloads a URL to a local temporary file using the WordPress HTTP API.
* Please note that the calling function must unlink() the file.
* @since 5.2.0 Signature Verification with SoftFail was added.
* @param string $url The URL of the file to download.
* @param int $timeout The timeout for the request to download the file.
* @param bool $signature_verification Whether to perform Signature Verification.
* @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.
return new WP_Error( 'http_no_url', __( 'Invalid URL Provided.' ) );
$url_filename = basename( parse_url( $url, PHP_URL_PATH ) );
$tmpfname = wp_tempnam( $url_filename );
return new WP_Error( 'http_no_file', __( 'Could not create Temporary file.' ) );
$response = wp_safe_remote_get(
if ( is_wp_error( $response ) ) {
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 != $response_code ) {
'code' => $response_code,
// Retrieve a sample of the response body for debugging purposes.
$tmpf = fopen( $tmpfname, 'rb' );
* Filters the maximum error response body size in `download_url()`.
* @param int $size The maximum error response body size. Default 1 KB.
$response_size = apply_filters( 'download_url_error_max_body_size', KB_IN_BYTES );
$data['body'] = fread( $tmpf, $response_size );
return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ), $data );
$content_md5 = wp_remote_retrieve_header( $response, 'content-md5' );
$md5_check = verify_file_md5( $tmpfname, $content_md5 );
if ( is_wp_error( $md5_check ) ) {
// If the caller expects signature verification to occur, check to see if this URL supports it.
if ( $signature_verification ) {
* Filters the list of hosts which should have Signature Verification attempted on.
* @param string[] $hostnames List of hostnames.
$signed_hostnames = apply_filters( 'wp_signature_hosts', array( 'wordpress.org', 'downloads.wordpress.org', 's.w.org' ) );
$signature_verification = in_array( parse_url( $url, PHP_URL_HOST ), $signed_hostnames, true );
// Perform signature valiation if supported.
if ( $signature_verification ) {
$signature = wp_remote_retrieve_header( $response, 'x-content-signature' );
// Retrieve signatures from a file if the header wasn't included.
// WordPress.org stores signatures at $package_url.sig.
$url_path = parse_url( $url, PHP_URL_PATH );
if ( '.zip' === substr( $url_path, -4 ) || '.tar.gz' === substr( $url_path, -7 ) ) {
$signature_url = str_replace( $url_path, $url_path . '.sig', $url );
* Filters the URL where the signature for a file is located.
* @param false|string $signature_url The URL where signatures can be found for a file, or false if none are known.
* @param string $url The URL being verified.
$signature_url = apply_filters( 'wp_signature_url', $signature_url, $url );
$signature_request = wp_safe_remote_get(
'limit_response_size' => 10 * KB_IN_BYTES, // 10KB should be large enough for quite a few signatures.
if ( ! is_wp_error( $signature_request ) && 200 === wp_remote_retrieve_response_code( $signature_request ) ) {
$signature = explode( "\n", wp_remote_retrieve_body( $signature_request ) );
$signature_verification = verify_file_signature( $tmpfname, $signature, basename( parse_url( $url, PHP_URL_PATH ) ) );
if ( is_wp_error( $signature_verification ) ) {
* Filters whether Signature Verification failures should be allowed to soft fail.
* WARNING: This may be removed from a future release.
* @param bool $signature_softfail If a softfail is allowed.
* @param string $url The url being accessed.
apply_filters( 'wp_signature_softfail', true, $url )
$signature_verification->add_data( $tmpfname, 'softfail-filename' );
return $signature_verification;
* Calculates and compares the MD5 of a file to its expected value.
* @param string $filename The filename to check the MD5 of.
* @param string $expected_md5 The expected MD5 of the file, either a base64-encoded raw md5,
* @return bool|WP_Error True on success, false when the MD5 format is unknown/unexpected,
function verify_file_md5( $filename, $expected_md5 ) {
if ( 32 == strlen( $expected_md5 ) ) {
$expected_raw_md5 = pack( 'H*', $expected_md5 );
} elseif ( 24 == strlen( $expected_md5 ) ) {
$expected_raw_md5 = base64_decode( $expected_md5 );
return false; // Unknown format.
$file_md5 = md5_file( $filename, true );
if ( $file_md5 === $expected_raw_md5 ) {
/* translators: 1: File checksum, 2: Expected checksum value. */
__( 'The checksum of the file (%1$s) does not match the expected checksum value (%2$s).' ),
bin2hex( $expected_raw_md5 )
* Verifies the contents of a file against its ED25519 signature.
* @param string $filename The file to validate.
* @param string|array $signatures A Signature provided for the file.
* @param string|false $filename_for_errors Optional. A friendly filename for errors.
* @return bool|WP_Error True on success, false if verification not attempted,
* or WP_Error describing an error condition.
function verify_file_signature( $filename, $signatures, $filename_for_errors = false ) {
if ( ! $filename_for_errors ) {
$filename_for_errors = wp_basename( $filename );
// Check we can process signatures.
if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) || ! in_array( 'sha384', array_map( 'strtolower', hash_algos() ), true ) ) {
'signature_verification_unsupported',
/* translators: %s: The filename of the package. */
__( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ),
'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ? 'sodium_crypto_sign_verify_detached' : 'sha384' )
// Check for a edge-case affecting PHP Maths abilities.
! 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
'signature_verification_unsupported',
/* translators: %s: The filename of the package. */
__( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ),
'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
// phpcs:ignore PHPCompatibility.Constants.NewConstants.sodium_library_versionFound
'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ),
// Verify runtime speed of Sodium_Compat is acceptable.
if ( ! extension_loaded( 'sodium' ) && ! ParagonIE_Sodium_Compat::polyfill_is_fast() ) {
$sodium_compat_is_fast = false;
// Allow for an old version of Sodium_Compat being loaded before the bundled WordPress one.
if ( method_exists( 'ParagonIE_Sodium_Compat', 'runtime_speed_test' ) ) {
// Run `ParagonIE_Sodium_Compat::runtime_speed_test()` in optimized integer mode, as that's what WordPress utilises during signing verifications.
// phpcs:disable WordPress.NamingConventions.ValidVariableName
$old_fastMult = ParagonIE_Sodium_Compat::$fastMult;
ParagonIE_Sodium_Compat::$fastMult = true;
$sodium_compat_is_fast = ParagonIE_Sodium_Compat::runtime_speed_test( 100, 10 );
ParagonIE_Sodium_Compat::$fastMult = $old_fastMult;
// 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 ) {
'signature_verification_unsupported',
/* translators: %s: The filename of the package. */
__( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ),
'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
// phpcs:ignore PHPCompatibility.Constants.NewConstants.sodium_library_versionFound
'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' ),
'signature_verification_no_signature',
/* translators: %s: The filename of the package. */
__( 'The authenticity of %s could not be verified as no signature was found.' ),
'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
'filename' => $filename_for_errors,
$trusted_keys = wp_trusted_keys();
$file_hash = hash_file( 'sha384', $filename, true );
mbstring_binary_safe_encoding();
foreach ( (array) $signatures as $signature ) {
$signature_raw = base64_decode( $signature );
// Ensure only valid-length signatures are considered.
if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $signature_raw ) ) {
foreach ( (array) $trusted_keys as $key ) {
$key_raw = base64_decode( $key );
// Only pass valid public keys through.
if ( SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES !== strlen( $key_raw ) ) {
if ( sodium_crypto_sign_verify_detached( $signature_raw, $file_hash, $key_raw ) ) {
reset_mbstring_encoding();
reset_mbstring_encoding();
'signature_verification_failed',
/* translators: %s: The filename of the package. */
__( 'The authenticity of %s could not be verified.' ),
'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
// Error data helpful for debugging:
'filename' => $filename_for_errors,
'signatures' => $signatures,
'hash' => bin2hex( $file_hash ),
'skipped_key' => $skipped_key,
'skipped_sig' => $skipped_signature,
// phpcs:ignore PHPCompatibility.Constants.NewConstants.sodium_library_versionFound
'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ),
* Retrieves the list of signing keys trusted by WordPress.
* @return string[] Array of base64-encoded signing keys.
function wp_trusted_keys() {
if ( time() < 1617235200 ) {
// WordPress.org Key #1 - This key is only valid before April 1st, 2021.
$trusted_keys[] = 'fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0=';
// TODO: Add key #2 with longer expiration.
* Filters the valid signing keys used to verify the contents of files.
* @param string[] $trusted_keys The trusted keys that may sign packages.
return apply_filters( 'wp_trusted_keys', $trusted_keys );
* Unzips a specified ZIP file to a location on the filesystem via the WordPress
* Filesystem Abstraction.
* Assumes that WP_Filesystem() has already been called and set up. Does not extract
* a root-level __MACOSX directory, if present.
* Attempts to increase the PHP memory limit to 256M before uncompressing. However,
* the most memory required shouldn't be much larger than the archive itself.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @param string $file Full path and filename of ZIP archive.
* @param string $to Full path on the filesystem to extract archive to.
* @return true|WP_Error True on success, WP_Error on failure.
function unzip_file( $file, $to ) {
if ( ! $wp_filesystem || ! is_object( $wp_filesystem ) ) {
return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
// Unzip can use a lot of memory, but not this much hopefully.
wp_raise_memory_limit( 'admin' );
$to = trailingslashit( $to );
// Determine any parent directories needed (of the upgrade directory).
if ( ! $wp_filesystem->is_dir( $to ) ) { // Only do parents if no children exist.
$path = preg_split( '![/\\\]!', untrailingslashit( $to ) );
for ( $i = count( $path ); $i >= 0; $i-- ) {
if ( empty( $path[ $i ] ) ) {
$dir = implode( '/', array_slice( $path, 0, $i + 1 ) );
if ( preg_match( '!^[a-z]:$!i', $dir ) ) { // Skip it if it looks like a Windows Drive letter.
if ( ! $wp_filesystem->is_dir( $dir ) ) {
break; // A folder exists, therefore we don't need to check the levels below this.
* Filters whether to use ZipArchive to unzip archives.
* @param bool $ziparchive Whether to use ZipArchive. Default true.
if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
$result = _unzip_file_ziparchive( $file, $to, $needed_dirs );
if ( true === $result ) {