ARGUS WordPress Defence 7.23.1 -- real, working automatic updates
Fixes a real bug: a normal (non-critical) update was previously detected and shown as available, but nothing ever actually installed it -- there was no button, cron path, or any other code that did. Automatic updates only ever worked for updates flagged critical, which isn't what "automatic" means. Now any newer, compatible, signature-verified update installs on its own. This is also the first release where the self-update mechanism itself ships in this self-distributed channel -- it was unconditionally excluded from every previous build (a WordPress.org-only restriction that doesn't apply here, since this channel isn't WordPress.org). manifest.json in this repo is the real, live update manifest: signed with Ed25519 (public key documented in README.md's Updating section), pointing at this exact release's ZIP and its real SHA-256. Verified end-to-end before publishing -- not just "the code looks right": ran a full real update cycle (an older installed version checking this manifest, downloading this exact package, verifying its signature and hash, replacing itself, and the site continuing to work with zero errors afterward) using the actual signing key and the actual package this commit ships. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+21
-8
@@ -8,12 +8,20 @@
|
||||
# the result against an explicit allowlist before packaging, failing
|
||||
# closed on anything unexpected rather than silently shipping it.
|
||||
#
|
||||
# Usage: bin/build-release.sh [git-ref] (defaults to HEAD)
|
||||
# Usage: bin/build-release.sh [git-ref] [channel]
|
||||
# channel: "self" (default) -- the self-distributed release (Gitea
|
||||
# releases page), includes the signed self-update client, since
|
||||
# nothing prohibits it outside WordPress.org.
|
||||
# "wporg" -- strips the self-update client too (and the
|
||||
# license/trial system, stripped either way) -- required before
|
||||
# any submission to the WordPress.org Plugin Directory, which
|
||||
# prohibits a plugin using any update channel but its own.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REF="${1:-HEAD}"
|
||||
CHANNEL="${2:-self}"
|
||||
BUILD_DIR="$(mktemp -d)"
|
||||
STAGE_DIR="${BUILD_DIR}/argus-wordpress-defence"
|
||||
OUT_DIR="${REPO_ROOT}/dist"
|
||||
@@ -35,13 +43,18 @@ rm -rf \
|
||||
"${STAGE_DIR}/.gitignore" \
|
||||
"${STAGE_DIR}/bin"
|
||||
|
||||
# A plugin hosted on WordPress.org must rely solely on WordPress.org's own
|
||||
# update channel -- never a self-update-from-external-manifest mechanism,
|
||||
# even one that ships inert-by-default. class_exists( 'Argus_Update_Client' )
|
||||
# guards every reference to this class elsewhere in the codebase, so removing
|
||||
# the file here is sufficient; nothing else needs to change per build.
|
||||
echo "==> Removing the self-update client (WordPress.org must be the only update channel)"
|
||||
rm -f "${STAGE_DIR}/includes/class-argus-update-client.php"
|
||||
if [ "${CHANNEL}" = "wporg" ]; then
|
||||
# A plugin hosted on WordPress.org must rely solely on WordPress.org's own
|
||||
# update channel -- never a self-update-from-external-manifest mechanism,
|
||||
# even one that ships inert-by-default. class_exists( 'Argus_Update_Client' )
|
||||
# guards every reference to this class elsewhere in the codebase, so
|
||||
# removing the file here is sufficient; nothing else needs to change.
|
||||
echo "==> [wporg channel] Removing the self-update client (WordPress.org must be the only update channel)"
|
||||
rm -f "${STAGE_DIR}/includes/class-argus-update-client.php"
|
||||
elif [ "${CHANNEL}" != "self" ]; then
|
||||
echo "==> BUILD FAILED: unknown channel '${CHANNEL}' (expected 'self' or 'wporg')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# WordPress.org explicitly prohibits trialware -- a plugin submitted to the
|
||||
# directory must stay genuinely, permanently functional with no license
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Signs an update manifest for Argus_Update_Client (includes/class-argus-update-client.php).
|
||||
* Never shipped in any release ZIP -- a dev/release-time tool only.
|
||||
*
|
||||
* Usage:
|
||||
* php bin/sign-manifest.php \
|
||||
* --private-key=<base64 Ed25519 secret key> \
|
||||
* --version=7.23.1 \
|
||||
* --package-url=https://.../argus-wordpress-defence-7.23.1.zip \
|
||||
* --sha256=<hex sha256 of that exact zip> \
|
||||
* [--min-php=7.4] [--min-wp=6.0] [--critical] \
|
||||
* [--out=manifest.json]
|
||||
*
|
||||
* The signed payload's field set and order (version, released_at,
|
||||
* package_url, sha256, min_php, min_wp, critical) MUST exactly match
|
||||
* Argus_Update_Client::canonical_payload() -- any drift and every
|
||||
* signature this produces fails verification client-side.
|
||||
*/
|
||||
|
||||
$args = array();
|
||||
foreach ( $argv as $arg ) {
|
||||
if ( 0 === strpos( $arg, '--' ) ) {
|
||||
$parts = explode( '=', substr( $arg, 2 ), 2 );
|
||||
$args[ $parts[0] ] = $parts[1] ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
function required( $args, $key ) {
|
||||
if ( empty( $args[ $key ] ) ) {
|
||||
fwrite( STDERR, "Missing required --{$key}\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
return $args[ $key ];
|
||||
}
|
||||
|
||||
$private_key_b64 = required( $args, 'private-key' );
|
||||
$version = required( $args, 'version' );
|
||||
$package_url = required( $args, 'package-url' );
|
||||
$sha256 = required( $args, 'sha256' );
|
||||
$min_php = $args['min-php'] ?? '7.4';
|
||||
$min_wp = $args['min-wp'] ?? '6.0';
|
||||
$critical = ! empty( $args['critical'] );
|
||||
$released_at = gmdate( 'c' );
|
||||
$out = $args['out'] ?? ( dirname( __DIR__ ) . '/manifest.json' );
|
||||
|
||||
if ( 64 !== strlen( $sha256 ) || ! ctype_xdigit( $sha256 ) ) {
|
||||
fwrite( STDERR, "--sha256 must be a 64-character hex string (run: sha256sum <zip>)\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
$private_key = base64_decode( $private_key_b64, true );
|
||||
if ( false === $private_key || SODIUM_CRYPTO_SIGN_SECRETKEYBYTES !== strlen( $private_key ) ) {
|
||||
fwrite( STDERR, "--private-key is not a valid base64-encoded Ed25519 secret key\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
// Must match Argus_Update_Client::canonical_payload() exactly -- same key
|
||||
// set, same order, same JSON_UNESCAPED_SLASHES flag.
|
||||
$ordered = array(
|
||||
'version' => $version,
|
||||
'released_at' => $released_at,
|
||||
'package_url' => $package_url,
|
||||
'sha256' => $sha256,
|
||||
'min_php' => $min_php,
|
||||
'min_wp' => $min_wp,
|
||||
'critical' => $critical,
|
||||
);
|
||||
$canonical = json_encode( $ordered, JSON_UNESCAPED_SLASHES );
|
||||
|
||||
$signature = sodium_crypto_sign_detached( $canonical, $private_key );
|
||||
$manifest = $ordered;
|
||||
$manifest['signature'] = base64_encode( $signature );
|
||||
|
||||
file_put_contents( $out, json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) . "\n" );
|
||||
|
||||
echo "Wrote {$out}\n";
|
||||
echo " version: {$version}\n";
|
||||
echo " package_url: {$package_url}\n";
|
||||
echo " sha256: {$sha256}\n";
|
||||
echo " critical: " . ( $critical ? 'true' : 'false' ) . "\n";
|
||||
Reference in New Issue
Block a user