How WordPress Plugin Auto Update Works

WordPress upgrades a plugin when an admin click on update-now button or upload a new zip file. Besides that, a WordPress plugin can also be updated automatically if an admin enables auto-update for that plugin. Here’s how plugin auto update works.

WP_Version_Check

When we click on Enable auto-updates for a plugin, WordPress store the plugin’s folder name in auto_update_plugins option. Later, the plugin (along with other auto update enabled plugin) gets updated on a recurring cron event wp_version_check. wp_version_check cron event is triggered in every 12 hour.

/**
 * @file wp-includes/update.php
 */

function wp_schedule_update_checks() {
	if ( ! wp_next_scheduled( 'wp_version_check' ) && ! wp_installing() ) {
		wp_schedule_event( time(), 'twicedaily', 'wp_version_check' );
	}
	// ...
}

In cases, WordPress also schedule extra event to run this job when WordPress version check request respond with a ttl property.

/**
 * @file wp-includes/update.php
 */

function wp_version_check( $extra_stats = array(), $force_check = false ) {
	// ...
	$url = 'http://api.wordpress.org/core/version-check/1.7/?' . http_build_query( $query, '', '&' );

	// ...
	$response = wp_remote_post( $url, $options );

	// ...
	$body = trim( wp_remote_retrieve_body( $response ) );
	$body = json_decode( $body, true );

	// ...
	if ( ! empty( $body['ttl'] ) ) {
		$ttl = (int) $body['ttl'];

		if ( $ttl && ( time() + $ttl < wp_next_scheduled( 'wp_version_check' ) ) ) {
			// Queue an event to re-run the update check in $ttl seconds.
			wp_schedule_single_event( time() + $ttl, 'wp_version_check' );
		}
	}
	// ...
}

wp_version_check cron event is handled by a function named wp_version_check. At the end of this function, WordPress triggers an action named wp_maybe_auto_update which performs the auto update. wp_maybe_auto_update action is only triggered when wp_version_check function runs in the cron context. Means, calling wp_version_check in a plugin will not trigger wp_maybe_auto_update action. wp_maybe_auto_update action is responded by a function named wp_maybe_auto_update.

WP_Maybe_Auto_Update

Here’s the function definition

/**
 * @file wp-includes/update.php
 */

function wp_maybe_auto_update() {
	include_once ABSPATH . 'wp-admin/includes/admin.php';
	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

	$upgrader = new WP_Automatic_Updater;
	$upgrader->run();
}

So the main task is done in WP_Automatic_Updater class.

The $upgrader->run() method first checks whether automatic update is enabled.

Automatic update might be disabled in many ways, defining wp-config constant (AUTOMATIC_UPDATER_DISABLED, DISALLOW_FILE_MODS), using hook (automatic_updater_disabled) and if WordPress is installing something (wp_installing function).

The next thing it checks whether current site is the main site of a network (for multisite installation). It doesn’t process auto update on sub-sites.

The next thing WordPress do is create a lock so that the updater doesn’t run recurrently rather only once. And remove few filters.

/**
 * @class WP_Automatic_Updater
 */

public function run() {
	if ( $this->is_disabled() ) {
		return;
	}

	if ( ! is_main_network() || ! is_main_site() ) {
		return;
	}

	if ( ! WP_Upgrader::create_lock( 'auto_updater' ) ) {
		return;
	}
	// ...
}

Updating Plugin

Next the process fetch plugin update information, and run each plugin update one by one. Plugin only gets updated if an update is available, auto-update is enabled & minimum php version requirements is met.

/**
 * @class WP_Automatic_Updater
 */

public function run() {
	// ...
	wp_update_plugins(); // Check for plugin updates.
	$plugin_updates = get_site_transient( 'update_plugins' );
	if ( $plugin_updates && ! empty( $plugin_updates->response ) ) {
		foreach ( $plugin_updates->response as $plugin ) {
			$this->update( 'plugin', $plugin );
		}
		// Force refresh of plugin update information.
		wp_clean_plugins_cache();
	}
	// ...
}

The update process uses Automatic_Upgrader_Skin class as skin, and Plugin_Upgrader class as upgrader. Skin is responsible handing the messages shown on interface, and upgrader is responsible for handing the file download, verification, replacement, deletion etc process. The next process is upgrading the plugin. $upgrader_item is the folder name (or single file name) of the plugin.

/**
 * @class WP_Automatic_Updater
 */

public function update( $type, $item ) {
	$skin = new Automatic_Upgrader_Skin;

	// ...
	$upgrader = new Plugin_Upgrader( $skin );
	$context  = WP_PLUGIN_DIR;

	// ...
	if ( ! $this->should_update( $type, $item, $context ) ) {
		return false;
	}

	$upgrader_item = $item->plugin;

	$upgrade_result = $upgrader->upgrade(
		$upgrader_item,
		array(
			'clear_update_cache'           => false,
			// Always use partial builds if possible for core updates.
			'pre_check_md5'                => false,
			// Only available for core updates.
			'attempt_rollback'             => true,
			// Allow relaxed file ownership in some scenarios.
			'allow_relaxed_file_ownership' => $allow_relaxed_file_ownership,
		)
	);
}

Now we jump to the Plugin_Upgrader::upgrade method. This upgrader method performs a check to see if the plugin does have an update. $r is the plugin update response, $r->package is the direct link to the new version zip file. Then it runs the upgrade. Following is the code

/**
 * @class Plugin_Upgrader
 */

public function upgrade( $plugin, $args = array() ) {
	// ...
	$current = get_site_transient( 'update_plugins' );

	// ...
	$r = $current->response[ $plugin ];
	
	// ...
	$this->run(
		array(
			'package'           => $r->package,
			'destination'       => WP_PLUGIN_DIR,
			'clear_destination' => true,
			'clear_working'     => true,
			'hook_extra'        => array(
				'plugin' => $plugin,
				'type'   => 'plugin',
				'action' => 'update',
			),
		)
	);
	// ...
);

This run method is defined in WP_Upgrader class. First it downloads the package, then extract it to wp-content/upgrade folder, then remove Then check for plugin file.

/**
 * @class Wp_Upgrader
 */

public function run( $options ) {
	// ...
	$download = $this->download_package( $options['package'], true, $options['hook_extra'] );

	// ...
	$delete_package = ( $download !== $options['package'] );

	// ...
	$working_dir = $this->unpack_package( $download, $delete_package );

	// ...
	$result = $this->install_package(
		array(
			'source'                      => $working_dir,
			'destination'                 => $options['destination'],
			'clear_destination'           => $options['clear_destination'],
			'abort_if_destination_exists' => $options['abort_if_destination_exists'],
			'clear_working'               => $options['clear_working'],
			'hook_extra'                  => $options['hook_extra'],
		)
	);

	// ...
}

There’s a interesting thing in this install_package method. If the new update zip contains a folder in the root and no other files, it extracts the folder content as the plugin folder / files. Means, you can zip a plugin of something/my-plugin/file.php as and update.

That’s it.