Automatic Updates For Private And Commercial Plugins

Last updated on June 26, 2015.

Since time immemorial, only plugins hosted in the official WordPress.org plugin directory have supported automatic updates. Now, I’ve written a PHP library that you can use to add automatic update capabilities to any plugin. Public, private and commercial plugins alike – all can now enjoy the benefits of automatic update notifications and one-click upgrades.

The custom update checker integrates closely with the upgrade system already built into WordPress, producing a seamless user experience. Observe :

An upgrade notice for a privately hosted plugin.

An upgrade notice for a privately hosted plugin.

The version information window with placeholder data

The version information window with placeholder data

Download

License

This library is released under the MIT License and is distributed free of charge. If you find it useful, consider making a donation.

Quick-start Guide

This section describes the quickest way to get automatic updates working for your plugin. Here’s what you’ll need to do: create a metadata file for your plugin, host it somewhere publicly accessible, and tell the update checker where to find it.

Lets start with the metadata. Copy the JSON code below into a new file and replace the placeholder values with your plugin’s info.

{
    "name" : "My Cool Plugin",
    "slug" : "my-cool-plugin",
    "download_url" : "https://example.com/plugins/my-cool-plugin.zip",
    "version" : "2.0",
    "author" : "John Smith",
    "sections" : {
        "description" : "Plugin description here. Basic HTML allowed."
    }
}

(This is the minimum amount of data required to make automatic updates work. In most cases, you will probably want to add a couple more fields. See the metadata docs for a full list.)

Most of the fields should be pretty self-explanatory, with one possible exception – the “slug”. WordPress expects all plugins that support automatic updates to have a unique textual identifier called the “slug”. Normally, slugs are assigned by the official plugin directory. For a private/commercial plugin that’s hosted elsewhere you’ll have to make something up. If unsure, just use the plugin’s file name without the “.php” extension (my-cool-plugin/my-cool-plugin.php becomes my-cool-plugin).

Upload the metadata file you just created to your web server. It doesn’t matter where exactly you put the file or how you name it. The important thing is for its URL to be accessible from wherever someone might install your plugin.

Next, copy the “plugin-update-checker” directory from the client library archive to your plugin’s directory. Then fire up your favourite code editor and add the following lines to the top of your plugin file:

require 'plugin-update-checker/plugin-update-checker.php';
$MyUpdateChecker = PucFactory::buildUpdateChecker(
    'https://example.com/path/to/metadata.json',
    __FILE__,
    'your-chosen-slug'
);

If you followed my advice and used the plugin’s file name as the slug, you can omit the third parameter of the PucFactory::buildUpdateChecker() call.

Tip: Sometimes you’ll run into a situation where another active plugin is also using this update checker. As a result, there could be several different versions of the library loaded at the same time. The above code snippet will always give you the latest available version. This can be a problem if your plugin expects an older version and is not API-compatible with the latest version.

To use a specific version of the update checker (e.g. the one included with your plugin), instantiate the PluginUpdateChecker_x_y class directly. Replace x and y with the major and minor version numbers:

//Use version 2.0 of the update checker.
require 'plugin-update-checker/plugin-update-checker.php';
$MyUpdateChecker = new PluginUpdateChecker_2_0 (
    'https://example.com/path/to/metadata.json',
    __FILE__,
    'your-chosen-slug'
);

And that, believe it or not, is it.

The PluginUpdateChecker class will handle the rest. It’ll check the metadata file every 12 hours and, if it discovers that a new version has been released, twiddle the right bits in the undocumented WP API to make it show up as a standard upgrade notification in the “Plugins” tab. Assuming you’ve provided a valid download_url, users will be able to install the update with a single click.

Tip: When creating the ZIP file for an update, put all plugin files inside a directory. The directory name should match the plugin slug. Do not put the files at the root of the ZIP archive – it can cause subtle bugs and errors when someone ties to install the update.

The rest of this post will be devoted to a more in-depth discussion of the update checker class and the metadata format.

The PluginUpdateChecker class

This class is the core of the update checker. It’s also the only part of the updater that you should need to deal with unless you decide to  extend the library yourself.

Class constructor

All configuration settings should be specified by passing them to the PucFactory::buildUpdateChecker() factory method, or directly to the PluginUpdateChecker constructor. Both takes the following parameters:

  • $metadataUrl – The full URL of the plugin’s metadata file.
  • $pluginFile – The path to the plugin’s file. In most cases you can simply use the __FILE__ constant here.
  • $slug – The plugin’s ‘slug’. If not specified, the filename part of $pluginFile (sans “.php”) will be used as the slug.
  • $checkPeriod – How often to check for updates (in hours). Defaults to checking every 12 hours. Set to zero to disable automatic update checks.
  • $optionName – Where to store book-keeping info about updates. Defaults to “external_updates-$slug”.

checkForUpdates()

Manually trigger an update check. This is especially useful when you’ve disabled automatic checks by setting $checkPeriod (above) to zero. This method takes no parameters and returns nothing.

addQueryArgFilter($callback)

Register a callback for filtering query arguments. Whenever the update checker needs to retrieve the metadata file, it will first run each filter callback and attach the query arguments that they return to the metadata URL. This lets you pass arbitrary data to the server hosting the metadata. For example, commercial plugins could use it to implement some kind of authorization scheme where only users that have the right “key” get automatic updates.

The callback function will be passed an associative array of query arguments and should return a modified array. By default, the update checker will add these arguments to the metadata URL:

  • installed_version – set to the currently installed version of the plugin.
  • checking_for_updates – set to 1 if checking for updates, absent otherwise (i.e. when loading data for the “Plugin Information” box).

This method takes one parameter – the callback function.

addHttpRequestArgFilter($callback)

Register a callback for filtering the various options passed to the built-in helper function wp_remote_get that the update checker uses to periodically download plugin metadata. The callback function should take one argument – an associative array of arguments – and return a modified array or arguments. See the WP documentation on wp_remote_get for details about what arguments are available and how they work.

This method takes one parameter – the callback function.

addResultFilter($callback)

Register a callback for filtering plugin info retrieved from the metadata URL.

The callback function should take two arguments. If the metadata was retrieved successfully, the first argument passed will be an instance of PluginInfo (see the source for a description of this class). Otherwise, it will be NULL. The second argument will be the corresponding return value of wp_remote_get (see WP docs for details). The callback function should return a new or modified instance of PluginInfo or NULL.

This method takes one parameter – the callback function.

Metadata format

The automatic update system uses a JSON-based file format to describe plugins.  Essentially, the entire file is one big JSON-encoded object (AKA hash-table or associative array). Each field – or array key – represents a piece of information about the latest version of the plugin. The full description of all available fields is here.

For the sake of simplicity, both general metadata and update-related information are stored in the same file. If this is undesirable, you can replace the plain JSON file with a script that checks for the presence of the the “checking_for_updates” query parameter and emits just the update-related fields if its set to “1”.

Notes

Your plugin must be active for updates to work. The update checker is just another piece of PHP code loaded and run by your plugin, and it won’t be run if the plugin is inactive.

One consequence of this that may not be immediately obvious is that on a multisite installation updates will only show up if the plugin is active on the main site. This is because update notifications usually appear in the network admin, and only plugins active on the main site are loaded in that case. The main site of a WordPress network is the one that was created first and has the path “/” in the Sites -> All Sites list.

Related posts :

496 Responses to “Automatic Updates For Private And Commercial Plugins”

  1. Chris says:

    This is great. Thanks for your work on this. The only issue I’ve run into so far is the plugin does not re-activate upon completion, and has to be enabled manually. Any thoughts on what might be causing this?

  2. Jānis Elsts says:

    Perhaps the directory name is wrong? Check the name of the plugin directory in /wp-content/plugins – is the same before and after update?

  3. Chris says:

    The directory name was identical before/after. I just ran another test and it worked fine. Not sure what happened, but I’ll keep my eye out on it and report back if I have any further problems. Thanks again, and sorry for the false alarm.

  4. Caroline says:

    I’m sorry but this code is awesome. Thank you so much. I will make a small donation now and put it on my calendar to pay a little more when I have more cash. It’s worth it. THANK YOU.

  5. Jānis Elsts says:

    Thank you for the donation 🙂

  6. Fernanda says:

    Hi, thank you for the class. Do you mind to explain the “checking_for_updates” query parameter point? I tried to check for this parameter using $_GET, $_POST, and even $_REQUEST, but it didn’t work. I would like that my themes and plugins to be download through wordpress, but not through direct link. It would be awesome if there is a way to identify when the request is coming from a wordpress installation. Is that parameter still valid for the newer wordpress version? Thanks

  7. Jānis Elsts says:

    The parameter would be in $_GET, and yes, it is still valid. The point of the “checking_for_updates” parameter is to help the server that hosts update information distinguish between two types of requests sent by the class:

    1. Retrieving all plugin metadata because it needs to be displayed in the “View version 1.2.3 details” popup.
    2. Checking for updates. In this case, the class really only needs a subset of the metadata, not all of it.

    It’s perfectly fine to send the complete metadata in both situations – the update checker will automatically pick the fields it needs and ignore the rest. However, some developers may want to distinguish between #1 and #2 to optimize bandwidth usage, collect additional statistics, etc.

    I would like that my themes and plugins to be download through wordpress, but not through direct link. It would be awesome if there is a way to identify when the request is coming from a wordpress installation.

    I’m sure you know that anti-piracy measures are never 100% effective. Your users will have access to your theme and plugin source code. A skilled programmer who has the source code and knows how to use HTTP traffic monitoring tools will find a way to fake download requests that look like they’re coming from WordPress.

    In any case, the “checking_for_updates” parameter is not a good choice for this purpose. Anyone can add a parameter to a URL.

    Personally, what I do is generate a unique license key to each. The key is used in update requests and download links. If it turns out that someone is abusing or distributing the links, I could block the key and their link would stop working.

  8. Pete says:

    Hi! Awesome tool; thanks for creating it!

    Does it support banner images for the plugin details? That is, when you click “View details for version x.x.x and there is a popup. Is it possible to use an image banner at the top of that pop-up, like so many plugins do?

    Thanks!

    Pete

  9. Jānis Elsts says:

    It doesn’t support banners right now, but it seems like it would be pretty easy to add that feature. You just need a “banners” key in the metadata and a little bit of extra code to hand that data to WordPress.

  10. Jānis Elsts says:

    Update: Banner support has been added. Get the new library version from the GitHub repository (relevant commit).

  11. Colin says:

    This worked for a bit but now nothing will update and, occasionally, I get this error:

    Notice: Failed to parse plugin metadata. Try validating your .json file with http://jsonlint.com/ in ##URL##/plugin-update-checker.php on line 783

    I have validated and revalidated my json file but nothing seems to work!

  12. Colin says:

    I figured out what was happening, it was a typo. But, now that it’s trying to update, I get:

    Download failed. A valid URL was not provided.

    The URL works and I can download the zip from the URL in my browser with no problems. Have you seen this before?

  13. Jānis Elsts says:

    I haven’t seen that before, no. Are you doing anything special with the URL, like generating it dynamically or appending additional query parameters? Also, could you post your .json file? I’ll check it for problems.

  14. Colin says:

    I got it. Apparently you can’t update from the same domain that the repository is on?

    http://www.emanueletessore.com/wordpress-download-failed-valid-url-provided/

  15. Chris Huff says:

    Thank you so much for making this. Working great. Is there a way to tweak it so that it doesn’t delete certain folders in the updating plugin? I’m imagining possibly copying the folders to a temp directory, then after the update is complete, moving the folders back over and deleting the temp directory. Is this possible?

  16. Jānis Elsts says:

    @ Colin:

    Interesting, I wasn’t aware of that restriction. The link you posted already shows how you could use the http_request_host_is_external filter to bypass it. Another option would be to use the http_request_args filter and set the reject_unsafe_urls request argument to false when the current URL points to your domain.

    @ Chris:

    You’d have to implement those changes separately. The update checker doesn’t actually install updates. It just gives update information to WordPress. It’s WordPress that displays the available update in the Dashboard, downloads the ZIP, extracts it to a temporary location, copies the new directories, and deletes the old version.

    You could probably use the upgrader_source_selection filter to back up your directories and either upgrader_process_complete or upgrader_post_install to restore them. Be warned that these hooks are poorly documented, so you may need to read WP source code to figure out how they work.

  17. Karl L says:

    Hey Jānis!
    I’ve got a question about this, since i cant get it to work.
    My plugin.zip is as following:

    plugin.zip
    7abc/
    7abc.php
    core/
    some-plugin-core-files.php
    plugin-updates/

    and still, when i tries to update it there is an errors saying:
    “The directory structure of the update is incorrect. All plugin files should be inside a directory named 7abc, not at the root of the ZIP file.”

    Any idea why i still get this error, even tho the files aint in zip root?
    Thanks in advance,
    /Karl

  18. Karl L says:

    To see the zip-structure better:
    plugin.zip
    -7abc/
    –7abc.php
    –core/
    —some-plugin-core-files.php
    –plugin-updates/

  19. Jānis Elsts says:

    Could you show me the .zip file? If you don’t want to post it publicly, you can send me a link via the contact form.

  20. Karl L says:

    Done and done! Check the form submit for link. 🙂

Leave a Reply