: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* the destination folder already exists. Default true.
* @type array $hook_extra Extra arguments to pass to the filter hooks called by
* WP_Upgrader::install_package(). Default empty array.
* @return array|WP_Error The result (also stored in `WP_Upgrader::$result`), or a WP_Error on failure.
public function install_package( $args = array() ) {
global $wp_filesystem, $wp_theme_directories;
'source' => '', // Please always pass this.
'destination' => '', // ...and this.
'clear_destination' => false,
'clear_working' => false,
'abort_if_destination_exists' => true,
$args = wp_parse_args( $args, $defaults );
// These were previously extract()'d.
$source = $args['source'];
$destination = $args['destination'];
$clear_destination = $args['clear_destination'];
if ( function_exists( 'set_time_limit' ) ) {
( ! is_string( $source ) || '' === $source || trim( $source ) !== $source ) ||
( ! is_string( $destination ) || '' === $destination || trim( $destination ) !== $destination )
return new WP_Error( 'bad_request', $this->strings['bad_request'] );
$this->skin->feedback( 'installing_package' );
* Filters the installation response before the installation has started.
* Returning a value that could be evaluated as a `WP_Error` will effectively
* short-circuit the installation, returning that value instead.
* @param bool|WP_Error $response Installation response.
* @param array $hook_extra Extra arguments passed to hooked filters.
$res = apply_filters( 'upgrader_pre_install', true, $args['hook_extra'] );
if ( is_wp_error( $res ) ) {
// Retain the original source and destinations.
$remote_source = $args['source'];
$local_destination = $destination;
$source_files = array_keys( $wp_filesystem->dirlist( $remote_source ) );
$remote_destination = $wp_filesystem->find_folder( $local_destination );
// Locate which directory to copy to the new folder. This is based on the actual folder holding the files.
if ( 1 === count( $source_files ) && $wp_filesystem->is_dir( trailingslashit( $args['source'] ) . $source_files[0] . '/' ) ) {
// Only one folder? Then we want its contents.
$source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] );
} elseif ( 0 === count( $source_files ) ) {
return new WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] );
* It's only a single file, the upgrader will use the folder name of this file as the destination folder.
* Folder name is based on zip filename.
$source = trailingslashit( $args['source'] );
* Filters the source file location for the upgrade package.
* @since 4.4.0 The $hook_extra parameter became available.
* @param string $source File source location.
* @param string $remote_source Remote file source location.
* @param WP_Upgrader $upgrader WP_Upgrader instance.
* @param array $hook_extra Extra arguments passed to hooked filters.
$source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] );
if ( is_wp_error( $source ) ) {
if ( ! empty( $args['hook_extra']['temp_backup'] ) ) {
$temp_backup = $this->move_to_temp_backup_dir( $args['hook_extra']['temp_backup'] );
if ( is_wp_error( $temp_backup ) ) {
$this->temp_backups[] = $args['hook_extra']['temp_backup'];
// Has the source location changed? If so, we need a new source_files list.
if ( $source !== $remote_source ) {
$source_files = array_keys( $wp_filesystem->dirlist( $source ) );
* Protection against deleting files in any important base directories.
* Theme_Upgrader & Plugin_Upgrader also trigger this, as they pass the
* destination directory (WP_PLUGIN_DIR / wp-content/themes) intending
* to copy the directory into the directory, whilst they pass the source
* as the actual files to copy.
$protected_directories = array( ABSPATH, WP_CONTENT_DIR, WP_PLUGIN_DIR, WP_CONTENT_DIR . '/themes' );
if ( is_array( $wp_theme_directories ) ) {
$protected_directories = array_merge( $protected_directories, $wp_theme_directories );
if ( in_array( $destination, $protected_directories, true ) ) {
$remote_destination = trailingslashit( $remote_destination ) . trailingslashit( basename( $source ) );
$destination = trailingslashit( $destination ) . trailingslashit( basename( $source ) );
if ( $clear_destination ) {
// We're going to clear the destination if there's something there.
$this->skin->feedback( 'remove_old' );
$removed = $this->clear_destination( $remote_destination );
* Filters whether the upgrader cleared the destination.
* @param true|WP_Error $removed Whether the destination was cleared.
* True upon success, WP_Error on failure.
* @param string $local_destination The local package destination.
* @param string $remote_destination The remote package destination.
* @param array $hook_extra Extra arguments passed to hooked filters.
$removed = apply_filters( 'upgrader_clear_destination', $removed, $local_destination, $remote_destination, $args['hook_extra'] );
if ( is_wp_error( $removed ) ) {
} elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) {
* If we're not clearing the destination folder and something exists there already, bail.
* But first check to see if there are actually any files in the folder.
$_files = $wp_filesystem->dirlist( $remote_destination );
if ( ! empty( $_files ) ) {
$wp_filesystem->delete( $remote_source, true ); // Clear out the source files.
return new WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination );
* If 'clear_working' is false, the source should not be removed, so use copy_dir() instead.
* Partial updates, like language packs, may want to retain the destination.
* If the destination exists or has contents, this may be a partial update,
* and the destination should not be removed, so use copy_dir() instead.
if ( $args['clear_working']
// Destination does not exist or has no contents.
! $wp_filesystem->exists( $remote_destination )
|| empty( $wp_filesystem->dirlist( $remote_destination ) )
$result = move_dir( $source, $remote_destination, true );
// Create destination if needed.
if ( ! $wp_filesystem->exists( $remote_destination ) ) {
if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination );
$result = copy_dir( $source, $remote_destination );
// Clear the working directory?
if ( $args['clear_working'] ) {
$wp_filesystem->delete( $remote_source, true );
if ( is_wp_error( $result ) ) {
$destination_name = basename( str_replace( $local_destination, '', $destination ) );
if ( '.' === $destination_name ) {
$this->result = compact( 'source', 'source_files', 'destination', 'destination_name', 'local_destination', 'remote_destination', 'clear_destination' );
* Filters the installation response after the installation has finished.
* @param bool $response Installation response.
* @param array $hook_extra Extra arguments passed to hooked filters.
* @param array $result Installation result data.
$res = apply_filters( 'upgrader_post_install', true, $args['hook_extra'], $this->result );
if ( is_wp_error( $res ) ) {
// Bombard the calling function will all the info which we've just used.
* Runs an upgrade/installation.
* Attempts to download the package (if it is not a local file), unpack it, and
* install it in the destination folder.
* @param array $options {
* Array or string of arguments for upgrading/installing a package.
* @type string $package The full path or URI of the package to install.
* @type string $destination The full path to the destination folder.
* @type bool $clear_destination Whether to delete any files already in the
* destination folder. Default false.
* @type bool $clear_working Whether to delete the files from the working
* directory after copying them to the destination.
* @type bool $abort_if_destination_exists Whether to abort the installation if the destination
* folder already exists. When true, `$clear_destination`
* should be false. Default true.
* @type bool $is_multi Whether this run is one of multiple upgrade/installation
* actions being performed in bulk. When true, the skin
* WP_Upgrader::header() and WP_Upgrader::footer()
* aren't called. Default false.
* @type array $hook_extra Extra arguments to pass to the filter hooks called by
* @return array|false|WP_Error The result from self::install_package() on success, otherwise a WP_Error,
* or false if unable to connect to the filesystem.
public function run( $options ) {
'package' => '', // Please always pass this.
'destination' => '', // ...and this.
'clear_destination' => false,
'abort_if_destination_exists' => true, // Abort if the destination directory exists. Pass clear_destination as false please.
'hook_extra' => array(), // Pass any extra $hook_extra args here, this will be passed to any hooked filters.
$options = wp_parse_args( $options, $defaults );
* Filters the package options before running an update.
* See also {@see 'upgrader_process_complete'}.
* @param array $options {
* Options used by the upgrader.
* @type string $package Package for update.
* @type string $destination Update location.
* @type bool $clear_destination Clear the destination resource.
* @type bool $clear_working Clear the working resource.
* @type bool $abort_if_destination_exists Abort if the Destination directory exists.
* @type bool $is_multi Whether the upgrader is running multiple times.
* @type array $hook_extra {
* @type string $action Type of action. Default 'update'.
* @type string $type Type of update process. Accepts 'plugin', 'theme', or 'core'.
* @type bool $bulk Whether the update process is a bulk update. Default true.
* @type string $plugin Path to the plugin file relative to the plugins directory.
* @type string $theme The stylesheet or template name of the theme.
* @type string $language_update_type The language pack update type. Accepts 'plugin', 'theme',
* @type object $language_update The language pack update offer.
$options = apply_filters( 'upgrader_package_options', $options );
if ( ! $options['is_multi'] ) { // Call $this->header separately if running multiple times.
// Connect to the filesystem first.
$res = $this->fs_connect( array( WP_CONTENT_DIR, $options['destination'] ) );
// Mainly for non-connected filesystem.
if ( ! $options['is_multi'] ) {
if ( is_wp_error( $res ) ) {
$this->skin->error( $res );
if ( ! $options['is_multi'] ) {
* Download the package. Note: If the package is the full path
* to an existing local file, it will be returned untouched.
$download = $this->download_package( $options['package'], false, $options['hook_extra'] );
* Allow for signature soft-fail.
* WARNING: This may be removed in the future.
if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
// Don't output the 'no signature could be found' failure message for now.
if ( 'signature_verification_no_signature' !== $download->get_error_code() || WP_DEBUG ) {
// Output the failure error as a normal feedback, and not as an error.
$this->skin->feedback( $download->get_error_message() );
// Report this failure back to WordPress.org for debugging purposes.
'signature_failure_code' => $download->get_error_code(),
'signature_failure_data' => $download->get_error_data(),
// Pretend this error didn't happen.
$download = $download->get_error_data( 'softfail-filename' );
if ( is_wp_error( $download ) ) {
$this->skin->error( $download );
if ( ! $options['is_multi'] ) {
$delete_package = ( $download !== $options['package'] ); // Do not delete a "local" file.
// Unzips the file into a temporary directory.
$working_dir = $this->unpack_package( $download, $delete_package );
if ( is_wp_error( $working_dir ) ) {
$this->skin->error( $working_dir );
if ( ! $options['is_multi'] ) {
// With the given options, this installs it to the destination directory.
$result = $this->install_package(
'source' => $working_dir,
'destination' => $options['destination'],
'clear_destination' => $options['clear_destination'],
'abort_if_destination_exists' => $options['abort_if_destination_exists'],
'clear_working' => $options['clear_working'],
'hook_extra' => $options['hook_extra'],
* Filters the result of WP_Upgrader::install_package().
* @param array|WP_Error $result Result from WP_Upgrader::install_package().
* @param array $hook_extra Extra arguments passed to hooked filters.
$result = apply_filters( 'upgrader_install_package_result', $result, $options['hook_extra'] );
$this->skin->set_result( $result );
if ( is_wp_error( $result ) ) {
// An automatic plugin update will have already performed its rollback.
if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
$this->temp_restores[] = $options['hook_extra']['temp_backup'];
* Restore the backup on shutdown.
* Actions running on `shutdown` are immune to PHP timeouts,
* so in case the failure was due to a PHP timeout,
* it will still be able to properly restore the previous version.
* Zero arguments are accepted as a string can sometimes be passed
* internally during actions, causing an error because
* `WP_Upgrader::restore_temp_backup()` expects an array.
add_action( 'shutdown', array( $this, 'restore_temp_backup' ), 10, 0 );
$this->skin->error( $result );
if ( ! method_exists( $this->skin, 'hide_process_failed' ) || ! $this->skin->hide_process_failed( $result ) ) {
$this->skin->feedback( 'process_failed' );
// Installation succeeded.
$this->skin->feedback( 'process_success' );
// Clean up the backup kept in the temporary backup directory.
if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
// Delete the backup on `shutdown` to avoid a PHP timeout.
add_action( 'shutdown', array( $this, 'delete_temp_backup' ), 100, 0 );
if ( ! $options['is_multi'] ) {
* Fires when the upgrader process is complete.
* See also {@see 'upgrader_package_options'}.
* @since 3.7.0 Added to WP_Upgrader::run().
* @since 4.6.0 `$translations` was added as a possible argument to `$hook_extra`.
* @param WP_Upgrader $upgrader WP_Upgrader instance. In other contexts this might be a
* Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance.
* @param array $hook_extra {
* Array of bulk item update data.
* @type string $action Type of action. Default 'update'.
* @type string $type Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
* @type bool $bulk Whether the update process is a bulk update. Default true.
* @type array $plugins Array of the basename paths of the plugins' main files.
* @type array $themes The theme slugs.
* @type array $translations {
* Array of translations update data.
* @type string $language The locale the translation is for.
* @type string $type Type of translation. Accepts 'plugin', 'theme', or 'core'.
* @type string $slug Text domain the translation is for. The slug of a theme/plugin or
* 'default' for core translations.
* @type string $version The version of a theme, plugin, or core.
do_action( 'upgrader_process_complete', $this, $options['hook_extra'] );
* Toggles maintenance mode for the site.
* Creates/deletes the maintenance file to enable/disable maintenance mode.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @param bool $enable True to enable maintenance mode, false to disable.
public function maintenance_mode( $enable = false ) {
if ( ! $wp_filesystem ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
$file = $wp_filesystem->abspath() . '.maintenance';
if ( ! wp_doing_cron() ) {
$this->skin->feedback( 'maintenance_start' );