How WordPress Plugin Auto Update Works

WordPress upgrades a plugin when an admin clicks the update-now button or uploads 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 stores the plugin’s folder name in auto_update_plugins option. Later, the plugin (along with other auto update enabled plugins) gets updated on a recurring cron event wp_version_check. wp_version_check cron event is triggered every 12 hours.

/**
 * @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 some cases, WordPress also schedules an extra event to run this job, when the WordPress version check request responds 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 handled 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 is whether the current site is the main site of a network (for multisite installations). It doesn’t process auto updates on sub-sites.

The next thing WordPress does is create a lock so that the updater doesn’t run concurrently, only one run at a time. It also removes a 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 fetches the plugin update information, and runs each plugin update one by one. A plugin only gets updated if an update is available, auto-update is enabled & the minimum php version requirement 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. The skin is responsible for handling the messages shown on the interface, and the upgrader is responsible for handling the file download, verification, replacement, deletion etc. 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 extracts it to the wp-content/upgrade folder, and then installs it in place of the old plugin.

/**
 * @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 an 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. That means, you can zip a plugin as something/my-plugin/file.php and it will still work as an update.

That’s it.