wp/wp-admin/includes/class-plugin-upgrader.php
changeset 7 cf61fcea0001
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     1 <?php
       
     2 /**
       
     3  * Upgrade API: Plugin_Upgrader class
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage Upgrader
       
     7  * @since 4.6.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Core class used for upgrading/installing plugins.
       
    12  *
       
    13  * It is designed to upgrade/install plugins from a local zip, remote zip URL,
       
    14  * or uploaded zip file.
       
    15  *
       
    16  * @since 2.8.0
       
    17  * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
       
    18  *
       
    19  * @see WP_Upgrader
       
    20  */
       
    21 class Plugin_Upgrader extends WP_Upgrader {
       
    22 
       
    23 	/**
       
    24 	 * Plugin upgrade result.
       
    25 	 *
       
    26 	 * @since 2.8.0
       
    27 	 * @var array|WP_Error $result
       
    28 	 *
       
    29 	 * @see WP_Upgrader::$result
       
    30 	 */
       
    31 	public $result;
       
    32 
       
    33 	/**
       
    34 	 * Whether a bulk upgrade/installation is being performed.
       
    35 	 *
       
    36 	 * @since 2.9.0
       
    37 	 * @var bool $bulk
       
    38 	 */
       
    39 	public $bulk = false;
       
    40 
       
    41 	/**
       
    42 	 * Initialize the upgrade strings.
       
    43 	 *
       
    44 	 * @since 2.8.0
       
    45 	 */
       
    46 	public function upgrade_strings() {
       
    47 		$this->strings['up_to_date'] = __('The plugin is at the latest version.');
       
    48 		$this->strings['no_package'] = __('Update package not available.');
       
    49 		/* translators: %s: package URL */
       
    50 		$this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s&#8230;' ), '<span class="code">%s</span>' );
       
    51 		$this->strings['unpack_package'] = __('Unpacking the update&#8230;');
       
    52 		$this->strings['remove_old'] = __('Removing the old version of the plugin&#8230;');
       
    53 		$this->strings['remove_old_failed'] = __('Could not remove the old plugin.');
       
    54 		$this->strings['process_failed'] = __('Plugin update failed.');
       
    55 		$this->strings['process_success'] = __('Plugin updated successfully.');
       
    56 		$this->strings['process_bulk_success'] = __('Plugins updated successfully.');
       
    57 	}
       
    58 
       
    59 	/**
       
    60 	 * Initialize the installation strings.
       
    61 	 *
       
    62 	 * @since 2.8.0
       
    63 	 */
       
    64 	public function install_strings() {
       
    65 		$this->strings['no_package'] = __('Installation package not available.');
       
    66 		/* translators: %s: package URL */
       
    67 		$this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s&#8230;' ), '<span class="code">%s</span>' );
       
    68 		$this->strings['unpack_package'] = __('Unpacking the package&#8230;');
       
    69 		$this->strings['installing_package'] = __('Installing the plugin&#8230;');
       
    70 		$this->strings['no_files'] = __('The plugin contains no files.');
       
    71 		$this->strings['process_failed'] = __('Plugin installation failed.');
       
    72 		$this->strings['process_success'] = __('Plugin installed successfully.');
       
    73 	}
       
    74 
       
    75 	/**
       
    76 	 * Install a plugin package.
       
    77 	 *
       
    78 	 * @since 2.8.0
       
    79 	 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional.
       
    80 	 *
       
    81 	 * @param string $package The full local path or URI of the package.
       
    82 	 * @param array  $args {
       
    83 	 *     Optional. Other arguments for installing a plugin package. Default empty array.
       
    84 	 *
       
    85 	 *     @type bool $clear_update_cache Whether to clear the plugin updates cache if successful.
       
    86 	 *                                    Default true.
       
    87 	 * }
       
    88 	 * @return bool|WP_Error True if the installation was successful, false or a WP_Error otherwise.
       
    89 	 */
       
    90 	public function install( $package, $args = array() ) {
       
    91 
       
    92 		$defaults = array(
       
    93 			'clear_update_cache' => true,
       
    94 		);
       
    95 		$parsed_args = wp_parse_args( $args, $defaults );
       
    96 
       
    97 		$this->init();
       
    98 		$this->install_strings();
       
    99 
       
   100 		add_filter('upgrader_source_selection', array($this, 'check_package') );
       
   101 		if ( $parsed_args['clear_update_cache'] ) {
       
   102 			// Clear cache so wp_update_plugins() knows about the new plugin.
       
   103 			add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 );
       
   104 		}
       
   105 
       
   106 		$this->run( array(
       
   107 			'package' => $package,
       
   108 			'destination' => WP_PLUGIN_DIR,
       
   109 			'clear_destination' => false, // Do not overwrite files.
       
   110 			'clear_working' => true,
       
   111 			'hook_extra' => array(
       
   112 				'type' => 'plugin',
       
   113 				'action' => 'install',
       
   114 			)
       
   115 		) );
       
   116 
       
   117 		remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 );
       
   118 		remove_filter('upgrader_source_selection', array($this, 'check_package') );
       
   119 
       
   120 		if ( ! $this->result || is_wp_error($this->result) )
       
   121 			return $this->result;
       
   122 
       
   123 		// Force refresh of plugin update information
       
   124 		wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
       
   125 
       
   126 		return true;
       
   127 	}
       
   128 
       
   129 	/**
       
   130 	 * Upgrade a plugin.
       
   131 	 *
       
   132 	 * @since 2.8.0
       
   133 	 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional.
       
   134 	 *
       
   135 	 * @param string $plugin The basename path to the main plugin file.
       
   136 	 * @param array  $args {
       
   137 	 *     Optional. Other arguments for upgrading a plugin package. Default empty array.
       
   138 	 *
       
   139 	 *     @type bool $clear_update_cache Whether to clear the plugin updates cache if successful.
       
   140 	 *                                    Default true.
       
   141 	 * }
       
   142 	 * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise.
       
   143 	 */
       
   144 	public function upgrade( $plugin, $args = array() ) {
       
   145 
       
   146 		$defaults = array(
       
   147 			'clear_update_cache' => true,
       
   148 		);
       
   149 		$parsed_args = wp_parse_args( $args, $defaults );
       
   150 
       
   151 		$this->init();
       
   152 		$this->upgrade_strings();
       
   153 
       
   154 		$current = get_site_transient( 'update_plugins' );
       
   155 		if ( !isset( $current->response[ $plugin ] ) ) {
       
   156 			$this->skin->before();
       
   157 			$this->skin->set_result(false);
       
   158 			$this->skin->error('up_to_date');
       
   159 			$this->skin->after();
       
   160 			return false;
       
   161 		}
       
   162 
       
   163 		// Get the URL to the zip file
       
   164 		$r = $current->response[ $plugin ];
       
   165 
       
   166 		add_filter('upgrader_pre_install', array($this, 'deactivate_plugin_before_upgrade'), 10, 2);
       
   167 		add_filter('upgrader_clear_destination', array($this, 'delete_old_plugin'), 10, 4);
       
   168 		//'source_selection' => array($this, 'source_selection'), //there's a trac ticket to move up the directory for zip's which are made a bit differently, useful for non-.org plugins.
       
   169 		if ( $parsed_args['clear_update_cache'] ) {
       
   170 			// Clear cache so wp_update_plugins() knows about the new plugin.
       
   171 			add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 );
       
   172 		}
       
   173 
       
   174 		$this->run( array(
       
   175 			'package' => $r->package,
       
   176 			'destination' => WP_PLUGIN_DIR,
       
   177 			'clear_destination' => true,
       
   178 			'clear_working' => true,
       
   179 			'hook_extra' => array(
       
   180 				'plugin' => $plugin,
       
   181 				'type' => 'plugin',
       
   182 				'action' => 'update',
       
   183 			),
       
   184 		) );
       
   185 
       
   186 		// Cleanup our hooks, in case something else does a upgrade on this connection.
       
   187 		remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 );
       
   188 		remove_filter('upgrader_pre_install', array($this, 'deactivate_plugin_before_upgrade'));
       
   189 		remove_filter('upgrader_clear_destination', array($this, 'delete_old_plugin'));
       
   190 
       
   191 		if ( ! $this->result || is_wp_error($this->result) )
       
   192 			return $this->result;
       
   193 
       
   194 		// Force refresh of plugin update information
       
   195 		wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
       
   196 
       
   197 		return true;
       
   198 	}
       
   199 
       
   200 	/**
       
   201 	 * Bulk upgrade several plugins at once.
       
   202 	 *
       
   203 	 * @since 2.8.0
       
   204 	 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional.
       
   205 	 *
       
   206 	 * @param array $plugins Array of the basename paths of the plugins' main files.
       
   207 	 * @param array $args {
       
   208 	 *     Optional. Other arguments for upgrading several plugins at once. Default empty array.
       
   209 	 *
       
   210 	 *     @type bool $clear_update_cache Whether to clear the plugin updates cache if successful.
       
   211 	 *                                    Default true.
       
   212 	 * }
       
   213 	 * @return array|false An array of results indexed by plugin file, or false if unable to connect to the filesystem.
       
   214 	 */
       
   215 	public function bulk_upgrade( $plugins, $args = array() ) {
       
   216 
       
   217 		$defaults = array(
       
   218 			'clear_update_cache' => true,
       
   219 		);
       
   220 		$parsed_args = wp_parse_args( $args, $defaults );
       
   221 
       
   222 		$this->init();
       
   223 		$this->bulk = true;
       
   224 		$this->upgrade_strings();
       
   225 
       
   226 		$current = get_site_transient( 'update_plugins' );
       
   227 
       
   228 		add_filter('upgrader_clear_destination', array($this, 'delete_old_plugin'), 10, 4);
       
   229 
       
   230 		$this->skin->header();
       
   231 
       
   232 		// Connect to the Filesystem first.
       
   233 		$res = $this->fs_connect( array(WP_CONTENT_DIR, WP_PLUGIN_DIR) );
       
   234 		if ( ! $res ) {
       
   235 			$this->skin->footer();
       
   236 			return false;
       
   237 		}
       
   238 
       
   239 		$this->skin->bulk_header();
       
   240 
       
   241 		/*
       
   242 		 * Only start maintenance mode if:
       
   243 		 * - running Multisite and there are one or more plugins specified, OR
       
   244 		 * - a plugin with an update available is currently active.
       
   245 		 * @TODO: For multisite, maintenance mode should only kick in for individual sites if at all possible.
       
   246 		 */
       
   247 		$maintenance = ( is_multisite() && ! empty( $plugins ) );
       
   248 		foreach ( $plugins as $plugin )
       
   249 			$maintenance = $maintenance || ( is_plugin_active( $plugin ) && isset( $current->response[ $plugin] ) );
       
   250 		if ( $maintenance )
       
   251 			$this->maintenance_mode(true);
       
   252 
       
   253 		$results = array();
       
   254 
       
   255 		$this->update_count = count($plugins);
       
   256 		$this->update_current = 0;
       
   257 		foreach ( $plugins as $plugin ) {
       
   258 			$this->update_current++;
       
   259 			$this->skin->plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin, false, true);
       
   260 
       
   261 			if ( !isset( $current->response[ $plugin ] ) ) {
       
   262 				$this->skin->set_result('up_to_date');
       
   263 				$this->skin->before();
       
   264 				$this->skin->feedback('up_to_date');
       
   265 				$this->skin->after();
       
   266 				$results[$plugin] = true;
       
   267 				continue;
       
   268 			}
       
   269 
       
   270 			// Get the URL to the zip file.
       
   271 			$r = $current->response[ $plugin ];
       
   272 
       
   273 			$this->skin->plugin_active = is_plugin_active($plugin);
       
   274 
       
   275 			$result = $this->run( array(
       
   276 				'package' => $r->package,
       
   277 				'destination' => WP_PLUGIN_DIR,
       
   278 				'clear_destination' => true,
       
   279 				'clear_working' => true,
       
   280 				'is_multi' => true,
       
   281 				'hook_extra' => array(
       
   282 					'plugin' => $plugin
       
   283 				)
       
   284 			) );
       
   285 
       
   286 			$results[$plugin] = $this->result;
       
   287 
       
   288 			// Prevent credentials auth screen from displaying multiple times
       
   289 			if ( false === $result )
       
   290 				break;
       
   291 		} //end foreach $plugins
       
   292 
       
   293 		$this->maintenance_mode(false);
       
   294 
       
   295 		// Force refresh of plugin update information.
       
   296 		wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
       
   297 
       
   298 		/** This action is documented in wp-admin/includes/class-wp-upgrader.php */
       
   299 		do_action( 'upgrader_process_complete', $this, array(
       
   300 			'action' => 'update',
       
   301 			'type' => 'plugin',
       
   302 			'bulk' => true,
       
   303 			'plugins' => $plugins,
       
   304 		) );
       
   305 
       
   306 		$this->skin->bulk_footer();
       
   307 
       
   308 		$this->skin->footer();
       
   309 
       
   310 		// Cleanup our hooks, in case something else does a upgrade on this connection.
       
   311 		remove_filter('upgrader_clear_destination', array($this, 'delete_old_plugin'));
       
   312 
       
   313 		return $results;
       
   314 	}
       
   315 
       
   316 	/**
       
   317 	 * Check a source package to be sure it contains a plugin.
       
   318 	 *
       
   319 	 * This function is added to the {@see 'upgrader_source_selection'} filter by
       
   320 	 * Plugin_Upgrader::install().
       
   321 	 *
       
   322 	 * @since 3.3.0
       
   323 	 *
       
   324 	 * @global WP_Filesystem_Base $wp_filesystem Subclass
       
   325 	 *
       
   326 	 * @param string $source The path to the downloaded package source.
       
   327 	 * @return string|WP_Error The source as passed, or a WP_Error object
       
   328 	 *                         if no plugins were found.
       
   329 	 */
       
   330 	public function check_package($source) {
       
   331 		global $wp_filesystem;
       
   332 
       
   333 		if ( is_wp_error($source) )
       
   334 			return $source;
       
   335 
       
   336 		$working_directory = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit(WP_CONTENT_DIR), $source);
       
   337 		if ( ! is_dir($working_directory) ) // Sanity check, if the above fails, let's not prevent installation.
       
   338 			return $source;
       
   339 
       
   340 		// Check the folder contains at least 1 valid plugin.
       
   341 		$plugins_found = false;
       
   342 		$files = glob( $working_directory . '*.php' );
       
   343 		if ( $files ) {
       
   344 			foreach ( $files as $file ) {
       
   345 				$info = get_plugin_data( $file, false, false );
       
   346 				if ( ! empty( $info['Name'] ) ) {
       
   347 					$plugins_found = true;
       
   348 					break;
       
   349 				}
       
   350 			}
       
   351 		}
       
   352 
       
   353 		if ( ! $plugins_found )
       
   354 			return new WP_Error( 'incompatible_archive_no_plugins', $this->strings['incompatible_archive'], __( 'No valid plugins were found.' ) );
       
   355 
       
   356 		return $source;
       
   357 	}
       
   358 
       
   359 	/**
       
   360 	 * Retrieve the path to the file that contains the plugin info.
       
   361 	 *
       
   362 	 * This isn't used internally in the class, but is called by the skins.
       
   363 	 *
       
   364 	 * @since 2.8.0
       
   365 	 *
       
   366 	 * @return string|false The full path to the main plugin file, or false.
       
   367 	 */
       
   368 	public function plugin_info() {
       
   369 		if ( ! is_array($this->result) )
       
   370 			return false;
       
   371 		if ( empty($this->result['destination_name']) )
       
   372 			return false;
       
   373 
       
   374 		$plugin = get_plugins('/' . $this->result['destination_name']); //Ensure to pass with leading slash
       
   375 		if ( empty($plugin) )
       
   376 			return false;
       
   377 
       
   378 		$pluginfiles = array_keys($plugin); //Assume the requested plugin is the first in the list
       
   379 
       
   380 		return $this->result['destination_name'] . '/' . $pluginfiles[0];
       
   381 	}
       
   382 
       
   383 	/**
       
   384 	 * Deactivates a plugin before it is upgraded.
       
   385 	 *
       
   386 	 * Hooked to the {@see 'upgrader_pre_install'} filter by Plugin_Upgrader::upgrade().
       
   387 	 *
       
   388 	 * @since 2.8.0
       
   389 	 * @since 4.1.0 Added a return value.
       
   390 	 *
       
   391 	 * @param bool|WP_Error  $return Upgrade offer return.
       
   392 	 * @param array          $plugin Plugin package arguments.
       
   393 	 * @return bool|WP_Error The passed in $return param or WP_Error.
       
   394 	 */
       
   395 	public function deactivate_plugin_before_upgrade($return, $plugin) {
       
   396 
       
   397 		if ( is_wp_error($return) ) //Bypass.
       
   398 			return $return;
       
   399 
       
   400 		// When in cron (background updates) don't deactivate the plugin, as we require a browser to reactivate it
       
   401 		if ( wp_doing_cron() )
       
   402 			return $return;
       
   403 
       
   404 		$plugin = isset($plugin['plugin']) ? $plugin['plugin'] : '';
       
   405 		if ( empty($plugin) )
       
   406 			return new WP_Error('bad_request', $this->strings['bad_request']);
       
   407 
       
   408 		if ( is_plugin_active($plugin) ) {
       
   409 			//Deactivate the plugin silently, Prevent deactivation hooks from running.
       
   410 			deactivate_plugins($plugin, true);
       
   411 		}
       
   412 
       
   413 		return $return;
       
   414 	}
       
   415 
       
   416 	/**
       
   417 	 * Delete the old plugin during an upgrade.
       
   418 	 *
       
   419 	 * Hooked to the {@see 'upgrader_clear_destination'} filter by
       
   420 	 * Plugin_Upgrader::upgrade() and Plugin_Upgrader::bulk_upgrade().
       
   421 	 *
       
   422 	 * @since 2.8.0
       
   423 	 *
       
   424 	 * @global WP_Filesystem_Base $wp_filesystem Subclass
       
   425      *
       
   426 	 * @param bool|WP_Error $removed
       
   427 	 * @param string        $local_destination
       
   428 	 * @param string        $remote_destination
       
   429 	 * @param array         $plugin
       
   430 	 * @return WP_Error|bool
       
   431 	 */
       
   432 	public function delete_old_plugin($removed, $local_destination, $remote_destination, $plugin) {
       
   433 		global $wp_filesystem;
       
   434 
       
   435 		if ( is_wp_error($removed) )
       
   436 			return $removed; //Pass errors through.
       
   437 
       
   438 		$plugin = isset($plugin['plugin']) ? $plugin['plugin'] : '';
       
   439 		if ( empty($plugin) )
       
   440 			return new WP_Error('bad_request', $this->strings['bad_request']);
       
   441 
       
   442 		$plugins_dir = $wp_filesystem->wp_plugins_dir();
       
   443 		$this_plugin_dir = trailingslashit( dirname($plugins_dir . $plugin) );
       
   444 
       
   445 		if ( ! $wp_filesystem->exists($this_plugin_dir) ) //If it's already vanished.
       
   446 			return $removed;
       
   447 
       
   448 		// If plugin is in its own directory, recursively delete the directory.
       
   449 		if ( strpos($plugin, '/') && $this_plugin_dir != $plugins_dir ) //base check on if plugin includes directory separator AND that it's not the root plugin folder
       
   450 			$deleted = $wp_filesystem->delete($this_plugin_dir, true);
       
   451 		else
       
   452 			$deleted = $wp_filesystem->delete($plugins_dir . $plugin);
       
   453 
       
   454 		if ( ! $deleted )
       
   455 			return new WP_Error('remove_old_failed', $this->strings['remove_old_failed']);
       
   456 
       
   457 		return true;
       
   458 	}
       
   459 }