File: /var/www/kevin-demo/wp-content/plugins/allspice/includes/self-update.php
<?php
// includes/self-update.php
//
// Self-hosted plugin updates. wordpress.org is not involved.
//
// The `Update URI: https://portal.allspicelabs.com/plugin` header in allspice.php does two
// things: it permanently prevents wordpress.org from ever serving an update for the
// "allspice" slug (so the public repo cannot shadow this plugin onto partner sites), and it
// makes core invoke the `update_plugins_portal.allspicelabs.com` filter below on every
// twice-daily update check. We answer it from a manifest we host next to the zip:
//
// https://portal.allspicelabs.com/plugin/manifest.json
// { "version": "1.5.01.1", "package": ".../plugin/allspice.zip",
// "requires": "6.0", "tested": "6.7", "requires_php": "7.4",
// "changelog_html": "<p>...</p>" }
//
// From there WordPress treats it like a repo plugin: the update badge, one-click update,
// the per-plugin auto-update toggle, and `wp plugin update allspice` all work natively.
// The package url stays stable (always allspice.zip), and the zip must unpack to an
// `allspice/` folder or core installs the update beside the current copy instead of
// over it.
//
// RELEASING (the whole flow):
// 1. Bump the header `Version:` in allspice.php - 4th segment only (1.5.01.1, 1.5.01.2, …).
// The hosted asset directories stay on the 1.5.01 channel (ALLSPICE_ASSETS_CHANNEL);
// the release version exists so version_compare can see the update.
// 2. Rebuild allspice.zip from v1.5.01/allspice/ and replace it in the portal repo's
// public/plugin/ (dev repo; the prod sync carries it over).
// 3. Set the same version in public/plugin/manifest.json.
// 4. Deploy the portal frontends. Sites see the update within ~12 hours, or immediately
// via Dashboard -> Updates -> "Check again".
if (!defined('ABSPATH')) exit;
const ALLSPICE_UPDATE_MANIFEST_URL = 'https://portal.allspicelabs.com/plugin/manifest.json';
const ALLSPICE_UPDATE_CACHE_KEY = 'allspice_update_manifest_v1';
/* Success cache matches core's twice-daily check cadence; failures retry sooner but still
rarely enough that a portal outage cannot turn every partner site into a hammer. */
const ALLSPICE_UPDATE_CACHE_TTL = 12 * HOUR_IN_SECONDS;
const ALLSPICE_UPDATE_FAILURE_TTL = HOUR_IN_SECONDS;
/*
* The manifest, cached. Returns a validated array or null; null is also cached (briefly)
* so a broken/unreachable manifest degrades to "no update offered", never to an error
* anywhere in wp-admin.
*/
function allspice_update_manifest() {
$cached = get_transient(ALLSPICE_UPDATE_CACHE_KEY);
if ($cached !== false) {
return is_array($cached) ? $cached : null; /* 'failed' sentinel -> null */
}
$manifest = null;
$response = wp_remote_get(ALLSPICE_UPDATE_MANIFEST_URL, [
'timeout' => 5,
'headers' => ['Accept' => 'application/json'],
]);
if (!is_wp_error($response) && (int) wp_remote_retrieve_response_code($response) === 200) {
$json = json_decode((string) wp_remote_retrieve_body($response), true);
$manifest = allspice_update_validate_manifest($json);
}
if ($manifest === null) {
set_transient(ALLSPICE_UPDATE_CACHE_KEY, 'failed', ALLSPICE_UPDATE_FAILURE_TTL);
return null;
}
set_transient(ALLSPICE_UPDATE_CACHE_KEY, $manifest, ALLSPICE_UPDATE_CACHE_TTL);
return $manifest;
}
/*
* Keep only a well-formed manifest: a plausible version and an https package url on OUR
* host. The host pin means that even a tampered/misconfigured manifest can never point
* partner sites at a zip we do not control.
*/
function allspice_update_validate_manifest($json) {
if (!is_array($json)) return null;
$version = isset($json['version']) ? trim((string) $json['version']) : '';
$package = isset($json['package']) ? trim((string) $json['package']) : '';
if ($version === '' || !preg_match('/^\d+(\.\d+)*$/', $version)) return null;
$host = wp_parse_url($package, PHP_URL_HOST);
if (!preg_match('#^https://#i', $package) || $host !== 'portal.allspicelabs.com') return null;
return [
'version' => $version,
'package' => $package,
'requires' => isset($json['requires']) ? (string) $json['requires'] : '',
'requires_php' => isset($json['requires_php']) ? (string) $json['requires_php'] : '',
'tested' => isset($json['tested']) ? (string) $json['tested'] : '',
'changelog_html' => isset($json['changelog_html']) ? (string) $json['changelog_html'] : '',
];
}
/* The update payload core expects, or null while current. */
function allspice_update_build_response() {
$manifest = allspice_update_manifest();
if ($manifest === null) return null;
$current = defined('ALLSPICE_PLUGIN_VERSION') ? ALLSPICE_PLUGIN_VERSION : allspice_get_plugin_version();
if (!version_compare($manifest['version'], $current, '>')) return null;
return [
'id' => 'portal.allspicelabs.com/plugin/allspice',
'slug' => 'allspice',
'plugin' => plugin_basename(ALLSPICE_PLUGIN_FILE),
'version' => $manifest['version'],
'new_version' => $manifest['version'],
'url' => 'https://portal.allspicelabs.com/plugin',
'package' => $manifest['package'],
'requires' => $manifest['requires'],
'requires_php' => $manifest['requires_php'],
'tested' => $manifest['tested'],
];
}
/*
* WP >= 5.8: core routes update checks for plugins carrying our Update URI hostname here.
* Return value contract: an update array when one exists, false to say "checked, current"
* (which stops core from asking wordpress.org), $update untouched only to abstain.
*/
add_filter('update_plugins_portal.allspicelabs.com', 'allspice_update_uri_check', 10, 3);
function allspice_update_uri_check($update, $plugin_data, $plugin_file) {
if ($plugin_file !== plugin_basename(ALLSPICE_PLUGIN_FILE)) return $update;
$response = allspice_update_build_response();
return $response !== null ? $response : false;
}
/*
* WP < 5.8 knows nothing of Update URI, so inject into the transient directly there.
* Guarded to old versions only - on modern WP the hook above owns the answer, and
* double-writing the same entry from two hooks invites drift between them.
*/
add_filter('pre_set_site_transient_update_plugins', 'allspice_update_transient_fallback');
function allspice_update_transient_fallback($transient) {
if (version_compare(get_bloginfo('version'), '5.8', '>=')) return $transient;
if (!is_object($transient)) return $transient;
$response = allspice_update_build_response();
if ($response === null) return $transient;
if (!isset($transient->response) || !is_array($transient->response)) {
$transient->response = [];
}
$transient->response[plugin_basename(ALLSPICE_PLUGIN_FILE)] = (object) $response;
return $transient;
}
/*
* "View details" modal. Without this, the version link on the Plugins screen asks
* wordpress.org about a slug it has never heard of and renders an error inside the modal.
*/
add_filter('plugins_api', 'allspice_update_plugin_details', 10, 3);
function allspice_update_plugin_details($result, $action, $args) {
if ($action !== 'plugin_information') return $result;
if (!is_object($args) || !isset($args->slug) || $args->slug !== 'allspice') return $result;
$manifest = allspice_update_manifest();
$current = defined('ALLSPICE_PLUGIN_VERSION') ? ALLSPICE_PLUGIN_VERSION : allspice_get_plugin_version();
return (object) [
'name' => 'Allspice',
'slug' => 'allspice',
'version' => $manifest !== null ? $manifest['version'] : $current,
'author' => 'Allspice Labs, Inc.',
'homepage' => 'https://allspicelabs.com',
'requires' => $manifest['requires'] ?? '',
'requires_php' => $manifest['requires_php'] ?? '',
'tested' => $manifest['tested'] ?? '',
'download_link' => $manifest['package'] ?? '',
'sections' => [
'description' => '<p>Loads the Allspice widget and analytics packages.</p>',
'changelog' => ($manifest['changelog_html'] ?? '') !== ''
? $manifest['changelog_html']
: '<p>See your Allspice publisher portal for release notes.</p>',
],
];
}