balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-upgrader.php (36586B)


      1 <?php
      2 /**
      3  * Upgrade API: WP_Upgrader class
      4  *
      5  * Requires skin classes and WP_Upgrader subclasses for backward compatibility.
      6  *
      7  * @package WordPress
      8  * @subpackage Upgrader
      9  * @since 2.8.0
     10  */
     11 
     12 /** WP_Upgrader_Skin class */
     13 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';
     14 
     15 /** Plugin_Upgrader_Skin class */
     16 require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader-skin.php';
     17 
     18 /** Theme_Upgrader_Skin class */
     19 require_once ABSPATH . 'wp-admin/includes/class-theme-upgrader-skin.php';
     20 
     21 /** Bulk_Upgrader_Skin class */
     22 require_once ABSPATH . 'wp-admin/includes/class-bulk-upgrader-skin.php';
     23 
     24 /** Bulk_Plugin_Upgrader_Skin class */
     25 require_once ABSPATH . 'wp-admin/includes/class-bulk-plugin-upgrader-skin.php';
     26 
     27 /** Bulk_Theme_Upgrader_Skin class */
     28 require_once ABSPATH . 'wp-admin/includes/class-bulk-theme-upgrader-skin.php';
     29 
     30 /** Plugin_Installer_Skin class */
     31 require_once ABSPATH . 'wp-admin/includes/class-plugin-installer-skin.php';
     32 
     33 /** Theme_Installer_Skin class */
     34 require_once ABSPATH . 'wp-admin/includes/class-theme-installer-skin.php';
     35 
     36 /** Language_Pack_Upgrader_Skin class */
     37 require_once ABSPATH . 'wp-admin/includes/class-language-pack-upgrader-skin.php';
     38 
     39 /** Automatic_Upgrader_Skin class */
     40 require_once ABSPATH . 'wp-admin/includes/class-automatic-upgrader-skin.php';
     41 
     42 /** WP_Ajax_Upgrader_Skin class */
     43 require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
     44 
     45 /**
     46  * Core class used for upgrading/installing a local set of files via
     47  * the Filesystem Abstraction classes from a Zip file.
     48  *
     49  * @since 2.8.0
     50  */
     51 class WP_Upgrader {
     52 
     53 	/**
     54 	 * The error/notification strings used to update the user on the progress.
     55 	 *
     56 	 * @since 2.8.0
     57 	 * @var array $strings
     58 	 */
     59 	public $strings = array();
     60 
     61 	/**
     62 	 * The upgrader skin being used.
     63 	 *
     64 	 * @since 2.8.0
     65 	 * @var Automatic_Upgrader_Skin|WP_Upgrader_Skin $skin
     66 	 */
     67 	public $skin = null;
     68 
     69 	/**
     70 	 * The result of the installation.
     71 	 *
     72 	 * This is set by WP_Upgrader::install_package(), only when the package is installed
     73 	 * successfully. It will then be an array, unless a WP_Error is returned by the
     74 	 * {@see 'upgrader_post_install'} filter. In that case, the WP_Error will be assigned to
     75 	 * it.
     76 	 *
     77 	 * @since 2.8.0
     78 	 *
     79 	 * @var array|WP_Error $result {
     80 	 *     @type string $source             The full path to the source the files were installed from.
     81 	 *     @type string $source_files       List of all the files in the source directory.
     82 	 *     @type string $destination        The full path to the installation destination folder.
     83 	 *     @type string $destination_name   The name of the destination folder, or empty if `$destination`
     84 	 *                                      and `$local_destination` are the same.
     85 	 *     @type string $local_destination  The full local path to the destination folder. This is usually
     86 	 *                                      the same as `$destination`.
     87 	 *     @type string $remote_destination The full remote path to the destination folder
     88 	 *                                      (i.e., from `$wp_filesystem`).
     89 	 *     @type bool   $clear_destination  Whether the destination folder was cleared.
     90 	 * }
     91 	 */
     92 	public $result = array();
     93 
     94 	/**
     95 	 * The total number of updates being performed.
     96 	 *
     97 	 * Set by the bulk update methods.
     98 	 *
     99 	 * @since 3.0.0
    100 	 * @var int $update_count
    101 	 */
    102 	public $update_count = 0;
    103 
    104 	/**
    105 	 * The current update if multiple updates are being performed.
    106 	 *
    107 	 * Used by the bulk update methods, and incremented for each update.
    108 	 *
    109 	 * @since 3.0.0
    110 	 * @var int
    111 	 */
    112 	public $update_current = 0;
    113 
    114 	/**
    115 	 * Construct the upgrader with a skin.
    116 	 *
    117 	 * @since 2.8.0
    118 	 *
    119 	 * @param WP_Upgrader_Skin $skin The upgrader skin to use. Default is a WP_Upgrader_Skin
    120 	 *                               instance.
    121 	 */
    122 	public function __construct( $skin = null ) {
    123 		if ( null === $skin ) {
    124 			$this->skin = new WP_Upgrader_Skin();
    125 		} else {
    126 			$this->skin = $skin;
    127 		}
    128 	}
    129 
    130 	/**
    131 	 * Initialize the upgrader.
    132 	 *
    133 	 * This will set the relationship between the skin being used and this upgrader,
    134 	 * and also add the generic strings to `WP_Upgrader::$strings`.
    135 	 *
    136 	 * @since 2.8.0
    137 	 */
    138 	public function init() {
    139 		$this->skin->set_upgrader( $this );
    140 		$this->generic_strings();
    141 	}
    142 
    143 	/**
    144 	 * Add the generic strings to WP_Upgrader::$strings.
    145 	 *
    146 	 * @since 2.8.0
    147 	 */
    148 	public function generic_strings() {
    149 		$this->strings['bad_request']       = __( 'Invalid data provided.' );
    150 		$this->strings['fs_unavailable']    = __( 'Could not access filesystem.' );
    151 		$this->strings['fs_error']          = __( 'Filesystem error.' );
    152 		$this->strings['fs_no_root_dir']    = __( 'Unable to locate WordPress root directory.' );
    153 		$this->strings['fs_no_content_dir'] = __( 'Unable to locate WordPress content directory (wp-content).' );
    154 		$this->strings['fs_no_plugins_dir'] = __( 'Unable to locate WordPress plugin directory.' );
    155 		$this->strings['fs_no_themes_dir']  = __( 'Unable to locate WordPress theme directory.' );
    156 		/* translators: %s: Directory name. */
    157 		$this->strings['fs_no_folder'] = __( 'Unable to locate needed folder (%s).' );
    158 
    159 		$this->strings['download_failed']      = __( 'Download failed.' );
    160 		$this->strings['installing_package']   = __( 'Installing the latest version&#8230;' );
    161 		$this->strings['no_files']             = __( 'The package contains no files.' );
    162 		$this->strings['folder_exists']        = __( 'Destination folder already exists.' );
    163 		$this->strings['mkdir_failed']         = __( 'Could not create directory.' );
    164 		$this->strings['incompatible_archive'] = __( 'The package could not be installed.' );
    165 		$this->strings['files_not_writable']   = __( 'The update cannot be installed because we will be unable to copy some files. This is usually due to inconsistent file permissions.' );
    166 
    167 		$this->strings['maintenance_start'] = __( 'Enabling Maintenance mode&#8230;' );
    168 		$this->strings['maintenance_end']   = __( 'Disabling Maintenance mode&#8230;' );
    169 	}
    170 
    171 	/**
    172 	 * Connect to the filesystem.
    173 	 *
    174 	 * @since 2.8.0
    175 	 *
    176 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
    177 	 *
    178 	 * @param string[] $directories                  Optional. Array of directories. If any of these do
    179 	 *                                               not exist, a WP_Error object will be returned.
    180 	 *                                               Default empty array.
    181 	 * @param bool     $allow_relaxed_file_ownership Whether to allow relaxed file ownership.
    182 	 *                                               Default false.
    183 	 * @return bool|WP_Error True if able to connect, false or a WP_Error otherwise.
    184 	 */
    185 	public function fs_connect( $directories = array(), $allow_relaxed_file_ownership = false ) {
    186 		global $wp_filesystem;
    187 
    188 		$credentials = $this->skin->request_filesystem_credentials( false, $directories[0], $allow_relaxed_file_ownership );
    189 		if ( false === $credentials ) {
    190 			return false;
    191 		}
    192 
    193 		if ( ! WP_Filesystem( $credentials, $directories[0], $allow_relaxed_file_ownership ) ) {
    194 			$error = true;
    195 			if ( is_object( $wp_filesystem ) && $wp_filesystem->errors->has_errors() ) {
    196 				$error = $wp_filesystem->errors;
    197 			}
    198 			// Failed to connect. Error and request again.
    199 			$this->skin->request_filesystem_credentials( $error, $directories[0], $allow_relaxed_file_ownership );
    200 			return false;
    201 		}
    202 
    203 		if ( ! is_object( $wp_filesystem ) ) {
    204 			return new WP_Error( 'fs_unavailable', $this->strings['fs_unavailable'] );
    205 		}
    206 
    207 		if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
    208 			return new WP_Error( 'fs_error', $this->strings['fs_error'], $wp_filesystem->errors );
    209 		}
    210 
    211 		foreach ( (array) $directories as $dir ) {
    212 			switch ( $dir ) {
    213 				case ABSPATH:
    214 					if ( ! $wp_filesystem->abspath() ) {
    215 						return new WP_Error( 'fs_no_root_dir', $this->strings['fs_no_root_dir'] );
    216 					}
    217 					break;
    218 				case WP_CONTENT_DIR:
    219 					if ( ! $wp_filesystem->wp_content_dir() ) {
    220 						return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
    221 					}
    222 					break;
    223 				case WP_PLUGIN_DIR:
    224 					if ( ! $wp_filesystem->wp_plugins_dir() ) {
    225 						return new WP_Error( 'fs_no_plugins_dir', $this->strings['fs_no_plugins_dir'] );
    226 					}
    227 					break;
    228 				case get_theme_root():
    229 					if ( ! $wp_filesystem->wp_themes_dir() ) {
    230 						return new WP_Error( 'fs_no_themes_dir', $this->strings['fs_no_themes_dir'] );
    231 					}
    232 					break;
    233 				default:
    234 					if ( ! $wp_filesystem->find_folder( $dir ) ) {
    235 						return new WP_Error( 'fs_no_folder', sprintf( $this->strings['fs_no_folder'], esc_html( basename( $dir ) ) ) );
    236 					}
    237 					break;
    238 			}
    239 		}
    240 		return true;
    241 	}
    242 
    243 	/**
    244 	 * Download a package.
    245 	 *
    246 	 * @since 2.8.0
    247 	 * @since 5.5.0 Added the `$hook_extra` parameter.
    248 	 *
    249 	 * @param string $package          The URI of the package. If this is the full path to an
    250 	 *                                 existing local file, it will be returned untouched.
    251 	 * @param bool   $check_signatures Whether to validate file signatures. Default false.
    252 	 * @param array  $hook_extra       Extra arguments to pass to the filter hooks. Default empty array.
    253 	 * @return string|WP_Error The full path to the downloaded package file, or a WP_Error object.
    254 	 */
    255 	public function download_package( $package, $check_signatures = false, $hook_extra = array() ) {
    256 		/**
    257 		 * Filters whether to return the package.
    258 		 *
    259 		 * @since 3.7.0
    260 		 * @since 5.5.0 Added the `$hook_extra` parameter.
    261 		 *
    262 		 * @param bool        $reply      Whether to bail without returning the package.
    263 		 *                                Default false.
    264 		 * @param string      $package    The package file name.
    265 		 * @param WP_Upgrader $upgrader   The WP_Upgrader instance.
    266 		 * @param array       $hook_extra Extra arguments passed to hooked filters.
    267 		 */
    268 		$reply = apply_filters( 'upgrader_pre_download', false, $package, $this, $hook_extra );
    269 		if ( false !== $reply ) {
    270 			return $reply;
    271 		}
    272 
    273 		if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) { // Local file or remote?
    274 			return $package; // Must be a local file.
    275 		}
    276 
    277 		if ( empty( $package ) ) {
    278 			return new WP_Error( 'no_package', $this->strings['no_package'] );
    279 		}
    280 
    281 		$this->skin->feedback( 'downloading_package', $package );
    282 
    283 		$download_file = download_url( $package, 300, $check_signatures );
    284 
    285 		if ( is_wp_error( $download_file ) && ! $download_file->get_error_data( 'softfail-filename' ) ) {
    286 			return new WP_Error( 'download_failed', $this->strings['download_failed'], $download_file->get_error_message() );
    287 		}
    288 
    289 		return $download_file;
    290 	}
    291 
    292 	/**
    293 	 * Unpack a compressed package file.
    294 	 *
    295 	 * @since 2.8.0
    296 	 *
    297 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
    298 	 *
    299 	 * @param string $package        Full path to the package file.
    300 	 * @param bool   $delete_package Optional. Whether to delete the package file after attempting
    301 	 *                               to unpack it. Default true.
    302 	 * @return string|WP_Error The path to the unpacked contents, or a WP_Error on failure.
    303 	 */
    304 	public function unpack_package( $package, $delete_package = true ) {
    305 		global $wp_filesystem;
    306 
    307 		$this->skin->feedback( 'unpack_package' );
    308 
    309 		$upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
    310 
    311 		// Clean up contents of upgrade directory beforehand.
    312 		$upgrade_files = $wp_filesystem->dirlist( $upgrade_folder );
    313 		if ( ! empty( $upgrade_files ) ) {
    314 			foreach ( $upgrade_files as $file ) {
    315 				$wp_filesystem->delete( $upgrade_folder . $file['name'], true );
    316 			}
    317 		}
    318 
    319 		// We need a working directory - strip off any .tmp or .zip suffixes.
    320 		$working_dir = $upgrade_folder . basename( basename( $package, '.tmp' ), '.zip' );
    321 
    322 		// Clean up working directory.
    323 		if ( $wp_filesystem->is_dir( $working_dir ) ) {
    324 			$wp_filesystem->delete( $working_dir, true );
    325 		}
    326 
    327 		// Unzip package to working directory.
    328 		$result = unzip_file( $package, $working_dir );
    329 
    330 		// Once extracted, delete the package if required.
    331 		if ( $delete_package ) {
    332 			unlink( $package );
    333 		}
    334 
    335 		if ( is_wp_error( $result ) ) {
    336 			$wp_filesystem->delete( $working_dir, true );
    337 			if ( 'incompatible_archive' === $result->get_error_code() ) {
    338 				return new WP_Error( 'incompatible_archive', $this->strings['incompatible_archive'], $result->get_error_data() );
    339 			}
    340 			return $result;
    341 		}
    342 
    343 		return $working_dir;
    344 	}
    345 
    346 	/**
    347 	 * Flatten the results of WP_Filesystem_Base::dirlist() for iterating over.
    348 	 *
    349 	 * @since 4.9.0
    350 	 * @access protected
    351 	 *
    352 	 * @param array  $nested_files Array of files as returned by WP_Filesystem_Base::dirlist().
    353 	 * @param string $path         Relative path to prepend to child nodes. Optional.
    354 	 * @return array A flattened array of the $nested_files specified.
    355 	 */
    356 	protected function flatten_dirlist( $nested_files, $path = '' ) {
    357 		$files = array();
    358 
    359 		foreach ( $nested_files as $name => $details ) {
    360 			$files[ $path . $name ] = $details;
    361 
    362 			// Append children recursively.
    363 			if ( ! empty( $details['files'] ) ) {
    364 				$children = $this->flatten_dirlist( $details['files'], $path . $name . '/' );
    365 
    366 				// Merge keeping possible numeric keys, which array_merge() will reindex from 0..n.
    367 				$files = $files + $children;
    368 			}
    369 		}
    370 
    371 		return $files;
    372 	}
    373 
    374 	/**
    375 	 * Clears the directory where this item is going to be installed into.
    376 	 *
    377 	 * @since 4.3.0
    378 	 *
    379 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
    380 	 *
    381 	 * @param string $remote_destination The location on the remote filesystem to be cleared
    382 	 * @return bool|WP_Error True upon success, WP_Error on failure.
    383 	 */
    384 	public function clear_destination( $remote_destination ) {
    385 		global $wp_filesystem;
    386 
    387 		$files = $wp_filesystem->dirlist( $remote_destination, true, true );
    388 
    389 		// False indicates that the $remote_destination doesn't exist.
    390 		if ( false === $files ) {
    391 			return true;
    392 		}
    393 
    394 		// Flatten the file list to iterate over.
    395 		$files = $this->flatten_dirlist( $files );
    396 
    397 		// Check all files are writable before attempting to clear the destination.
    398 		$unwritable_files = array();
    399 
    400 		// Check writability.
    401 		foreach ( $files as $filename => $file_details ) {
    402 			if ( ! $wp_filesystem->is_writable( $remote_destination . $filename ) ) {
    403 				// Attempt to alter permissions to allow writes and try again.
    404 				$wp_filesystem->chmod( $remote_destination . $filename, ( 'd' === $file_details['type'] ? FS_CHMOD_DIR : FS_CHMOD_FILE ) );
    405 				if ( ! $wp_filesystem->is_writable( $remote_destination . $filename ) ) {
    406 					$unwritable_files[] = $filename;
    407 				}
    408 			}
    409 		}
    410 
    411 		if ( ! empty( $unwritable_files ) ) {
    412 			return new WP_Error( 'files_not_writable', $this->strings['files_not_writable'], implode( ', ', $unwritable_files ) );
    413 		}
    414 
    415 		if ( ! $wp_filesystem->delete( $remote_destination, true ) ) {
    416 			return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] );
    417 		}
    418 
    419 		return true;
    420 	}
    421 
    422 	/**
    423 	 * Install a package.
    424 	 *
    425 	 * Copies the contents of a package form a source directory, and installs them in
    426 	 * a destination directory. Optionally removes the source. It can also optionally
    427 	 * clear out the destination folder if it already exists.
    428 	 *
    429 	 * @since 2.8.0
    430 	 *
    431 	 * @global WP_Filesystem_Base $wp_filesystem        WordPress filesystem subclass.
    432 	 * @global array              $wp_theme_directories
    433 	 *
    434 	 * @param array|string $args {
    435 	 *     Optional. Array or string of arguments for installing a package. Default empty array.
    436 	 *
    437 	 *     @type string $source                      Required path to the package source. Default empty.
    438 	 *     @type string $destination                 Required path to a folder to install the package in.
    439 	 *                                               Default empty.
    440 	 *     @type bool   $clear_destination           Whether to delete any files already in the destination
    441 	 *                                               folder. Default false.
    442 	 *     @type bool   $clear_working               Whether to delete the files form the working directory
    443 	 *                                               after copying to the destination. Default false.
    444 	 *     @type bool   $abort_if_destination_exists Whether to abort the installation if
    445 	 *                                               the destination folder already exists. Default true.
    446 	 *     @type array  $hook_extra                  Extra arguments to pass to the filter hooks called by
    447 	 *                                               WP_Upgrader::install_package(). Default empty array.
    448 	 * }
    449 	 *
    450 	 * @return array|WP_Error The result (also stored in `WP_Upgrader::$result`), or a WP_Error on failure.
    451 	 */
    452 	public function install_package( $args = array() ) {
    453 		global $wp_filesystem, $wp_theme_directories;
    454 
    455 		$defaults = array(
    456 			'source'                      => '', // Please always pass this.
    457 			'destination'                 => '', // ...and this.
    458 			'clear_destination'           => false,
    459 			'clear_working'               => false,
    460 			'abort_if_destination_exists' => true,
    461 			'hook_extra'                  => array(),
    462 		);
    463 
    464 		$args = wp_parse_args( $args, $defaults );
    465 
    466 		// These were previously extract()'d.
    467 		$source            = $args['source'];
    468 		$destination       = $args['destination'];
    469 		$clear_destination = $args['clear_destination'];
    470 
    471 		set_time_limit( 300 );
    472 
    473 		if ( empty( $source ) || empty( $destination ) ) {
    474 			return new WP_Error( 'bad_request', $this->strings['bad_request'] );
    475 		}
    476 		$this->skin->feedback( 'installing_package' );
    477 
    478 		/**
    479 		 * Filters the install response before the installation has started.
    480 		 *
    481 		 * Returning a truthy value, or one that could be evaluated as a WP_Error
    482 		 * will effectively short-circuit the installation, returning that value
    483 		 * instead.
    484 		 *
    485 		 * @since 2.8.0
    486 		 *
    487 		 * @param bool|WP_Error $response   Response.
    488 		 * @param array         $hook_extra Extra arguments passed to hooked filters.
    489 		 */
    490 		$res = apply_filters( 'upgrader_pre_install', true, $args['hook_extra'] );
    491 
    492 		if ( is_wp_error( $res ) ) {
    493 			return $res;
    494 		}
    495 
    496 		// Retain the original source and destinations.
    497 		$remote_source     = $args['source'];
    498 		$local_destination = $destination;
    499 
    500 		$source_files       = array_keys( $wp_filesystem->dirlist( $remote_source ) );
    501 		$remote_destination = $wp_filesystem->find_folder( $local_destination );
    502 
    503 		// Locate which directory to copy to the new folder. This is based on the actual folder holding the files.
    504 		if ( 1 === count( $source_files ) && $wp_filesystem->is_dir( trailingslashit( $args['source'] ) . $source_files[0] . '/' ) ) {
    505 			// Only one folder? Then we want its contents.
    506 			$source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] );
    507 		} elseif ( 0 === count( $source_files ) ) {
    508 			// There are no files?
    509 			return new WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] );
    510 		} else {
    511 			// It's only a single file, the upgrader will use the folder name of this file as the destination folder.
    512 			// Folder name is based on zip filename.
    513 			$source = trailingslashit( $args['source'] );
    514 		}
    515 
    516 		/**
    517 		 * Filters the source file location for the upgrade package.
    518 		 *
    519 		 * @since 2.8.0
    520 		 * @since 4.4.0 The $hook_extra parameter became available.
    521 		 *
    522 		 * @param string      $source        File source location.
    523 		 * @param string      $remote_source Remote file source location.
    524 		 * @param WP_Upgrader $upgrader      WP_Upgrader instance.
    525 		 * @param array       $hook_extra    Extra arguments passed to hooked filters.
    526 		 */
    527 		$source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] );
    528 
    529 		if ( is_wp_error( $source ) ) {
    530 			return $source;
    531 		}
    532 
    533 		// Has the source location changed? If so, we need a new source_files list.
    534 		if ( $source !== $remote_source ) {
    535 			$source_files = array_keys( $wp_filesystem->dirlist( $source ) );
    536 		}
    537 
    538 		/*
    539 		 * Protection against deleting files in any important base directories.
    540 		 * Theme_Upgrader & Plugin_Upgrader also trigger this, as they pass the
    541 		 * destination directory (WP_PLUGIN_DIR / wp-content/themes) intending
    542 		 * to copy the directory into the directory, whilst they pass the source
    543 		 * as the actual files to copy.
    544 		 */
    545 		$protected_directories = array( ABSPATH, WP_CONTENT_DIR, WP_PLUGIN_DIR, WP_CONTENT_DIR . '/themes' );
    546 
    547 		if ( is_array( $wp_theme_directories ) ) {
    548 			$protected_directories = array_merge( $protected_directories, $wp_theme_directories );
    549 		}
    550 
    551 		if ( in_array( $destination, $protected_directories, true ) ) {
    552 			$remote_destination = trailingslashit( $remote_destination ) . trailingslashit( basename( $source ) );
    553 			$destination        = trailingslashit( $destination ) . trailingslashit( basename( $source ) );
    554 		}
    555 
    556 		if ( $clear_destination ) {
    557 			// We're going to clear the destination if there's something there.
    558 			$this->skin->feedback( 'remove_old' );
    559 
    560 			$removed = $this->clear_destination( $remote_destination );
    561 
    562 			/**
    563 			 * Filters whether the upgrader cleared the destination.
    564 			 *
    565 			 * @since 2.8.0
    566 			 *
    567 			 * @param true|WP_Error $removed            Whether the destination was cleared. true upon success, WP_Error on failure.
    568 			 * @param string        $local_destination  The local package destination.
    569 			 * @param string        $remote_destination The remote package destination.
    570 			 * @param array         $hook_extra         Extra arguments passed to hooked filters.
    571 			 */
    572 			$removed = apply_filters( 'upgrader_clear_destination', $removed, $local_destination, $remote_destination, $args['hook_extra'] );
    573 
    574 			if ( is_wp_error( $removed ) ) {
    575 				return $removed;
    576 			}
    577 		} elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) {
    578 			// If we're not clearing the destination folder and something exists there already, bail.
    579 			// But first check to see if there are actually any files in the folder.
    580 			$_files = $wp_filesystem->dirlist( $remote_destination );
    581 			if ( ! empty( $_files ) ) {
    582 				$wp_filesystem->delete( $remote_source, true ); // Clear out the source files.
    583 				return new WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination );
    584 			}
    585 		}
    586 
    587 		// Create destination if needed.
    588 		if ( ! $wp_filesystem->exists( $remote_destination ) ) {
    589 			if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
    590 				return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination );
    591 			}
    592 		}
    593 
    594 		// Copy new version of item into place.
    595 		$result = copy_dir( $source, $remote_destination );
    596 		if ( is_wp_error( $result ) ) {
    597 			if ( $args['clear_working'] ) {
    598 				$wp_filesystem->delete( $remote_source, true );
    599 			}
    600 			return $result;
    601 		}
    602 
    603 		// Clear the working folder?
    604 		if ( $args['clear_working'] ) {
    605 			$wp_filesystem->delete( $remote_source, true );
    606 		}
    607 
    608 		$destination_name = basename( str_replace( $local_destination, '', $destination ) );
    609 		if ( '.' === $destination_name ) {
    610 			$destination_name = '';
    611 		}
    612 
    613 		$this->result = compact( 'source', 'source_files', 'destination', 'destination_name', 'local_destination', 'remote_destination', 'clear_destination' );
    614 
    615 		/**
    616 		 * Filters the installation response after the installation has finished.
    617 		 *
    618 		 * @since 2.8.0
    619 		 *
    620 		 * @param bool  $response   Installation response.
    621 		 * @param array $hook_extra Extra arguments passed to hooked filters.
    622 		 * @param array $result     Installation result data.
    623 		 */
    624 		$res = apply_filters( 'upgrader_post_install', true, $args['hook_extra'], $this->result );
    625 
    626 		if ( is_wp_error( $res ) ) {
    627 			$this->result = $res;
    628 			return $res;
    629 		}
    630 
    631 		// Bombard the calling function will all the info which we've just used.
    632 		return $this->result;
    633 	}
    634 
    635 	/**
    636 	 * Run an upgrade/installation.
    637 	 *
    638 	 * Attempts to download the package (if it is not a local file), unpack it, and
    639 	 * install it in the destination folder.
    640 	 *
    641 	 * @since 2.8.0
    642 	 *
    643 	 * @param array $options {
    644 	 *     Array or string of arguments for upgrading/installing a package.
    645 	 *
    646 	 *     @type string $package                     The full path or URI of the package to install.
    647 	 *                                               Default empty.
    648 	 *     @type string $destination                 The full path to the destination folder.
    649 	 *                                               Default empty.
    650 	 *     @type bool   $clear_destination           Whether to delete any files already in the
    651 	 *                                               destination folder. Default false.
    652 	 *     @type bool   $clear_working               Whether to delete the files form the working
    653 	 *                                               directory after copying to the destination.
    654 	 *                                               Default false.
    655 	 *     @type bool   $abort_if_destination_exists Whether to abort the installation if the destination
    656 	 *                                               folder already exists. When true, `$clear_destination`
    657 	 *                                               should be false. Default true.
    658 	 *     @type bool   $is_multi                    Whether this run is one of multiple upgrade/installation
    659 	 *                                               actions being performed in bulk. When true, the skin
    660 	 *                                               WP_Upgrader::header() and WP_Upgrader::footer()
    661 	 *                                               aren't called. Default false.
    662 	 *     @type array  $hook_extra                  Extra arguments to pass to the filter hooks called by
    663 	 *                                               WP_Upgrader::run().
    664 	 * }
    665 	 * @return array|false|WP_Error The result from self::install_package() on success, otherwise a WP_Error,
    666 	 *                              or false if unable to connect to the filesystem.
    667 	 */
    668 	public function run( $options ) {
    669 
    670 		$defaults = array(
    671 			'package'                     => '', // Please always pass this.
    672 			'destination'                 => '', // ...and this.
    673 			'clear_destination'           => false,
    674 			'abort_if_destination_exists' => true, // Abort if the destination directory exists. Pass clear_destination as false please.
    675 			'clear_working'               => true,
    676 			'is_multi'                    => false,
    677 			'hook_extra'                  => array(), // Pass any extra $hook_extra args here, this will be passed to any hooked filters.
    678 		);
    679 
    680 		$options = wp_parse_args( $options, $defaults );
    681 
    682 		/**
    683 		 * Filters the package options before running an update.
    684 		 *
    685 		 * See also {@see 'upgrader_process_complete'}.
    686 		 *
    687 		 * @since 4.3.0
    688 		 *
    689 		 * @param array $options {
    690 		 *     Options used by the upgrader.
    691 		 *
    692 		 *     @type string $package                     Package for update.
    693 		 *     @type string $destination                 Update location.
    694 		 *     @type bool   $clear_destination           Clear the destination resource.
    695 		 *     @type bool   $clear_working               Clear the working resource.
    696 		 *     @type bool   $abort_if_destination_exists Abort if the Destination directory exists.
    697 		 *     @type bool   $is_multi                    Whether the upgrader is running multiple times.
    698 		 *     @type array  $hook_extra {
    699 		 *         Extra hook arguments.
    700 		 *
    701 		 *         @type string $action               Type of action. Default 'update'.
    702 		 *         @type string $type                 Type of update process. Accepts 'plugin', 'theme', or 'core'.
    703 		 *         @type bool   $bulk                 Whether the update process is a bulk update. Default true.
    704 		 *         @type string $plugin               Path to the plugin file relative to the plugins directory.
    705 		 *         @type string $theme                The stylesheet or template name of the theme.
    706 		 *         @type string $language_update_type The language pack update type. Accepts 'plugin', 'theme',
    707 		 *                                            or 'core'.
    708 		 *         @type object $language_update      The language pack update offer.
    709 		 *     }
    710 		 * }
    711 		 */
    712 		$options = apply_filters( 'upgrader_package_options', $options );
    713 
    714 		if ( ! $options['is_multi'] ) { // Call $this->header separately if running multiple times.
    715 			$this->skin->header();
    716 		}
    717 
    718 		// Connect to the filesystem first.
    719 		$res = $this->fs_connect( array( WP_CONTENT_DIR, $options['destination'] ) );
    720 		// Mainly for non-connected filesystem.
    721 		if ( ! $res ) {
    722 			if ( ! $options['is_multi'] ) {
    723 				$this->skin->footer();
    724 			}
    725 			return false;
    726 		}
    727 
    728 		$this->skin->before();
    729 
    730 		if ( is_wp_error( $res ) ) {
    731 			$this->skin->error( $res );
    732 			$this->skin->after();
    733 			if ( ! $options['is_multi'] ) {
    734 				$this->skin->footer();
    735 			}
    736 			return $res;
    737 		}
    738 
    739 		/*
    740 		 * Download the package (Note, This just returns the filename
    741 		 * of the file if the package is a local file)
    742 		 */
    743 		$download = $this->download_package( $options['package'], true, $options['hook_extra'] );
    744 
    745 		// Allow for signature soft-fail.
    746 		// WARNING: This may be removed in the future.
    747 		if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
    748 
    749 			// Don't output the 'no signature could be found' failure message for now.
    750 			if ( 'signature_verification_no_signature' !== $download->get_error_code() || WP_DEBUG ) {
    751 				// Output the failure error as a normal feedback, and not as an error.
    752 				$this->skin->feedback( $download->get_error_message() );
    753 
    754 				// Report this failure back to WordPress.org for debugging purposes.
    755 				wp_version_check(
    756 					array(
    757 						'signature_failure_code' => $download->get_error_code(),
    758 						'signature_failure_data' => $download->get_error_data(),
    759 					)
    760 				);
    761 			}
    762 
    763 			// Pretend this error didn't happen.
    764 			$download = $download->get_error_data( 'softfail-filename' );
    765 		}
    766 
    767 		if ( is_wp_error( $download ) ) {
    768 			$this->skin->error( $download );
    769 			$this->skin->after();
    770 			if ( ! $options['is_multi'] ) {
    771 				$this->skin->footer();
    772 			}
    773 			return $download;
    774 		}
    775 
    776 		$delete_package = ( $download !== $options['package'] ); // Do not delete a "local" file.
    777 
    778 		// Unzips the file into a temporary directory.
    779 		$working_dir = $this->unpack_package( $download, $delete_package );
    780 		if ( is_wp_error( $working_dir ) ) {
    781 			$this->skin->error( $working_dir );
    782 			$this->skin->after();
    783 			if ( ! $options['is_multi'] ) {
    784 				$this->skin->footer();
    785 			}
    786 			return $working_dir;
    787 		}
    788 
    789 		// With the given options, this installs it to the destination directory.
    790 		$result = $this->install_package(
    791 			array(
    792 				'source'                      => $working_dir,
    793 				'destination'                 => $options['destination'],
    794 				'clear_destination'           => $options['clear_destination'],
    795 				'abort_if_destination_exists' => $options['abort_if_destination_exists'],
    796 				'clear_working'               => $options['clear_working'],
    797 				'hook_extra'                  => $options['hook_extra'],
    798 			)
    799 		);
    800 
    801 		/**
    802 		 * Filters the result of WP_Upgrader::install_package().
    803 		 *
    804 		 * @since 5.7.0
    805 		 *
    806 		 * @param array|WP_Error $result     Result from WP_Upgrader::install_package().
    807 		 * @param array          $hook_extra Extra arguments passed to hooked filters.
    808 		 */
    809 		$result = apply_filters( 'upgrader_install_package_result', $result, $options['hook_extra'] );
    810 
    811 		$this->skin->set_result( $result );
    812 		if ( is_wp_error( $result ) ) {
    813 			$this->skin->error( $result );
    814 
    815 			if ( ! method_exists( $this->skin, 'hide_process_failed' ) || ! $this->skin->hide_process_failed( $result ) ) {
    816 				$this->skin->feedback( 'process_failed' );
    817 			}
    818 		} else {
    819 			// Installation succeeded.
    820 			$this->skin->feedback( 'process_success' );
    821 		}
    822 
    823 		$this->skin->after();
    824 
    825 		if ( ! $options['is_multi'] ) {
    826 
    827 			/**
    828 			 * Fires when the upgrader process is complete.
    829 			 *
    830 			 * See also {@see 'upgrader_package_options'}.
    831 			 *
    832 			 * @since 3.6.0
    833 			 * @since 3.7.0 Added to WP_Upgrader::run().
    834 			 * @since 4.6.0 `$translations` was added as a possible argument to `$hook_extra`.
    835 			 *
    836 			 * @param WP_Upgrader $this WP_Upgrader instance. In other contexts, $this, might be a
    837 			 *                          Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance.
    838 			 * @param array       $hook_extra {
    839 			 *     Array of bulk item update data.
    840 			 *
    841 			 *     @type string $action       Type of action. Default 'update'.
    842 			 *     @type string $type         Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
    843 			 *     @type bool   $bulk         Whether the update process is a bulk update. Default true.
    844 			 *     @type array  $plugins      Array of the basename paths of the plugins' main files.
    845 			 *     @type array  $themes       The theme slugs.
    846 			 *     @type array  $translations {
    847 			 *         Array of translations update data.
    848 			 *
    849 			 *         @type string $language The locale the translation is for.
    850 			 *         @type string $type     Type of translation. Accepts 'plugin', 'theme', or 'core'.
    851 			 *         @type string $slug     Text domain the translation is for. The slug of a theme/plugin or
    852 			 *                                'default' for core translations.
    853 			 *         @type string $version  The version of a theme, plugin, or core.
    854 			 *     }
    855 			 * }
    856 			 */
    857 			do_action( 'upgrader_process_complete', $this, $options['hook_extra'] );
    858 
    859 			$this->skin->footer();
    860 		}
    861 
    862 		return $result;
    863 	}
    864 
    865 	/**
    866 	 * Toggle maintenance mode for the site.
    867 	 *
    868 	 * Creates/deletes the maintenance file to enable/disable maintenance mode.
    869 	 *
    870 	 * @since 2.8.0
    871 	 *
    872 	 * @global WP_Filesystem_Base $wp_filesystem Subclass
    873 	 *
    874 	 * @param bool $enable True to enable maintenance mode, false to disable.
    875 	 */
    876 	public function maintenance_mode( $enable = false ) {
    877 		global $wp_filesystem;
    878 		$file = $wp_filesystem->abspath() . '.maintenance';
    879 		if ( $enable ) {
    880 			$this->skin->feedback( 'maintenance_start' );
    881 			// Create maintenance file to signal that we are upgrading.
    882 			$maintenance_string = '<?php $upgrading = ' . time() . '; ?>';
    883 			$wp_filesystem->delete( $file );
    884 			$wp_filesystem->put_contents( $file, $maintenance_string, FS_CHMOD_FILE );
    885 		} elseif ( ! $enable && $wp_filesystem->exists( $file ) ) {
    886 			$this->skin->feedback( 'maintenance_end' );
    887 			$wp_filesystem->delete( $file );
    888 		}
    889 	}
    890 
    891 	/**
    892 	 * Creates a lock using WordPress options.
    893 	 *
    894 	 * @since 4.5.0
    895 	 *
    896 	 * @param string $lock_name       The name of this unique lock.
    897 	 * @param int    $release_timeout Optional. The duration in seconds to respect an existing lock.
    898 	 *                                Default: 1 hour.
    899 	 * @return bool False if a lock couldn't be created or if the lock is still valid. True otherwise.
    900 	 */
    901 	public static function create_lock( $lock_name, $release_timeout = null ) {
    902 		global $wpdb;
    903 		if ( ! $release_timeout ) {
    904 			$release_timeout = HOUR_IN_SECONDS;
    905 		}
    906 		$lock_option = $lock_name . '.lock';
    907 
    908 		// Try to lock.
    909 		$lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_option, time() ) );
    910 
    911 		if ( ! $lock_result ) {
    912 			$lock_result = get_option( $lock_option );
    913 
    914 			// If a lock couldn't be created, and there isn't a lock, bail.
    915 			if ( ! $lock_result ) {
    916 				return false;
    917 			}
    918 
    919 			// Check to see if the lock is still valid. If it is, bail.
    920 			if ( $lock_result > ( time() - $release_timeout ) ) {
    921 				return false;
    922 			}
    923 
    924 			// There must exist an expired lock, clear it and re-gain it.
    925 			WP_Upgrader::release_lock( $lock_name );
    926 
    927 			return WP_Upgrader::create_lock( $lock_name, $release_timeout );
    928 		}
    929 
    930 		// Update the lock, as by this point we've definitely got a lock, just need to fire the actions.
    931 		update_option( $lock_option, time() );
    932 
    933 		return true;
    934 	}
    935 
    936 	/**
    937 	 * Releases an upgrader lock.
    938 	 *
    939 	 * @since 4.5.0
    940 	 *
    941 	 * @see WP_Upgrader::create_lock()
    942 	 *
    943 	 * @param string $lock_name The name of this unique lock.
    944 	 * @return bool True if the lock was successfully released. False on failure.
    945 	 */
    946 	public static function release_lock( $lock_name ) {
    947 		return delete_option( $lock_name . '.lock' );
    948 	}
    949 
    950 }
    951 
    952 /** Plugin_Upgrader class */
    953 require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
    954 
    955 /** Theme_Upgrader class */
    956 require_once ABSPATH . 'wp-admin/includes/class-theme-upgrader.php';
    957 
    958 /** Language_Pack_Upgrader class */
    959 require_once ABSPATH . 'wp-admin/includes/class-language-pack-upgrader.php';
    960 
    961 /** Core_Upgrader class */
    962 require_once ABSPATH . 'wp-admin/includes/class-core-upgrader.php';
    963 
    964 /** File_Upload_Upgrader class */
    965 require_once ABSPATH . 'wp-admin/includes/class-file-upload-upgrader.php';
    966 
    967 /** WP_Automatic_Updater class */
    968 require_once ABSPATH . 'wp-admin/includes/class-wp-automatic-updater.php';