Self-Hosted Plugin Updates

Before all – if any of the explanations in this tutorial seems difficult for you, you can always get any of my plugins and explore the code.

Step 1. Create a .json File with Update Information and Plugin ZIP Archive on Your Server

WordPress Update API accepts certain parameters like plugin name, plugin author, changelog etc. You can:

In this tutorial I will show you the first way, because it is more clear and simple to understand.

First of all your have to create a .json file somewhere on your server, for example YOUR_WEBSITE/info.json. Please take a look at the parameters which I think really should be on your server:

{
	"version" => "1.1",
	"download_url" : "https://rudrastyh.com/misha-plugin.zip",
	"requires" : "3.0",
	"tested" : "4.8.1",
	"requires_php" : "5.3",
	"last_updated" : "2017-08-17 02:10:00",
	"sections" : {
		"description" : "This is the plugin to test your updater script",
		"installation" : "Upload the plugin to your blog, Activate it, that's it!",
		"changelog" : "<h4>1.1 –  January 17, 2020</h4><ul><li>Some bugs are fixed.</li><li>Release date.</li></ul>"
	},
	"banners" : {
		"low" : "https://YOUR_WEBSITE/banner-772x250.jpg",
		"high" : "https://YOUR_WEBSITE/banner-1544x500.jpg"
	},
	"screenshots" : "<ol><li><a href='IMG_URL' target='_blank'><img src='IMG_URL' alt='CAPTION' /></a><p>CAPTION</p></li></ol>"
}

There are a plenty of other parameters as well, all of them I describe below. And it is on your choice which of them to specify in your info.json file.

Also an important note about the files and folders structure inside a zip archive.

No 👿

Yes 😊

Here is what happened if you skip this advise: after you click the “Update” button, your old version of the plugin will be removed, but the new one won’t replace it but will be created in a randomly generated folder like misha-plugin-xhfuif/ as a result you will get a notice: “The misha-plugin.php has been deactivated due to an error: Plugin file does not exist.”

The misha-plugin.php has been deactivated due to an error: Plugin file does not exist.

Step 2. Plugin Information for the Popup

When you click “View Details”, WordPress shows you the appropriate plugin information from its own server. And the most interesting thing is that this info can be hooked. It can be hooked before WordPress connects to its own server. So, it doesn’t affect website performance. While exploring WP code, I begin to love it more and more.

Custom update popup for my Instagram plugin.
It is a part of my custom updater script that I’ve implemented in my Instagram plugin.
add_filter('plugins_api', 'misha_plugin_info', 20, 3);
/*
 * $res empty at this step
 * $action 'plugin_information'
 * $args stdClass Object ( [slug] => woocommerce [is_ssl] => [fields] => Array ( [banners] => 1 [reviews] => 1 [downloaded] => [active_installs] => 1 ) [per_page] => 24 [locale] => en_US )
 */
function misha_plugin_info( $res, $action, $args ){
 
	// do nothing if this is not about getting plugin information
	if( 'plugin_information' !== $action ) {
		return false;
	}
 
	$plugin_slug = 'YOUR PLUGIN SLUG'; // we are going to use it in many places in this function
 
	// do nothing if it is not our plugin
	if( $plugin_slug !== $args->slug ) {
		return false;
	}
 
	// trying to get from cache first
	if( false == $remote = get_transient( 'misha_update_' . $plugin_slug ) ) {
 
		// info.json is the file with the actual plugin information on your server
		$remote = wp_remote_get( 'https://rudrastyh.com/wp-content/uploads/info.json', array(
			'timeout' => 10,
			'headers' => array(
				'Accept' => 'application/json'
			) )
		);
 
		if ( ! is_wp_error( $remote ) && isset( $remote['response']['code'] ) && $remote['response']['code'] == 200 && ! empty( $remote['body'] ) ) {
			set_transient( 'misha_update_' . $plugin_slug, $remote, 43200 ); // 12 hours cache
		}
 
	}
 
	if( ! is_wp_error( $remote ) && isset( $remote['response']['code'] ) && $remote['response']['code'] == 200 && ! empty( $remote['body'] ) ) {
 
		$remote = json_decode( $remote['body'] );
		$res = new stdClass();
 
		$res->name = $remote->name;
		$res->slug = $plugin_slug;
		$res->version = $remote->version;
		$res->tested = $remote->tested;
		$res->requires = $remote->requires;
		$res->author = '<a href="https://rudrastyh.com">Misha Rudrastyh</a>';
		$res->author_profile = 'https://profiles.wordpress.org/rudrastyh';
		$res->download_link = $remote->download_url;
		$res->trunk = $remote->download_url;
		$res->requires_php = '5.3';
		$res->last_updated = $remote->last_updated;
		$res->sections = array(
			'description' => $remote->sections->description,
			'installation' => $remote->sections->installation,
			'changelog' => $remote->sections->changelog
			// you can add your custom sections (tabs) here
		);
 
		// in case you want the screenshots tab, use the following HTML format for its content:
		// <ol><li><a href="IMG_URL" target="_blank"><img src="IMG_URL" alt="CAPTION" /></a><p>CAPTION</p></li></ol>
		if( !empty( $remote->sections->screenshots ) ) {
			$res->sections['screenshots'] = $remote->sections->screenshots;
		}
 
		$res->banners = array(
			'low' => 'https://YOUR_WEBSITE/banner-772x250.jpg',
			'high' => 'https://YOUR_WEBSITE/banner-1544x500.jpg'
		);
		return $res;
 
	}
 
	return false;
 
}

Let me describe some more parameters that are not mentioned in the code:

contributors
The associative array of contributors wordpress.org profile name and URLs, like

 

array(
	'rudrastyh' => 'https://profiles.wordpress.org/rudrastyh',
	'contributer' => 'https://profiles.wordpress.org/contributer'
)
WordPress contributors that are displayed in a plugin update popup
requires_php
The minimum required version of PHP
rating
The rating count from 1 to 100
ratings
The 5-star rating, how much votes for each star, example:

 

array(
	5 => 2104
	4 => 116,
	3 => 64,
	2 => 57,
	1 => 175
)
num_ratings
The amount of overall votes

 

WordPress plugin rating that is displayed in plugin update popup
support_threads
The number of threads on support forum
support_threads_resolved
The number of resolved threads on support forum
active_installs
The number of active plugin installations
added
The date in YYYY-MM-DD format
homepage
The plugin homepage URL
reviews
The HTML of plugin reviews. Review example:
WordPress plugin reviews which are displayed in plugin update info
versions
The array of plugin versions and their URLs, example

 

array(
	'1.0' => 'URL of zip archive of 1.0 plugin version',
	'trunk' => 'URL of the latest plugin version'
)
donate_link
The URL of donate link

Step 3. Push the Update Information into WP Transients

Demo plugin with custom update server.
add_filter('site_transient_update_plugins', 'misha_push_update' );
 
function misha_push_update( $transient ){
 
	if ( empty($transient->checked ) ) {
            return $transient;
        }
 
	// trying to get from cache first, to disable cache comment 10,20,21,22,24
	if( false == $remote = get_transient( 'misha_upgrade_YOUR_PLUGIN_SLUG' ) ) {
 
		// info.json is the file with the actual plugin information on your server
		$remote = wp_remote_get( 'https://YOUR_WEBSITE/SOME_PATH/info.json', array(
			'timeout' => 10,
			'headers' => array(
				'Accept' => 'application/json'
			) )
		);
 
		if ( !is_wp_error( $remote ) && isset( $remote['response']['code'] ) && $remote['response']['code'] == 200 && !empty( $remote['body'] ) ) {
			set_transient( 'misha_upgrade_YOUR_PLUGIN_SLUG', $remote, 43200 ); // 12 hours cache
		}
 
	}
 
	if( $remote ) {
 
		$remote = json_decode( $remote['body'] );
 
		// your installed plugin version should be on the line below! You can obtain it dynamically of course 
		if( $remote && version_compare( '1.0', $remote->version, '<' ) && version_compare($remote->requires, get_bloginfo('version'), '<' ) ) {
			$res = new stdClass();
			$res->slug = 'YOUR_PLUGIN_SLUG';
			$res->plugin = 'YOUR_PLUGIN_FOLDER/YOUR_PLUGIN_SLUG.php'; // it could be just YOUR_PLUGIN_SLUG.php if your plugin doesn't have its own directory
			$res->new_version = $remote->version;
			$res->tested = $remote->tested;
			$res->package = $remote->download_url;
           		$transient->response[$res->plugin] = $res;
           		//$transient->checked[$res->plugin] = $remote->version;
           	}
 
	}
        return $transient;
}

Step 4. Cache the results to make it awesomely fast

The last step is to clean the cache after the plugin has been updated.

add_action( 'upgrader_process_complete', 'misha_after_update', 10, 2 );
 
function misha_after_update( $upgrader_object, $options ) {
	if ( $options['action'] == 'update' && $options['type'] === 'plugin' )  {
		// just clean the cache when new plugin version is installed
		delete_transient( 'misha_upgrade_YOUR_PLUGIN_SLUG' );
	}
}
Misha Rudrastyh

Misha Rudrastyh

I love WordPress, WooCommerce and Gutenberg so much. 11 yrs of experience.

Need some custom developer help? Get in touch