Custom WordPress Plug-in Repository With Automatic Updates
, updated:

Custom WordPress Plug-in Repository With Automatic Updates

I have written my own anti-spam plugin. It has been live-tested 3 months. Since then the code has proven to be very effective in combat against post-spam.

I decided to create a full-featured plugin and distribute it over 20 WordPress sites. And here comes the problem which is a combination of 2 axioms:

When I improve the code, I cannot update plug-in manually over 20 websites. This can be solved by uploading plugin code to the official WordPress repository what is not a big deal since I already maintain one such plug-in.

This procedure has one disadvantage, especially when it comes to anti-spam plugins. Open-sourcing code makes such plug-in vulnerable because a potential spammer can learn how to by-pass protection algorithm.

Solution to the need of standard WordPress updates without source-code disclosure over official plug-in repository is to create a private WordPress repo.

How hard it is? Let’s ask Google. I was surprised when I found several pre-made libraries while most robust is wp-update-server created by Yahnis Elsts.

It’s actively maintained and.. real overkill for my need of “1-plugin-repository“. What else is out there? Misha Rudrastyh wrote a beautiful post on the topic Self-Hosted Plugin Updates.

All you need it to add 3 additional “add_action” to your plugin + PHP controller (your very simple private plugin repository server) responding with jSon on is-there-a-new-release question.

Code for a plugin

Place following snippets to your plug-in or theme.

Pop-up with details about plugin update visible when there is a new release

define( 'PREFIX_PLUGIN_VERSION', '1.7' );

###################
# Automatic updates
###################
/**
 * Pop-up with details about plugin update visible when there is a new release
 */

function prefix_plugin_info( $res, $action, $args ) {

    // do nothing if this is not about getting plugin information
    if ($action !== 'plugin_information') {
        return false;
    }

    // do nothing if it is not our plugin
    if ('pluginslug' !== $args->slug) {
        return $res;
    }

    // trying to get from cache first, to disable cache comment 23,33,34,35,36
    if (false == $remote = get_transient( 'prefix_upgrade_pluginslug' )) {

        // info.json is the file with the actual information about plug-in on your server
        $remote = wp_remote_get( 'https://www.example.com/foldername/get-info.php?slug=pluginslug&action=info', 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( 'prefix_upgrade_pluginslug', $remote, 21600 ); // 6 hours cache
        }
    }

    if (!is_wp_error( $remote )) {

        $remote = json_decode( $remote[ 'body' ] );

        $res = new stdClass();
        $res->name = $remote->name;
        $res->slug = $remote->slug;
        $res->version = $remote->version;
        $res->tested = $remote->tested;
        $res->requires = $remote->requires;
        $res->author = $remote->author;
        $res->author_profile = $remote->author_homepage;
        $res->download_link = $remote->download_link;
        $res->trunk = $remote->download_link;
        $res->last_updated = $remote->last_updated;
        $res->sections = array(
            'description' => $remote->sections->description, // description tab
            'installation' => $remote->sections->installation, // installation tab
                // you can add your custom sections (tabs) here like 'changelog'
        );
        $res->banners = array(
            'low' => $remote->banners->low,
            'high' => $remote->banners->high,
        );

        return $res;
    }

    return false;

}

add_filter( 'plugins_api', 'prefix_plugin_info', 20, 3 );

Push update itself for plugin or theme

/**
 * Push update itself for plugin or theme
 */
function prefix_push_update( $transient ) {

    if (empty( $transient->checked )) {
        return $transient;
    }

    // trying to get from cache first, to disable cache comment 11,20,21,22,23
    if (false == $remote = get_transient( 'prefix_upgrade_pluginslug' )) {
        // info.json is the file with the actual plugin information on your server
        $remote = wp_remote_get( 'https://www.jasom.net/repo/get-info.php?slug=pluginslug&action=update', 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( 'prefix_upgrade_pluginslug', $remote, 21600 ); // 6 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( prefix_PLUGIN_VERSION, $remote->version, '<' ) && version_compare( $remote->requires, get_bloginfo( 'version' ), '<' )) {
            $res = new stdClass();
            $res->slug = 'pluginslug';
            $res->plugin = 'pluginslug/pluginslug.php'; // it could be just pluginslug.php if your plugin doesn't have its own directory
            $res->new_version = $remote->version;
            $res->tested = $remote->tested;
            $res->package = $remote->download_link;
            $transient->response[ $res->plugin ] = $res;
            //$transient->checked[$res->plugin] = $remote->version;
        }
    }
    return $transient;

}

add_filter( 'site_transient_update_plugins', 'prefix_push_update' );

Cache the results to make update process fast

/**
 * Cache the results to make update process fast
 */
function prefix_after_update( $upgrader_object, $options ) {
    if ($options[ 'action' ] == 'update' && $options[ 'type' ] === 'plugin') {
        // just clean the cache when new plugin version is installed
        delete_transient( 'prefix_upgrade_pluginslug' );
    }

}

add_action( 'upgrader_process_complete', 'prefix_after_update', 10, 2 );

How my “custom-wordpress-plugin-repository” works

Controller for a simple repository server is uploaded to Github. I will summarize how to install and configure it + how the script works.

Download file and configure it with your domain name and folder where it runs. It requires you to run cron job for the url:

https://www.example.com/repofolder/get-info.php?action=cron

Upload your zipped plugin to repo folder. Controller on cron run will automatically recognize all .zip files, creates a new jSon file for each plugin with basic information extracted from plugin.php file and logs cron run to the file cron.log.

It checks if uploaded .zip files have up-to-date jSon which is used to store information when WordPress installation with the plugin enabled requests the info under URL as follows:

https://www.example.com/repofolder/get-info.php?slug=pluginslug

Every WordPress request is logged to the file called requests.log where you can see requested plugin, the domain request came from and the time of the request.

Ok, that’s all. Fork, build upon, enjoy.

Leave a Reply

Your email address will not be published. Required fields are marked *

I’ve written a little plugin for WP in the last few weeks. And have been hanging on the problem of the update function for a long time.

I read your blog post with great interest. Thanks a lot for this!

I then installed your demo plugin in my test environment. When I click the button “Check again” in the dashboard, I don’t get an updated display.

Get-info.php can be reached via Postman. I am using WP 5.3.2

When I run the get-info.php directly, I get this error:

Fatal error: Uncaught Error: Class’ ZipArchive ‘not found in /otl/get-info.php:233 Stack trace: # 0 /otl/get-info.php(159): RepoServer-> unzipPlugin (‘ mypluginslug1.z. .. ‘) # 1 /otl/get-info.php(130): RepoServer-> createJsonInfoFile (‘ mypluginslug1.z … ‘) # 2 /otl/get-info.php(64): RepoServer-> cron () # 3 /otl/get-info.php(275): RepoServer -> __ construct () # 4 {main} thrown in /otl/get-info.php on line 233

Do you have any idea how I can fix the errors? I’ve been working on it all week now and I’m not really getting anywhere.

Thank you for your time.

Excellent post! I’ve set up a custom repo for a couple of my custom plugins thanks to you! Do you know to add icons too?

I’ve tried this but the sites are not showing an icon:
$this->data[ “icons” ] = [
“1x” => Config::DOMAIN . ‘/’ . Config::DIR . ‘/assets/’ . $this->slug . ‘-icon-128×128.png’,
“2x” => Config::DOMAIN . ‘/’ . Config::DIR . ‘/assets/’ . $this->slug . ‘-icon-256×256.png’,
];

Assuming that format of an array is right, then have on your mind there is an update cache: WordPress cache request to plugin’s repository server certain time so you need manually flush such cache or wait, I think, 6 hours until next update request is make.

Yes, I’ve waited for the cache and, also after 6 hours released a new version, and still no icon. So maybe the format isn’t right. Do you know where I can read about the right format for the json?

↑ Up