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}" );
}
}
}
}
PHPMy 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