HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
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>',
        ],
    ];
}