wp/wp-admin/includes/class-wp-upgrader.php
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
    46  * Core class used for upgrading/installing a local set of files via
    46  * Core class used for upgrading/installing a local set of files via
    47  * the Filesystem Abstraction classes from a Zip file.
    47  * the Filesystem Abstraction classes from a Zip file.
    48  *
    48  *
    49  * @since 2.8.0
    49  * @since 2.8.0
    50  */
    50  */
       
    51 #[AllowDynamicProperties]
    51 class WP_Upgrader {
    52 class WP_Upgrader {
    52 
    53 
    53 	/**
    54 	/**
    54 	 * The error/notification strings used to update the user on the progress.
    55 	 * The error/notification strings used to update the user on the progress.
    55 	 *
    56 	 *
   110 	 * @var int
   111 	 * @var int
   111 	 */
   112 	 */
   112 	public $update_current = 0;
   113 	public $update_current = 0;
   113 
   114 
   114 	/**
   115 	/**
       
   116 	 * Stores the list of plugins or themes added to temporary backup directory.
       
   117 	 *
       
   118 	 * Used by the rollback functions.
       
   119 	 *
       
   120 	 * @since 6.3.0
       
   121 	 * @var array
       
   122 	 */
       
   123 	private $temp_backups = array();
       
   124 
       
   125 	/**
       
   126 	 * Stores the list of plugins or themes to be restored from temporary backup directory.
       
   127 	 *
       
   128 	 * Used by the rollback functions.
       
   129 	 *
       
   130 	 * @since 6.3.0
       
   131 	 * @var array
       
   132 	 */
       
   133 	private $temp_restores = array();
       
   134 
       
   135 	/**
   115 	 * Construct the upgrader with a skin.
   136 	 * Construct the upgrader with a skin.
   116 	 *
   137 	 *
   117 	 * @since 2.8.0
   138 	 * @since 2.8.0
   118 	 *
   139 	 *
   119 	 * @param WP_Upgrader_Skin $skin The upgrader skin to use. Default is a WP_Upgrader_Skin
   140 	 * @param WP_Upgrader_Skin $skin The upgrader skin to use. Default is a WP_Upgrader_Skin
   126 			$this->skin = $skin;
   147 			$this->skin = $skin;
   127 		}
   148 		}
   128 	}
   149 	}
   129 
   150 
   130 	/**
   151 	/**
   131 	 * Initialize the upgrader.
   152 	 * Initializes the upgrader.
   132 	 *
   153 	 *
   133 	 * This will set the relationship between the skin being used and this upgrader,
   154 	 * This will set the relationship between the skin being used and this upgrader,
   134 	 * and also add the generic strings to `WP_Upgrader::$strings`.
   155 	 * and also add the generic strings to `WP_Upgrader::$strings`.
   135 	 *
   156 	 *
       
   157 	 * Additionally, it will schedule a weekly task to clean up the temporary backup directory.
       
   158 	 *
   136 	 * @since 2.8.0
   159 	 * @since 2.8.0
       
   160 	 * @since 6.3.0 Added the `schedule_temp_backup_cleanup()` task.
   137 	 */
   161 	 */
   138 	public function init() {
   162 	public function init() {
   139 		$this->skin->set_upgrader( $this );
   163 		$this->skin->set_upgrader( $this );
   140 		$this->generic_strings();
   164 		$this->generic_strings();
   141 	}
   165 
   142 
   166 		if ( ! wp_installing() ) {
   143 	/**
   167 			$this->schedule_temp_backup_cleanup();
   144 	 * Add the generic strings to WP_Upgrader::$strings.
   168 		}
       
   169 	}
       
   170 
       
   171 	/**
       
   172 	 * Schedules the cleanup of the temporary backup directory.
       
   173 	 *
       
   174 	 * @since 6.3.0
       
   175 	 */
       
   176 	protected function schedule_temp_backup_cleanup() {
       
   177 		if ( false === wp_next_scheduled( 'wp_delete_temp_updater_backups' ) ) {
       
   178 			wp_schedule_event( time(), 'weekly', 'wp_delete_temp_updater_backups' );
       
   179 		}
       
   180 	}
       
   181 
       
   182 	/**
       
   183 	 * Adds the generic strings to WP_Upgrader::$strings.
   145 	 *
   184 	 *
   146 	 * @since 2.8.0
   185 	 * @since 2.8.0
   147 	 */
   186 	 */
   148 	public function generic_strings() {
   187 	public function generic_strings() {
   149 		$this->strings['bad_request']       = __( 'Invalid data provided.' );
   188 		$this->strings['bad_request']    = __( 'Invalid data provided.' );
   150 		$this->strings['fs_unavailable']    = __( 'Could not access filesystem.' );
   189 		$this->strings['fs_unavailable'] = __( 'Could not access filesystem.' );
   151 		$this->strings['fs_error']          = __( 'Filesystem error.' );
   190 		$this->strings['fs_error']       = __( 'Filesystem error.' );
   152 		$this->strings['fs_no_root_dir']    = __( 'Unable to locate WordPress root directory.' );
   191 		$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).' );
   192 		/* translators: %s: Directory name. */
       
   193 		$this->strings['fs_no_content_dir'] = sprintf( __( 'Unable to locate WordPress content directory (%s).' ), 'wp-content' );
   154 		$this->strings['fs_no_plugins_dir'] = __( 'Unable to locate WordPress plugin directory.' );
   194 		$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.' );
   195 		$this->strings['fs_no_themes_dir']  = __( 'Unable to locate WordPress theme directory.' );
   156 		/* translators: %s: Directory name. */
   196 		/* translators: %s: Directory name. */
   157 		$this->strings['fs_no_folder'] = __( 'Unable to locate needed folder (%s).' );
   197 		$this->strings['fs_no_folder'] = __( 'Unable to locate needed folder (%s).' );
   158 
   198 
       
   199 		$this->strings['no_package']           = __( 'Package not available.' );
   159 		$this->strings['download_failed']      = __( 'Download failed.' );
   200 		$this->strings['download_failed']      = __( 'Download failed.' );
   160 		$this->strings['installing_package']   = __( 'Installing the latest version…' );
   201 		$this->strings['installing_package']   = __( 'Installing the latest version…' );
   161 		$this->strings['no_files']             = __( 'The package contains no files.' );
   202 		$this->strings['no_files']             = __( 'The package contains no files.' );
   162 		$this->strings['folder_exists']        = __( 'Destination folder already exists.' );
   203 		$this->strings['folder_exists']        = __( 'Destination folder already exists.' );
   163 		$this->strings['mkdir_failed']         = __( 'Could not create directory.' );
   204 		$this->strings['mkdir_failed']         = __( 'Could not create directory.' );
   164 		$this->strings['incompatible_archive'] = __( 'The package could not be installed.' );
   205 		$this->strings['incompatible_archive'] = __( 'The package could not be installed.' );
   165 		$this->strings['files_not_writable']   = __( 'The update cannot be installed because some files could not be copied. This is usually due to inconsistent file permissions.' );
   206 		$this->strings['files_not_writable']   = __( 'The update cannot be installed because some files could not be copied. This is usually due to inconsistent file permissions.' );
   166 
   207 
   167 		$this->strings['maintenance_start'] = __( 'Enabling Maintenance mode…' );
   208 		$this->strings['maintenance_start'] = __( 'Enabling Maintenance mode…' );
   168 		$this->strings['maintenance_end']   = __( 'Disabling Maintenance mode…' );
   209 		$this->strings['maintenance_end']   = __( 'Disabling Maintenance mode…' );
   169 	}
   210 
   170 
   211 		/* translators: %s: upgrade-temp-backup */
   171 	/**
   212 		$this->strings['temp_backup_mkdir_failed'] = sprintf( __( 'Could not create the %s directory.' ), 'upgrade-temp-backup' );
   172 	 * Connect to the filesystem.
   213 		/* translators: %s: upgrade-temp-backup */
       
   214 		$this->strings['temp_backup_move_failed'] = sprintf( __( 'Could not move the old version to the %s directory.' ), 'upgrade-temp-backup' );
       
   215 		/* translators: %s: The plugin or theme slug. */
       
   216 		$this->strings['temp_backup_restore_failed'] = __( 'Could not restore the original version of %s.' );
       
   217 		/* translators: %s: The plugin or theme slug. */
       
   218 		$this->strings['temp_backup_delete_failed'] = __( 'Could not delete the temporary backup directory for %s.' );
       
   219 	}
       
   220 
       
   221 	/**
       
   222 	 * Connects to the filesystem.
   173 	 *
   223 	 *
   174 	 * @since 2.8.0
   224 	 * @since 2.8.0
   175 	 *
   225 	 *
   176 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
   226 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
   177 	 *
   227 	 *
   239 		}
   289 		}
   240 		return true;
   290 		return true;
   241 	}
   291 	}
   242 
   292 
   243 	/**
   293 	/**
   244 	 * Download a package.
   294 	 * Downloads a package.
   245 	 *
   295 	 *
   246 	 * @since 2.8.0
   296 	 * @since 2.8.0
   247 	 * @since 5.2.0 Added the `$check_signatures` parameter.
   297 	 * @since 5.2.0 Added the `$check_signatures` parameter.
   248 	 * @since 5.5.0 Added the `$hook_extra` parameter.
   298 	 * @since 5.5.0 Added the `$hook_extra` parameter.
   249 	 *
   299 	 *
   289 
   339 
   290 		return $download_file;
   340 		return $download_file;
   291 	}
   341 	}
   292 
   342 
   293 	/**
   343 	/**
   294 	 * Unpack a compressed package file.
   344 	 * Unpacks a compressed package file.
   295 	 *
   345 	 *
   296 	 * @since 2.8.0
   346 	 * @since 2.8.0
   297 	 *
   347 	 *
   298 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
   348 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
   299 	 *
   349 	 *
   304 	 */
   354 	 */
   305 	public function unpack_package( $package, $delete_package = true ) {
   355 	public function unpack_package( $package, $delete_package = true ) {
   306 		global $wp_filesystem;
   356 		global $wp_filesystem;
   307 
   357 
   308 		$this->skin->feedback( 'unpack_package' );
   358 		$this->skin->feedback( 'unpack_package' );
       
   359 
       
   360 		if ( ! $wp_filesystem->wp_content_dir() ) {
       
   361 			return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
       
   362 		}
   309 
   363 
   310 		$upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
   364 		$upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
   311 
   365 
   312 		// Clean up contents of upgrade directory beforehand.
   366 		// Clean up contents of upgrade directory beforehand.
   313 		$upgrade_files = $wp_filesystem->dirlist( $upgrade_folder );
   367 		$upgrade_files = $wp_filesystem->dirlist( $upgrade_folder );
   343 
   397 
   344 		return $working_dir;
   398 		return $working_dir;
   345 	}
   399 	}
   346 
   400 
   347 	/**
   401 	/**
   348 	 * Flatten the results of WP_Filesystem_Base::dirlist() for iterating over.
   402 	 * Flattens the results of WP_Filesystem_Base::dirlist() for iterating over.
   349 	 *
   403 	 *
   350 	 * @since 4.9.0
   404 	 * @since 4.9.0
   351 	 * @access protected
   405 	 * @access protected
   352 	 *
   406 	 *
   353 	 * @param array  $nested_files Array of files as returned by WP_Filesystem_Base::dirlist().
   407 	 * @param array  $nested_files Array of files as returned by WP_Filesystem_Base::dirlist().
   426 	 * Copies the contents of a package from a source directory, and installs them in
   480 	 * Copies the contents of a package from a source directory, and installs them in
   427 	 * a destination directory. Optionally removes the source. It can also optionally
   481 	 * a destination directory. Optionally removes the source. It can also optionally
   428 	 * clear out the destination folder if it already exists.
   482 	 * clear out the destination folder if it already exists.
   429 	 *
   483 	 *
   430 	 * @since 2.8.0
   484 	 * @since 2.8.0
       
   485 	 * @since 6.2.0 Use move_dir() instead of copy_dir() when possible.
   431 	 *
   486 	 *
   432 	 * @global WP_Filesystem_Base $wp_filesystem        WordPress filesystem subclass.
   487 	 * @global WP_Filesystem_Base $wp_filesystem        WordPress filesystem subclass.
   433 	 * @global array              $wp_theme_directories
   488 	 * @global array              $wp_theme_directories
   434 	 *
   489 	 *
   435 	 * @param array|string $args {
   490 	 * @param array|string $args {
   467 		// These were previously extract()'d.
   522 		// These were previously extract()'d.
   468 		$source            = $args['source'];
   523 		$source            = $args['source'];
   469 		$destination       = $args['destination'];
   524 		$destination       = $args['destination'];
   470 		$clear_destination = $args['clear_destination'];
   525 		$clear_destination = $args['clear_destination'];
   471 
   526 
   472 		set_time_limit( 300 );
   527 		if ( function_exists( 'set_time_limit' ) ) {
   473 
   528 			set_time_limit( 300 );
   474 		if ( empty( $source ) || empty( $destination ) ) {
   529 		}
       
   530 
       
   531 		if (
       
   532 			( ! is_string( $source ) || '' === $source || trim( $source ) !== $source ) ||
       
   533 			( ! is_string( $destination ) || '' === $destination || trim( $destination ) !== $destination )
       
   534 		) {
   475 			return new WP_Error( 'bad_request', $this->strings['bad_request'] );
   535 			return new WP_Error( 'bad_request', $this->strings['bad_request'] );
   476 		}
   536 		}
   477 		$this->skin->feedback( 'installing_package' );
   537 		$this->skin->feedback( 'installing_package' );
   478 
   538 
   479 		/**
   539 		/**
   506 			$source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] );
   566 			$source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] );
   507 		} elseif ( 0 === count( $source_files ) ) {
   567 		} elseif ( 0 === count( $source_files ) ) {
   508 			// There are no files?
   568 			// There are no files?
   509 			return new WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] );
   569 			return new WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] );
   510 		} else {
   570 		} else {
   511 			// It's only a single file, the upgrader will use the folder name of this file as the destination folder.
   571 			/*
   512 			// Folder name is based on zip filename.
   572 			 * It's only a single file, the upgrader will use the folder name of this file as the destination folder.
       
   573 			 * Folder name is based on zip filename.
       
   574 			 */
   513 			$source = trailingslashit( $args['source'] );
   575 			$source = trailingslashit( $args['source'] );
   514 		}
   576 		}
   515 
   577 
   516 		/**
   578 		/**
   517 		 * Filters the source file location for the upgrade package.
   579 		 * Filters the source file location for the upgrade package.
   526 		 */
   588 		 */
   527 		$source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] );
   589 		$source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] );
   528 
   590 
   529 		if ( is_wp_error( $source ) ) {
   591 		if ( is_wp_error( $source ) ) {
   530 			return $source;
   592 			return $source;
       
   593 		}
       
   594 
       
   595 		if ( ! empty( $args['hook_extra']['temp_backup'] ) ) {
       
   596 			$temp_backup = $this->move_to_temp_backup_dir( $args['hook_extra']['temp_backup'] );
       
   597 
       
   598 			if ( is_wp_error( $temp_backup ) ) {
       
   599 				return $temp_backup;
       
   600 			}
       
   601 
       
   602 			$this->temp_backups[] = $args['hook_extra']['temp_backup'];
   531 		}
   603 		}
   532 
   604 
   533 		// Has the source location changed? If so, we need a new source_files list.
   605 		// Has the source location changed? If so, we need a new source_files list.
   534 		if ( $source !== $remote_source ) {
   606 		if ( $source !== $remote_source ) {
   535 			$source_files = array_keys( $wp_filesystem->dirlist( $source ) );
   607 			$source_files = array_keys( $wp_filesystem->dirlist( $source ) );
   574 
   646 
   575 			if ( is_wp_error( $removed ) ) {
   647 			if ( is_wp_error( $removed ) ) {
   576 				return $removed;
   648 				return $removed;
   577 			}
   649 			}
   578 		} elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) {
   650 		} elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) {
   579 			// If we're not clearing the destination folder and something exists there already, bail.
   651 			/*
   580 			// But first check to see if there are actually any files in the folder.
   652 			 * If we're not clearing the destination folder and something exists there already, bail.
       
   653 			 * But first check to see if there are actually any files in the folder.
       
   654 			 */
   581 			$_files = $wp_filesystem->dirlist( $remote_destination );
   655 			$_files = $wp_filesystem->dirlist( $remote_destination );
   582 			if ( ! empty( $_files ) ) {
   656 			if ( ! empty( $_files ) ) {
   583 				$wp_filesystem->delete( $remote_source, true ); // Clear out the source files.
   657 				$wp_filesystem->delete( $remote_source, true ); // Clear out the source files.
   584 				return new WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination );
   658 				return new WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination );
   585 			}
   659 			}
   586 		}
   660 		}
   587 
   661 
   588 		// Create destination if needed.
   662 		/*
   589 		if ( ! $wp_filesystem->exists( $remote_destination ) ) {
   663 		 * If 'clear_working' is false, the source should not be removed, so use copy_dir() instead.
   590 			if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
   664 		 *
   591 				return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination );
   665 		 * Partial updates, like language packs, may want to retain the destination.
   592 			}
   666 		 * If the destination exists or has contents, this may be a partial update,
   593 		}
   667 		 * and the destination should not be removed, so use copy_dir() instead.
   594 
   668 		 */
   595 		// Copy new version of item into place.
   669 		if ( $args['clear_working']
   596 		$result = copy_dir( $source, $remote_destination );
   670 			&& (
   597 		if ( is_wp_error( $result ) ) {
   671 				// Destination does not exist or has no contents.
   598 			if ( $args['clear_working'] ) {
   672 				! $wp_filesystem->exists( $remote_destination )
   599 				$wp_filesystem->delete( $remote_source, true );
   673 				|| empty( $wp_filesystem->dirlist( $remote_destination ) )
   600 			}
   674 			)
   601 			return $result;
   675 		) {
   602 		}
   676 			$result = move_dir( $source, $remote_destination, true );
   603 
   677 		} else {
   604 		// Clear the working folder?
   678 			// Create destination if needed.
       
   679 			if ( ! $wp_filesystem->exists( $remote_destination ) ) {
       
   680 				if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
       
   681 					return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination );
       
   682 				}
       
   683 			}
       
   684 			$result = copy_dir( $source, $remote_destination );
       
   685 		}
       
   686 
       
   687 		// Clear the working directory?
   605 		if ( $args['clear_working'] ) {
   688 		if ( $args['clear_working'] ) {
   606 			$wp_filesystem->delete( $remote_source, true );
   689 			$wp_filesystem->delete( $remote_source, true );
       
   690 		}
       
   691 
       
   692 		if ( is_wp_error( $result ) ) {
       
   693 			return $result;
   607 		}
   694 		}
   608 
   695 
   609 		$destination_name = basename( str_replace( $local_destination, '', $destination ) );
   696 		$destination_name = basename( str_replace( $local_destination, '', $destination ) );
   610 		if ( '.' === $destination_name ) {
   697 		if ( '.' === $destination_name ) {
   611 			$destination_name = '';
   698 			$destination_name = '';
   632 		// Bombard the calling function will all the info which we've just used.
   719 		// Bombard the calling function will all the info which we've just used.
   633 		return $this->result;
   720 		return $this->result;
   634 	}
   721 	}
   635 
   722 
   636 	/**
   723 	/**
   637 	 * Run an upgrade/installation.
   724 	 * Runs an upgrade/installation.
   638 	 *
   725 	 *
   639 	 * Attempts to download the package (if it is not a local file), unpack it, and
   726 	 * Attempts to download the package (if it is not a local file), unpack it, and
   640 	 * install it in the destination folder.
   727 	 * install it in the destination folder.
   641 	 *
   728 	 *
   642 	 * @since 2.8.0
   729 	 * @since 2.8.0
   739 
   826 
   740 		/*
   827 		/*
   741 		 * Download the package. Note: If the package is the full path
   828 		 * Download the package. Note: If the package is the full path
   742 		 * to an existing local file, it will be returned untouched.
   829 		 * to an existing local file, it will be returned untouched.
   743 		 */
   830 		 */
   744 		$download = $this->download_package( $options['package'], true, $options['hook_extra'] );
   831 		$download = $this->download_package( $options['package'], false, $options['hook_extra'] );
   745 
   832 
   746 		// Allow for signature soft-fail.
   833 		/*
   747 		// WARNING: This may be removed in the future.
   834 		 * Allow for signature soft-fail.
       
   835 		 * WARNING: This may be removed in the future.
       
   836 		 */
   748 		if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
   837 		if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
   749 
   838 
   750 			// Don't output the 'no signature could be found' failure message for now.
   839 			// Don't output the 'no signature could be found' failure message for now.
   751 			if ( 'signature_verification_no_signature' !== $download->get_error_code() || WP_DEBUG ) {
   840 			if ( 'signature_verification_no_signature' !== $download->get_error_code() || WP_DEBUG ) {
   752 				// Output the failure error as a normal feedback, and not as an error.
   841 				// Output the failure error as a normal feedback, and not as an error.
   808 		 * @param array          $hook_extra Extra arguments passed to hooked filters.
   897 		 * @param array          $hook_extra Extra arguments passed to hooked filters.
   809 		 */
   898 		 */
   810 		$result = apply_filters( 'upgrader_install_package_result', $result, $options['hook_extra'] );
   899 		$result = apply_filters( 'upgrader_install_package_result', $result, $options['hook_extra'] );
   811 
   900 
   812 		$this->skin->set_result( $result );
   901 		$this->skin->set_result( $result );
       
   902 
   813 		if ( is_wp_error( $result ) ) {
   903 		if ( is_wp_error( $result ) ) {
       
   904 			// An automatic plugin update will have already performed its rollback.
       
   905 			if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
       
   906 				$this->temp_restores[] = $options['hook_extra']['temp_backup'];
       
   907 
       
   908 				/*
       
   909 				 * Restore the backup on shutdown.
       
   910 				 * Actions running on `shutdown` are immune to PHP timeouts,
       
   911 				 * so in case the failure was due to a PHP timeout,
       
   912 				 * it will still be able to properly restore the previous version.
       
   913 				 *
       
   914 				 * Zero arguments are accepted as a string can sometimes be passed
       
   915 				 * internally during actions, causing an error because
       
   916 				 * `WP_Upgrader::restore_temp_backup()` expects an array.
       
   917 				 */
       
   918 				add_action( 'shutdown', array( $this, 'restore_temp_backup' ), 10, 0 );
       
   919 			}
   814 			$this->skin->error( $result );
   920 			$this->skin->error( $result );
   815 
   921 
   816 			if ( ! method_exists( $this->skin, 'hide_process_failed' ) || ! $this->skin->hide_process_failed( $result ) ) {
   922 			if ( ! method_exists( $this->skin, 'hide_process_failed' ) || ! $this->skin->hide_process_failed( $result ) ) {
   817 				$this->skin->feedback( 'process_failed' );
   923 				$this->skin->feedback( 'process_failed' );
   818 			}
   924 			}
   820 			// Installation succeeded.
   926 			// Installation succeeded.
   821 			$this->skin->feedback( 'process_success' );
   927 			$this->skin->feedback( 'process_success' );
   822 		}
   928 		}
   823 
   929 
   824 		$this->skin->after();
   930 		$this->skin->after();
       
   931 
       
   932 		// Clean up the backup kept in the temporary backup directory.
       
   933 		if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
       
   934 			// Delete the backup on `shutdown` to avoid a PHP timeout.
       
   935 			add_action( 'shutdown', array( $this, 'delete_temp_backup' ), 100, 0 );
       
   936 		}
   825 
   937 
   826 		if ( ! $options['is_multi'] ) {
   938 		if ( ! $options['is_multi'] ) {
   827 
   939 
   828 			/**
   940 			/**
   829 			 * Fires when the upgrader process is complete.
   941 			 * Fires when the upgrader process is complete.
   862 
   974 
   863 		return $result;
   975 		return $result;
   864 	}
   976 	}
   865 
   977 
   866 	/**
   978 	/**
   867 	 * Toggle maintenance mode for the site.
   979 	 * Toggles maintenance mode for the site.
   868 	 *
   980 	 *
   869 	 * Creates/deletes the maintenance file to enable/disable maintenance mode.
   981 	 * Creates/deletes the maintenance file to enable/disable maintenance mode.
   870 	 *
   982 	 *
   871 	 * @since 2.8.0
   983 	 * @since 2.8.0
   872 	 *
   984 	 *
   874 	 *
   986 	 *
   875 	 * @param bool $enable True to enable maintenance mode, false to disable.
   987 	 * @param bool $enable True to enable maintenance mode, false to disable.
   876 	 */
   988 	 */
   877 	public function maintenance_mode( $enable = false ) {
   989 	public function maintenance_mode( $enable = false ) {
   878 		global $wp_filesystem;
   990 		global $wp_filesystem;
       
   991 
       
   992 		if ( ! $wp_filesystem ) {
       
   993 			require_once ABSPATH . 'wp-admin/includes/file.php';
       
   994 			WP_Filesystem();
       
   995 		}
       
   996 
   879 		$file = $wp_filesystem->abspath() . '.maintenance';
   997 		$file = $wp_filesystem->abspath() . '.maintenance';
   880 		if ( $enable ) {
   998 		if ( $enable ) {
   881 			$this->skin->feedback( 'maintenance_start' );
   999 			if ( ! wp_doing_cron() ) {
       
  1000 				$this->skin->feedback( 'maintenance_start' );
       
  1001 			}
   882 			// Create maintenance file to signal that we are upgrading.
  1002 			// Create maintenance file to signal that we are upgrading.
   883 			$maintenance_string = '<?php $upgrading = ' . time() . '; ?>';
  1003 			$maintenance_string = '<?php $upgrading = ' . time() . '; ?>';
   884 			$wp_filesystem->delete( $file );
  1004 			$wp_filesystem->delete( $file );
   885 			$wp_filesystem->put_contents( $file, $maintenance_string, FS_CHMOD_FILE );
  1005 			$wp_filesystem->put_contents( $file, $maintenance_string, FS_CHMOD_FILE );
   886 		} elseif ( ! $enable && $wp_filesystem->exists( $file ) ) {
  1006 		} elseif ( ! $enable && $wp_filesystem->exists( $file ) ) {
   887 			$this->skin->feedback( 'maintenance_end' );
  1007 			if ( ! wp_doing_cron() ) {
       
  1008 				$this->skin->feedback( 'maintenance_end' );
       
  1009 			}
   888 			$wp_filesystem->delete( $file );
  1010 			$wp_filesystem->delete( $file );
   889 		}
  1011 		}
   890 	}
  1012 	}
   891 
  1013 
   892 	/**
  1014 	/**
   893 	 * Creates a lock using WordPress options.
  1015 	 * Creates a lock using WordPress options.
   894 	 *
  1016 	 *
   895 	 * @since 4.5.0
  1017 	 * @since 4.5.0
       
  1018 	 *
       
  1019 	 * @global wpdb $wpdb The WordPress database abstraction object.
   896 	 *
  1020 	 *
   897 	 * @param string $lock_name       The name of this unique lock.
  1021 	 * @param string $lock_name       The name of this unique lock.
   898 	 * @param int    $release_timeout Optional. The duration in seconds to respect an existing lock.
  1022 	 * @param int    $release_timeout Optional. The duration in seconds to respect an existing lock.
   899 	 *                                Default: 1 hour.
  1023 	 *                                Default: 1 hour.
   900 	 * @return bool False if a lock couldn't be created or if the lock is still valid. True otherwise.
  1024 	 * @return bool False if a lock couldn't be created or if the lock is still valid. True otherwise.
   905 			$release_timeout = HOUR_IN_SECONDS;
  1029 			$release_timeout = HOUR_IN_SECONDS;
   906 		}
  1030 		}
   907 		$lock_option = $lock_name . '.lock';
  1031 		$lock_option = $lock_name . '.lock';
   908 
  1032 
   909 		// Try to lock.
  1033 		// Try to lock.
   910 		$lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_option, time() ) );
  1034 		$lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'off') /* LOCK */", $lock_option, time() ) );
   911 
  1035 
   912 		if ( ! $lock_result ) {
  1036 		if ( ! $lock_result ) {
   913 			$lock_result = get_option( $lock_option );
  1037 			$lock_result = get_option( $lock_option );
   914 
  1038 
   915 			// If a lock couldn't be created, and there isn't a lock, bail.
  1039 			// If a lock couldn't be created, and there isn't a lock, bail.
   945 	 * @return bool True if the lock was successfully released. False on failure.
  1069 	 * @return bool True if the lock was successfully released. False on failure.
   946 	 */
  1070 	 */
   947 	public static function release_lock( $lock_name ) {
  1071 	public static function release_lock( $lock_name ) {
   948 		return delete_option( $lock_name . '.lock' );
  1072 		return delete_option( $lock_name . '.lock' );
   949 	}
  1073 	}
       
  1074 
       
  1075 	/**
       
  1076 	 * Moves the plugin or theme being updated into a temporary backup directory.
       
  1077 	 *
       
  1078 	 * @since 6.3.0
       
  1079 	 *
       
  1080 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
       
  1081 	 *
       
  1082 	 * @param string[] $args {
       
  1083 	 *     Array of data for the temporary backup.
       
  1084 	 *
       
  1085 	 *     @type string $slug Plugin or theme slug.
       
  1086 	 *     @type string $src  Path to the root directory for plugins or themes.
       
  1087 	 *     @type string $dir  Destination subdirectory name. Accepts 'plugins' or 'themes'.
       
  1088 	 * }
       
  1089 	 *
       
  1090 	 * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
       
  1091 	 */
       
  1092 	public function move_to_temp_backup_dir( $args ) {
       
  1093 		global $wp_filesystem;
       
  1094 
       
  1095 		if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
       
  1096 			return false;
       
  1097 		}
       
  1098 
       
  1099 		/*
       
  1100 		 * Skip any plugin that has "." as its slug.
       
  1101 		 * A slug of "." will result in a `$src` value ending in a period.
       
  1102 		 *
       
  1103 		 * On Windows, this will cause the 'plugins' folder to be moved,
       
  1104 		 * and will cause a failure when attempting to call `mkdir()`.
       
  1105 		 */
       
  1106 		if ( '.' === $args['slug'] ) {
       
  1107 			return false;
       
  1108 		}
       
  1109 
       
  1110 		if ( ! $wp_filesystem->wp_content_dir() ) {
       
  1111 			return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
       
  1112 		}
       
  1113 
       
  1114 		$dest_dir = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/';
       
  1115 		$sub_dir  = $dest_dir . $args['dir'] . '/';
       
  1116 
       
  1117 		// Create the temporary backup directory if it does not exist.
       
  1118 		if ( ! $wp_filesystem->is_dir( $sub_dir ) ) {
       
  1119 			if ( ! $wp_filesystem->is_dir( $dest_dir ) ) {
       
  1120 				$wp_filesystem->mkdir( $dest_dir, FS_CHMOD_DIR );
       
  1121 			}
       
  1122 
       
  1123 			if ( ! $wp_filesystem->mkdir( $sub_dir, FS_CHMOD_DIR ) ) {
       
  1124 				// Could not create the backup directory.
       
  1125 				return new WP_Error( 'fs_temp_backup_mkdir', $this->strings['temp_backup_mkdir_failed'] );
       
  1126 			}
       
  1127 		}
       
  1128 
       
  1129 		$src_dir = $wp_filesystem->find_folder( $args['src'] );
       
  1130 		$src     = trailingslashit( $src_dir ) . $args['slug'];
       
  1131 		$dest    = $dest_dir . trailingslashit( $args['dir'] ) . $args['slug'];
       
  1132 
       
  1133 		// Delete the temporary backup directory if it already exists.
       
  1134 		if ( $wp_filesystem->is_dir( $dest ) ) {
       
  1135 			$wp_filesystem->delete( $dest, true );
       
  1136 		}
       
  1137 
       
  1138 		// Move to the temporary backup directory.
       
  1139 		$result = move_dir( $src, $dest, true );
       
  1140 		if ( is_wp_error( $result ) ) {
       
  1141 			return new WP_Error( 'fs_temp_backup_move', $this->strings['temp_backup_move_failed'] );
       
  1142 		}
       
  1143 
       
  1144 		return true;
       
  1145 	}
       
  1146 
       
  1147 	/**
       
  1148 	 * Restores the plugin or theme from temporary backup.
       
  1149 	 *
       
  1150 	 * @since 6.3.0
       
  1151 	 * @since 6.6.0 Added the `$temp_backups` parameter.
       
  1152 	 *
       
  1153 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
       
  1154 	 *
       
  1155 	 * @param array[] $temp_backups {
       
  1156 	 *     Optional. An array of temporary backups.
       
  1157 	 *
       
  1158 	 *     @type array ...$0 {
       
  1159 	 *         Information about the backup.
       
  1160 	 *
       
  1161 	 *         @type string $dir  The temporary backup location in the upgrade-temp-backup directory.
       
  1162 	 *         @type string $slug The item's slug.
       
  1163 	 *         @type string $src  The directory where the original is stored. For example, `WP_PLUGIN_DIR`.
       
  1164 	 *     }
       
  1165 	 * }
       
  1166 	 * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
       
  1167 	 */
       
  1168 	public function restore_temp_backup( array $temp_backups = array() ) {
       
  1169 		global $wp_filesystem;
       
  1170 
       
  1171 		$errors = new WP_Error();
       
  1172 
       
  1173 		if ( empty( $temp_backups ) ) {
       
  1174 			$temp_backups = $this->temp_restores;
       
  1175 		}
       
  1176 
       
  1177 		foreach ( $temp_backups as $args ) {
       
  1178 			if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
       
  1179 				return false;
       
  1180 			}
       
  1181 
       
  1182 			if ( ! $wp_filesystem->wp_content_dir() ) {
       
  1183 				$errors->add( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
       
  1184 				return $errors;
       
  1185 			}
       
  1186 
       
  1187 			$src      = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/' . $args['dir'] . '/' . $args['slug'];
       
  1188 			$dest_dir = $wp_filesystem->find_folder( $args['src'] );
       
  1189 			$dest     = trailingslashit( $dest_dir ) . $args['slug'];
       
  1190 
       
  1191 			if ( $wp_filesystem->is_dir( $src ) ) {
       
  1192 				// Cleanup.
       
  1193 				if ( $wp_filesystem->is_dir( $dest ) && ! $wp_filesystem->delete( $dest, true ) ) {
       
  1194 					$errors->add(
       
  1195 						'fs_temp_backup_delete',
       
  1196 						sprintf( $this->strings['temp_backup_restore_failed'], $args['slug'] )
       
  1197 					);
       
  1198 					continue;
       
  1199 				}
       
  1200 
       
  1201 				// Move it.
       
  1202 				$result = move_dir( $src, $dest, true );
       
  1203 				if ( is_wp_error( $result ) ) {
       
  1204 					$errors->add(
       
  1205 						'fs_temp_backup_delete',
       
  1206 						sprintf( $this->strings['temp_backup_restore_failed'], $args['slug'] )
       
  1207 					);
       
  1208 					continue;
       
  1209 				}
       
  1210 			}
       
  1211 		}
       
  1212 
       
  1213 		return $errors->has_errors() ? $errors : true;
       
  1214 	}
       
  1215 
       
  1216 	/**
       
  1217 	 * Deletes a temporary backup.
       
  1218 	 *
       
  1219 	 * @since 6.3.0
       
  1220 	 * @since 6.6.0 Added the `$temp_backups` parameter.
       
  1221 	 *
       
  1222 	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
       
  1223 	 *
       
  1224 	 * @param array[] $temp_backups {
       
  1225 	 *     Optional. An array of temporary backups.
       
  1226 	 *
       
  1227 	 *     @type array ...$0 {
       
  1228 	 *         Information about the backup.
       
  1229 	 *
       
  1230 	 *         @type string $dir  The temporary backup location in the upgrade-temp-backup directory.
       
  1231 	 *         @type string $slug The item's slug.
       
  1232 	 *         @type string $src  The directory where the original is stored. For example, `WP_PLUGIN_DIR`.
       
  1233 	 *     }
       
  1234 	 * }
       
  1235 	 * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
       
  1236 	 */
       
  1237 	public function delete_temp_backup( array $temp_backups = array() ) {
       
  1238 		global $wp_filesystem;
       
  1239 
       
  1240 		$errors = new WP_Error();
       
  1241 
       
  1242 		if ( empty( $temp_backups ) ) {
       
  1243 			$temp_backups = $this->temp_backups;
       
  1244 		}
       
  1245 
       
  1246 		foreach ( $temp_backups as $args ) {
       
  1247 			if ( empty( $args['slug'] ) || empty( $args['dir'] ) ) {
       
  1248 				return false;
       
  1249 			}
       
  1250 
       
  1251 			if ( ! $wp_filesystem->wp_content_dir() ) {
       
  1252 				$errors->add( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
       
  1253 				return $errors;
       
  1254 			}
       
  1255 
       
  1256 			$temp_backup_dir = $wp_filesystem->wp_content_dir() . "upgrade-temp-backup/{$args['dir']}/{$args['slug']}";
       
  1257 
       
  1258 			if ( ! $wp_filesystem->delete( $temp_backup_dir, true ) ) {
       
  1259 				$errors->add(
       
  1260 					'temp_backup_delete_failed',
       
  1261 					sprintf( $this->strings['temp_backup_delete_failed'], $args['slug'] )
       
  1262 				);
       
  1263 				continue;
       
  1264 			}
       
  1265 		}
       
  1266 
       
  1267 		return $errors->has_errors() ? $errors : true;
       
  1268 	}
   950 }
  1269 }
   951 
  1270 
   952 /** Plugin_Upgrader class */
  1271 /** Plugin_Upgrader class */
   953 require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
  1272 require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
   954 
  1273