In this tutorial I will show you how to develop a custom update repository for your private plugin. It will be helpful if you’re developing plugins for sale.
We are not going to use any ready or bloated PHP-libraries for that, only two filter hooks plugins_api
and site_transient_update_plugins
and transient cache, that’s all.
You can also download the full code as a ready plugin from my GitHub.
And remember if you need some custom coding help, you can always get in touch with me and my team.
And now let’s go do it!
1. Prepare your Custom Update Server #
Update server could be anything, it can be a subdomain or just a directory on your website.
The long story short, here is the screenshot of the files on my update server.
Let’s configure our info.json
file now.
{
"name" : "Misha Update Checker",
"slug" : "misha-update-checker",
"author" : "<a href='https://rudrastyh.com'>Misha Rudrastyh</a>",
"author_profile" : "http://profiles.wordpress.org/rudrastyh",
"version" : "2.0",
"download_url" : "https://rudrastyh.com/wp-content/uploads/updater/misha-update-checker.zip",
"requires" : "3.0",
"tested" : "5.8",
"requires_php" : "5.3",
"last_updated" : "2021-01-30 02:10:00",
"sections" : {
"description" : "This simple plugin does nothing, only gets updates from a custom server",
"installation" : "Click the activate button and that's it.",
"changelog" : "<h4>1.0 – 1 august 2021</h4><ul><li>Bug fixes.</li><li>Initital release.</li></ul>"
},
"banners" : {
"low" : "https://rudrastyh.com/wp-content/uploads/updater/banner-772x250.jpg",
"high" : "https://rudrastyh.com/wp-content/uploads/updater/banner-1544x500.jpg"
}
}
Also do not forget to check if info.json
is valid, you can do it with any json validator you find in google, I used this one for example.
Another way is to generate info.json
via PHP, here is how you can do it.
$update = array(
'name' => 'Misha Update Checker',
'slug' => 'misha-update-checker',
...
);
header( 'Content-Type: application/json' );
echo json_encode( $update );
The advantage of PHP method is that you can pass some get parameters to it like info.php?plugin_id=5
or check plugin license as well.
Please note, that this json-file could have any parameter names, it doesn’t matter in this step. It matters below, when we pass these parameters into WordPress Plugins API.
Also an important note about the files and folders structure inside a zip archive.
No 👿 |
Yes ✔️ |
misha-update-checker.zip ––misha-update-checker.php ––readme.txt |
misha-update-checker.zip ––misha-update-checker/ ––––misha-update-checker.php ––––readme.txt |
Here is what happens 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-update-checker-xhfuif/
as a result you will get a notice: “The plugin has been deactivated due to an error: Plugin file does not exist.”
2. Plugin Information Modal #
Usually when you click “View Details”, WordPress shows you the appropriate plugin information from its own wordpress.org update server. And the most interesting thing is that this info can be hooked before WordPress connects to wordpress.org server.
Here is what I am talking about:
For the simplicity of the code I skipped using Transients API to cache the remote request, but you have to use it in your code. Check the plugin code on GitHub.
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;
}
// do nothing if it is not our plugin
if( plugin_basename( __DIR__ ) !== $args->slug ) {
return false;
}
// info.json is the file with the actual plugin information on your server
$remote = wp_remote_get(
'https://rudrastyh.com/wp-content/uploads/updater/info.json',
array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json'
)
)
);
// do nothing if we don't get the correct response from the server
if(
is_wp_error( $remote )
|| 200 !== wp_remote_retrieve_response_code( $remote )
|| empty( wp_remote_retrieve_body( $remote )
) {
return false;
}
$remote = json_decode( wp_remote_retrieve_body( $remote ) );
$res = new stdClass();
$res->name = $remote->name;
$res->slug = $remote->slug;
$res->author = $remote->author;
$res->author_profile = $remote->author_profile;
$res->version = $remote->version;
$res->tested = $remote->tested;
$res->requires = $remote->requires;
$res->requires_php = $remote->requires_php;
$res->download_link = $remote->download_url;
$res->trunk = $remote->download_url;
$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' => $remote->banners->low,
'high' => $remote->banners->high
);
return $res;
}
On line 15 we get the plugin slug using plugin_basename( __DIR__ )
, I would like to talk more on that:
plugin_basename( __DIR__ ) |
misha-update-checker |
plugin_basename( __FILE__ ) |
misha-update-checker/misha-update-checker.php |
And please keep in mind, that the link “View details” won’t appear until you implement the next step of this tutorial.
stdClass object properties #
In the above code we created stdClass
object with some parameter to pass into WordPress API.
- name
- Plugin name.
- slug
- Plugin slug.
- author
- Plugin author.
- author_profile.
- Author profile URL on wordpress.org, example https://profiles.wordpress.org/rudrastyh/.
- contributors
- The associative array of contributors wordpress.org profile name and URLs, like
array(
'rudrastyh' => 'https://profiles.wordpress.org/rudrastyh',
'contributor' => 'https://profiles.wordpress.org/contributor',
)
- version
- Current plugin version available to install.
- tested
- The latest WordPress version plugin tested with.
- requires
- The minimum WordPress version required.
- requires_php
- The minimum PHP version required.
- 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
- 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
- last updated
- The date in YYYY-MM-DD format
- homepage
- The plugin homepage URL
- reviews
- The HTML of plugin reviews.
- 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
Push the Update Information into WP Transients #
This is the final step, after implementing it you will got this:
And again, for the simplicity of the code below I skipped using Transients API to cache the remote request, but you have to use it. Check how in the plugin code on GitHub.
add_filter( 'site_transient_update_plugins', 'misha_push_update' );
function misha_push_update( $transient ){
if ( empty( $transient->checked ) ) {
return $transient;
}
$remote = wp_remote_get(
'https://rudrastyh.com/wp-content/uploads/updater/info.json',
array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json'
)
)
);
if(
is_wp_error( $remote )
|| 200 !== wp_remote_retrieve_response_code( $remote )
|| empty( wp_remote_retrieve_body( $remote )
) {
return $transient;
}
$remote = json_decode( wp_remote_retrieve_body( $remote ) );
// your installed plugin version should be on the line below! You can obtain it dynamically of course
if(
$remote
&& version_compare( $this->version, $remote->version, '<' )
&& version_compare( $remote->requires, get_bloginfo( 'version' ), '<' )
&& version_compare( $remote->requires_php, PHP_VERSION, '<' )
) {
$res = new stdClass();
$res->slug = $remote->slug;
$res->plugin = plugin_basename( __FILE__ ); // 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;
}