Custom Updater – Work In Progress

Tech Articles | April 17, 2025 | Blog, Coding, Plugins, Wordpress

In the last few weeks, I’ve made some good progress with SDKS for Hoster, WP Software License & WooCommerce API Manager, which gives me a pretty universal setup for future development if I don’t want to use SureCart. In fact,, using these custom SDK’S, I can deploy a “woocommerce site” and have it serving a license and updates in a matter of minutes to a new plugin. My work on a Quadmenus version has stalled due to support times and a worrying ability to “generate orders” using the secret key.

My free plugins update using a slightly modified version of Yahnis Elsts plugin-update-checker and connect to a server running a reasonably heavily modified wp-update-server from the same author, a version of which can be found here with added API authentication so each plugin has its own “key” and will only serve that plugin as an update.

I have begun working on a custom integration for the plugins to reduce the updater size from , and this is the initial version of that updater.

<?php
/**
 * Universal Updater Drop‑In for Plugins & Themes
 * Encapsulated in UUPD_Updater – safe to include multiple times.
 *
 * Usage:
 *
 *   1) Copy this file to `inc/updater.php` inside your plugin or theme.
 *
 *   2) In a **plugin**, in your main plugin file (e.g. `rup-changelogger.php`), hard‑code your version
 *      and bootstrap on `plugins_loaded` + `admin_init` to avoid text‑domain warnings:
 *
 *        // Define your plugin version
 *        define( 'RUP_PLUGIN_VERSION', '1.0' );
 *
 *      // Register updater early enough for the Updates screen
 *        add_action( 'plugins_loaded', function() {
            $updater_config = [
                'plugin_file' => plugin_basename( __FILE__ ),
                'slug'        => 'rup-changelogger',  // "rup-changelogger"
                'name'        => 'Changelogger',        // "Changelogger"
                'version'     => RUP_PLUGIN_VERSION,     // "1.01"
                'key'         => 'YourSecretKeyHere',
                'server'      => 'https://updater.reallyusefulplugins.com/u/',
            ];
 *            require_once __DIR__ . '/inc/updater.php';
 *            new UUPD_Updater( $updater_config );
 *        } );
 *
 *   3) In a **theme**, in your theme’s `functions.php`, bootstrap on `after_setup_theme` + `admin_init`:
 *
 *        add_action( 'after_setup_theme', function() {
 *            $updater_config = [
 *                'slug'    => 'test-updater-theme',    // match your theme folder & Text Domain
 *                'name'    => 'Test Updater Theme',    // your theme’s human name
 *                'version' => '1.0.0',                 // match your style.css Version header
 *                'key'     => 'YourSecretKeyHere',
 *                'server'  => 'https://updater.reallyusefulplugins.com/u/',
 *            ];
 *            require get_stylesheet_directory() . '/inc/updater.php';
 *            // register update‐checker in time for WP Admin Updates
 *            add_action( 'admin_init', function() use ( $updater_config ) {
 *                new UUPD_Updater( $updater_config );
 *            } );
 *        } );
 *
 *   4) To enable logging of every HTTP call and injection step, add anywhere:
 *
 *        add_filter( 'updater_enable_debug', fn( $e ) => true );
 *
 *   5) In your `wp-config.php`, be sure to turn on WP_DEBUG_LOG:
 *
 *        define( 'WP_DEBUG',     true );
 *        define( 'WP_DEBUG_LOG', true );
 *
 * Now your updater will:
 *  - Fetch `/u/?action=get_metadata&slug={slug}&key={key}` on‐demand
 *  - Cache the JSON for 1 hour
 *  - Inject plugin updates if `plugin_file` is present
 *  - Inject theme updates otherwise
 *  - Populate the “View details” dialogs with your changelog
 */

if ( ! class_exists( 'UUPD_Updater' ) ) {

    class UUPD_Updater {

        /** @var array */
        private $config;

        /**
         * @param array $config {
         *   string 'slug'        Plugin or theme slug.
         *   string 'name'        Human‐readable name.
         *   string 'version'     Current version.
         *   string 'key'         Your secret key.
         *   string 'server'      Base URL of your updater endpoint.
         *   string 'plugin_file' Only for plugins: plugin_basename(__FILE__).
         * }
         */
        public function __construct( array $config ) {
            $this->config = $config;
            $this->register_hooks();
        }

        /** Attach only the appropriate hooks. */
        private function register_hooks() {
            if ( ! empty( $this->config['plugin_file'] ) ) {
                add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'plugin_update' ], 10, 1 );
                add_filter( 'plugins_api',                          [ $this, 'plugin_info' ],   10, 3 );
            } else {
                add_filter( 'pre_set_site_transient_update_themes', [ $this, 'theme_update' ],  10, 1 );
                add_filter( 'themes_api',                           [ $this, 'theme_info' ],     10, 3 );
            }
        }

        /** Perform the HTTP request and cache the JSON for 6 hours. */
        private function fetch_remote() {
            $c    = $this->config;
            $slug = rawurlencode( $c['slug'] );
            $key  = rawurlencode( $c['key'] );

            // get the current site’s host (e.g. “example.com”)
            $home = untrailingslashit( home_url() );
            $host = rawurlencode( wp_parse_url( $home, PHP_URL_HOST ) );

            // build URL with domain= query-param
            $url = untrailingslashit( $c['server'] ) 
                 . "/?action=get_metadata&slug={$slug}&key={$key}&domain={$host}";

            $this->log( "→ Fetching metadata: {$url}" );
            $resp = wp_remote_get( $url, [
                'timeout' => 15,
                'headers' => [
                    'Accept'     => 'application/json',
                    'User-Agent' => 'WordPress/' . get_bloginfo('version'),
                ],
            ] );

            if ( is_wp_error( $resp ) ) {
                return $this->log( '✗ HTTP error: ' . $resp->get_error_message() );
            }

            $code = wp_remote_retrieve_response_code( $resp );
            $body = wp_remote_retrieve_body( $resp );
            $this->log( "← HTTP {$code}: " . trim( $body ) );
            if ( 200 !== (int) $code ) {
                return;
            }

            $meta = json_decode( $body );
            if ( ! $meta ) {
                return $this->log( '✗ JSON decode failed: ' . var_export( $body, true ) );
            }

            // Ensure expected properties exist
            foreach ( [ 'version','tested','requires','changelog_html','homepage','download_url' ] as $f ) {
                if ( ! isset( $meta->$f ) ) {
                    $meta->$f = '';
                }
            }

            // Cache for 6 hours
            set_transient( 'upd_' . $c['slug'], $meta, 6 * HOUR_IN_SECONDS );
            $this->log( "✓ Cached metadata for '{$c['slug']}' → v{$meta->version} (6h)" );
        }


        /** Plugin: inject update when remote > current */
        public function plugin_update( $trans ) {
            $c    = $this->config;
            $slug = $c['slug'];
            $file = $c['plugin_file'];

            $this->log( "→ Plugin-update hook for '{$slug}'" );
            $current = $trans->checked[ $file ] ?? $c['version'];
            $this->log( "    Current plugin v{$current}" );

            // On-demand fetch if missing
            if ( false === ( $meta = get_transient( 'upd_' . $slug ) ) ) {
                $this->fetch_remote();
                $meta = get_transient( 'upd_' . $slug );
            }
            if ( ! $meta ) {
                return $trans;
            }
            $this->log( "    Remote plugin v{$meta->version}" );

            if ( version_compare( $meta->version, $current, '<=' ) ) {
                $this->log( "    ✗ No newer version" );
                return $trans;
            }

            $this->log( "    ✓ Injecting plugin update → v{$meta->version}" );
            $trans->response[ $file ] = (object) [
                'slug'        => $slug,
                'new_version' => $meta->version,
                'package'     => $meta->download_url,
                'tested'      => $meta->tested,
                'requires'    => $meta->requires,
                'sections'    => [ 'changelog' => $meta->changelog_html ],
            ];

            return $trans;
        }

        /** Plugin: details popup */
        public function plugin_info( $res, $action, $args ) {
            $c = $this->config;
            if ( 'plugin_information' !== $action || $args->slug !== $c['slug'] ) {
                return $res;
            }
            $meta = get_transient( 'upd_' . $c['slug'] );
            if ( ! $meta ) {
                return $res;
            }
            return (object) [
                'name'          => $c['name'],
                'slug'          => $c['slug'],
                'version'       => $meta->version,
                'tested'        => $meta->tested,
                'requires'      => $meta->requires,
                'sections'      => [ 'changelog' => $meta->changelog_html ],
                'download_link' => $meta->download_url,
            ];
        }

        /** Theme: inject update when remote > current */
        public function theme_update( $trans ) {
            $c    = $this->config;
            $slug = $c['slug'];

            $this->log( "→ Theme-update hook for '{$slug}'" );
            $current = $trans->checked[ $slug ] ?? wp_get_theme( $slug )->get( 'Version' );
            $this->log( "    Current theme v{$current}" );

            // On-demand fetch if missing
            if ( false === ( $meta = get_transient( 'upd_' . $slug ) ) ) {
                $this->fetch_remote();
                $meta = get_transient( 'upd_' . $slug );
            }
            if ( ! $meta ) {
                return $trans;
            }
            $this->log( "    Remote theme v{$meta->version}" );

            if ( version_compare( $meta->version, $current, '<=' ) ) {
                $this->log( "    ✗ No newer version" );
                return $trans;
            }

            $this->log( "    ✓ Injecting theme update → v{$meta->version}" );
            $trans->response[ $slug ] = [
                'theme'       => $slug,
                'new_version' => $meta->version,
                'package'     => $meta->download_url,
                'url'         => $meta->homepage,
            ];

            return $trans;
        }

        /** Theme: details popup */
        public function theme_info( $res, $action, $args ) {
            $c = $this->config;
            if ( 'theme_information' !== $action || $args->slug !== $c['slug'] ) {
                return $res;
            }
            $meta = get_transient( 'upd_' . $c['slug'] );
            if ( ! $meta ) {
                return $res;
            }
            return (object) [
                'name'          => $c['name'],
                'slug'          => $c['slug'],
                'version'       => $meta->version,
                'tested'        => $meta->tested,
                'requires'      => $meta->requires,
                'sections'      => [ 'changelog' => $meta->changelog_html ],
                'download_link' => $meta->download_url,
                'homepage'      => $meta->homepage,
            ];
        }

        /** Optional debug logger */
        private function log( $msg ) {
            if ( apply_filters( 'updater_enable_debug', false ) ) {
                error_log( "[Updater] {$msg}" );
            }
        }
    }
}
PHP

My plan with this is simple: to have a much lighter integration with the free plugins. This means some of the smaller ones will be much smaller in size and will fully auto-update using my custom endpoint and the set license keys. What’s more, it’s designed to work with WP Update Server

The Good News is I am very nearly done with the boring dev prep and can get back to creating something again

27th April 2025 – Small Update to send domain details with the update

Support the Author

buy me a coffee
Really Useful Plugin Logo
Appoligies for any spelling and grammer issue. As a dyslexic i need to rely on tools for this they like me are not perfect but I do try my best