JPKComGitPluginUpdater
in package
Class JPKComGitPluginUpdater
Handles plugin updates from GitHub-hosted JSON manifests.
Why a family: WordPress only runs the code of ACTIVE plugins, so a copy of this class inside an inactive plugin never hooks anything and that plugin would never be offered an update. Every JPKCom plugin therefore registers its copy in a shared registry, and the copy with the highest UPDATER_VERSION (ties: the first registered) does the work for every installed JPKCom plugin. All other 1.5+ copies stay passive. Copies older than 1.5 do not know the registry and keep serving their own plugin; the leader leaves plugins whose old copy is loaded alone, so the two never disagree about a manifest or a package.
Table of Contents
Constants
- UPDATER_VERSION : mixed = '1.5.0'
- Version of this updater implementation. Decides the leader election, so it must be raised with every change to the family logic.
- ASSET_PATTERN : mixed = '~^(jpkcom-[a-z0-9-]+)\.zip\z~'
- File name of a JPKCom release asset, lower-cased, as the install gate claims it.
- HOST_FAIL_KEY : mixed = 'jpk_git_update_host_fail'
- Host-wide breaker: one dead or slow github.io must not cost a timeout per plugin.
- MANIFEST_URL_FORMAT : mixed = 'https://jpkcom.github.io/%1$s/plugin_%1$s.json'
- Manifest location for a plugin whose copy is not loaded.
- PACKAGE_PATTERN : mixed = '~^https://github\.com/(?i:JPKCom)/(jpkcom-[a-z...
- Release assets are published as <slug>.zip on the plugin's own repository.
- PLUGIN_URI_PATTERN : mixed = '~^https://github\.com/JPKCom/(jpkcom-[a-z0-9-]...
- A JPKCom plugin is recognised by its Plugin URI, which every released version of every JPKCom plugin carries (unlike the Update URI header, added only in the 2026-10-19 releases), together with the canonical file <slug>/<slug>.php.
- READ_FETCH_BUDGET : mixed = 5
- Uncached manifest fetches allowed per request while rendering update data.
- REGISTRY : mixed = 'jpkcom_git_updater_registry'
- Global registry shared by all copies (each copy lives in its own namespace, so class statics are not shared). Entries are plain arrays with the scalar keys v, id, file, slug, manifest, version. Later versions may add keys but must never rename or retype these.
- SLOW_FETCH_SECONDS : mixed = 3.0
- A successful fetch slower than this (seconds) also trips the host breaker.
- SLUG_PATTERN : mixed = '~^jpkcom-[a-z0-9-]+\z~'
- Slug of a JPKCom plugin, as used for folder and main file.
Properties
- $cache_key : string
- $current_version : string
- $discovered : array<string, array{slug: string, file: string, version: string, manifest: string, frozen: bool}>|null
- $leader_for_size : int
- $leader_id : int|null
- $manifest_url : string
- $members : array<string, array{slug: string, file: string, version: string, manifest: string, frozen: bool}>|null
- $members_signature : string
- $plugin_file : string
- $plugin_slug : string
- $read_fetches : int
- $validated_urls : array<string, bool>
Methods
- __construct() : mixed
- Constructor
- check_update() : mixed
- Inject update information for every family member.
- clear_cache() : void
- Clear cached manifests after an update.
- forget_members() : void
- Drop the plugin scan and member cache after a plugin was deleted or updated.
- plugin_info() : mixed
- Provide detailed plugin info in the “View Details” modal.
- strip_packages() : mixed
- Remove JPKCom package URLs before the update transient is stored.
- verify_download_checksum() : mixed
- Verify download checksum before installation.
- cache_key_for() : string
- Manifest cache key, shared with every copy back to 1.4.0.
- checksum_error() : WP_Error
- Checksum mismatch error.
- discovered() : array<string, array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}>
- Every installed plugin in the canonical <slug>/<slug>.php layout whose Plugin URI is its JPKCom repository, keyed by plugin file.
- elect() : int|null
- Elect the leader among the registered copies.
- entry() : object
- Build one update entry.
- get_remote_manifest() : object|null
- Fetch and decode a remote manifest file.
- icons() : array<string, string>
- Icon URLs from the manifest as an array (core indexes it with []).
- is_background() : bool
- Whether this request is cron or WP-CLI, where latency does not matter.
- is_leader() : bool
- Whether this copy is the elected leader.
- is_plugin_loaded() : bool
- Whether a plugin's main file was actually included in this request.
- is_registered_file() : bool
- Whether a plugin file belongs to a registered (1.5+) copy.
- is_vcs_checkout() : bool
- Whether a plugin directory is a version-control checkout.
- log() : void
- Write a log line in WP_DEBUG mode.
- may_fetch_on_read() : bool
- Whether rendering update data may fetch an uncached manifest.
- member_by_folder() : array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}|null
- Find a member by its folder name (the slug WordPress uses for it).
- members() : array<string, array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}>
- The JPKCom plugins this leader serves, keyed by plugin file.
- package_url() : string
- The manifest's download URL, if it is a release asset of this very plugin.
- register() : void
- Add this copy to the shared registry.
- registry() : array<int, array{v: string, id: int, file: string, slug: string, manifest: string, version: string}>
- Valid registry entries, in registration order.
- remember_failure() : void
- Back off after a failed manifest fetch.
- runs_old_copy() : bool
- Whether a JPKCom plugin's own pre-1.5 updater copy runs in this request.
- text() : string
- A manifest value as plain text, or the fallback when it is not a scalar.
- url() : string
- A manifest value as a sanitised URL, or the fallback.
Constants
UPDATER_VERSION
Version of this updater implementation. Decides the leader election, so it must be raised with every change to the family logic.
public
mixed
UPDATER_VERSION
= '1.5.0'
ASSET_PATTERN
File name of a JPKCom release asset, lower-cased, as the install gate claims it.
private
mixed
ASSET_PATTERN
= '~^(jpkcom-[a-z0-9-]+)\.zip\z~'
HOST_FAIL_KEY
Host-wide breaker: one dead or slow github.io must not cost a timeout per plugin.
private
mixed
HOST_FAIL_KEY
= 'jpk_git_update_host_fail'
MANIFEST_URL_FORMAT
Manifest location for a plugin whose copy is not loaded.
private
mixed
MANIFEST_URL_FORMAT
= 'https://jpkcom.github.io/%1$s/plugin_%1$s.json'
PACKAGE_PATTERN
Release assets are published as <slug>.zip on the plugin's own repository.
private
mixed
PACKAGE_PATTERN
= '~^https://github\.com/(?i:JPKCom)/(jpkcom-[a-z0-9-]+)/releases/download/[^/?#]+/\1\.zip\z~'
The organisation is matched case-insensitively because GitHub resolves it that way: a hand-typed lowercase URL must not slip past the checksum gate.
PLUGIN_URI_PATTERN
A JPKCom plugin is recognised by its Plugin URI, which every released version of every JPKCom plugin carries (unlike the Update URI header, added only in the 2026-10-19 releases), together with the canonical file <slug>/<slug>.php.
private
mixed
PLUGIN_URI_PATTERN
= '~^https://github\.com/JPKCom/(jpkcom-[a-z0-9-]+)/?\z~'
The ~ delimiter matters: none of these patterns may contain it.
READ_FETCH_BUDGET
Uncached manifest fetches allowed per request while rendering update data.
private
mixed
READ_FETCH_BUDGET
= 5
REGISTRY
Global registry shared by all copies (each copy lives in its own namespace, so class statics are not shared). Entries are plain arrays with the scalar keys v, id, file, slug, manifest, version. Later versions may add keys but must never rename or retype these.
private
mixed
REGISTRY
= 'jpkcom_git_updater_registry'
SLOW_FETCH_SECONDS
A successful fetch slower than this (seconds) also trips the host breaker.
private
mixed
SLOW_FETCH_SECONDS
= 3.0
SLUG_PATTERN
Slug of a JPKCom plugin, as used for folder and main file.
private
mixed
SLUG_PATTERN
= '~^jpkcom-[a-z0-9-]+\z~'
Properties
$cache_key
private
string
$cache_key
Cache key for transient
$current_version
private
string
$current_version
Current plugin version
$discovered
private
array<string, array{slug: string, file: string, version: string, manifest: string, frozen: bool}>|null
$discovered
= null
Per-request scan of installed JPKCom plugins
$leader_for_size
private
int
$leader_for_size
= -1
Registry size the cached election result belongs to
$leader_id
private
int|null
$leader_id
= null
Object id of the elected leader
$manifest_url
private
string
$manifest_url
Remote manifest URL
$members
private
array<string, array{slug: string, file: string, version: string, manifest: string, frozen: bool}>|null
$members
= null
Per-request family member cache
$members_signature
private
string
$members_signature
= ''
Registry signature the member cache was built for
$plugin_file
private
string
$plugin_file
Path to main plugin file
$plugin_slug
private
string
$plugin_slug
Plugin slug (directory name)
$read_fetches
private
int
$read_fetches
= 0
Uncached manifest fetches made by the read filter in this request
$validated_urls
private
array<string, bool>
$validated_urls
= []
Per-request memo of wp_http_validate_url() results
Methods
__construct()
Constructor
public
__construct(string $plugin_file, string $current_version, string $manifest_url) : mixed
Parameters
- $plugin_file : string
-
Absolute path to the main plugin file (FILE).
- $current_version : string
-
Current plugin version.
- $manifest_url : string
-
Full URL to the remote JSON manifest.
check_update()
Inject update information for every family member.
public
check_update(mixed $transient) : mixed
Runs on every read of the update_plugins site transient. Entries are built
fresh on each read (so the package that strip_packages() removed before
storage is back for the upgrader) and never carry autoupdate (core would
treat it as a forced auto-update) or translations (installed unverified).
Parameters
- $transient : mixed
-
WordPress transient data.
clear_cache()
Clear cached manifests after an update.
public
clear_cache(mixed $upgrader, mixed $options) : void
Single updates pass plugin, bulk updates (AJAX, update-core, WP-CLI) pass
plugins.
Parameters
- $upgrader : mixed
-
WordPress upgrader instance.
- $options : mixed
-
Upgrade options.
forget_members()
Drop the plugin scan and member cache after a plugin was deleted or updated.
public
forget_members() : void
plugin_info()
Provide detailed plugin info in the “View Details” modal.
public
plugin_info(mixed $result, mixed $action, mixed $args) : mixed
Answers for every family member, so the modal of an inactive plugin works and never falls through to wordpress.org, where a squatted slug would show someone else's plugin.
Parameters
- $result : mixed
-
Default response.
- $action : mixed
-
Current action.
- $args : mixed
-
API request arguments.
strip_packages()
Remove JPKCom package URLs before the update transient is stored.
public
strip_packages(mixed $value) : mixed
The leader re-injects them on every read. A request in which no 1.5+ copy is loaded (every JPKCom plugin deactivated, WP-CLI --skip-plugins, a multisite main site without JPKCom code) then finds an empty package, which core refuses ("Automatic update is unavailable"), instead of a URL it would install without any checksum. Best effort: a third party that writes the option directly bypasses this filter.
Parameters
- $value : mixed
-
Transient value about to be stored.
verify_download_checksum()
Verify download checksum before installation.
public
verify_download_checksum(mixed $reply, mixed $package[, mixed $upgrader = null ][, mixed $hook_extra = [] ]) : mixed
Ownership is decided first: a package belongs to the family when the
upgrader names a member plugin ($hook_extra['plugin'], set for every
update) or when its URL is a JPKCom release asset (installs, which carry no
plugin name). Anything else passes through untouched. For a family package
nothing is trusted without a hash match against the manifest, including a
file an earlier filter already returned.
Parameters
- $reply : mixed
-
Whether to bail without returning the package (default false).
- $package : mixed
-
The package file name or URL.
- $upgrader : mixed = null
-
The WP_Upgrader instance.
- $hook_extra : mixed = []
-
Extra arguments passed to hooked filters.
Return values
mixed —Path to the verified package, $reply when the package is not ours, WP_Error if verification fails.
cache_key_for()
Manifest cache key, shared with every copy back to 1.4.0.
private
cache_key_for(string $slug) : string
Parameters
- $slug : string
-
Plugin slug.
Return values
stringchecksum_error()
Checksum mismatch error.
private
checksum_error(string $calculated[, string $expected = '' ]) : WP_Error
Parameters
- $calculated : string
-
Calculated hash or a placeholder.
- $expected : string = ''
-
Expected hash.
Return values
WP_Errordiscovered()
Every installed plugin in the canonical <slug>/<slug>.php layout whose Plugin URI is its JPKCom repository, keyed by plugin file.
private
discovered() : array<string, array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}>
Reads the headers of jpkcom-* candidates only, not the whole plugin directory, because this runs on every read of the update transient.
Return values
array<string, array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}>elect()
Elect the leader among the registered copies.
private
elect(array<string|int, mixed> $entries) : int|null
Deliberately looser than registry(): any copy inside a jpkcom-* plugin with a comparable version takes part, so a future copy that changes its manifest location or the shape of its entry still wins over this one instead of leaving two leaders.
Parameters
- $entries : array<string|int, mixed>
-
Raw registry.
Return values
int|null —Object id of the leader.
entry()
Build one update entry.
private
entry(array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool} $member, string $new_version, string $package, array<string, string> $icons, string $tested, string $requires, string $requires_php) : object
Parameters
- $member : array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}
-
Member.
- $new_version : string
-
Manifest version.
- $package : string
-
Validated package URL or ''.
- $icons : array<string, string>
-
Icons.
- $tested : string
-
Tested up to.
- $requires : string
-
Requires at least.
- $requires_php : string
-
Requires PHP.
Return values
objectget_remote_manifest()
Fetch and decode a remote manifest file.
private
get_remote_manifest(string $slug, string $manifest_url[, string $context = 'action' ]) : object|null
Uses a locking mechanism to prevent race conditions when multiple requests try to fetch the manifest simultaneously.
Parameters
- $slug : string
-
Plugin slug.
- $manifest_url : string
-
Manifest URL.
- $context : string = 'action'
-
'read' (rendering update data: may be denied a fetch, counts against the request budget of an interactive request, honours the host breaker) or 'action' (install, update, details modal).
Return values
object|null —Decoded manifest or null on failure.
icons()
Icon URLs from the manifest as an array (core indexes it with []).
private
icons(object $remote, string $slug) : array<string, string>
Parameters
- $remote : object
-
Manifest.
- $slug : string
-
Plugin slug.
Return values
array<string, string>is_background()
Whether this request is cron or WP-CLI, where latency does not matter.
private
is_background() : bool
Return values
boolis_leader()
Whether this copy is the elected leader.
private
is_leader() : bool
Highest UPDATER_VERSION wins, ties go to the first registered copy. Not memoised: a transient read at init priority 5 can happen before every copy has registered, and the answer must settle once they have.
Return values
boolis_plugin_loaded()
Whether a plugin's main file was actually included in this request.
private
is_plugin_loaded(string $file) : bool
Asks PHP, not the active_plugins option: wp --skip-plugins=<slug> keeps
the option but never loads the file, and a plugin listed as active but not
loaded must be served like an inactive one.
Parameters
- $file : string
-
Plugin file relative to the plugins directory.
Return values
boolis_registered_file()
Whether a plugin file belongs to a registered (1.5+) copy.
private
is_registered_file(string $file) : bool
Parameters
- $file : string
-
Plugin file.
Return values
boolis_vcs_checkout()
Whether a plugin directory is a version-control checkout.
private
is_vcs_checkout(string $dir) : bool
Updating one replaces the directory and deletes the working tree, so such a member is never offered an update. Same rule and filter as core's automatic updater.
Parameters
- $dir : string
-
Absolute plugin directory.
Return values
boollog()
Write a log line in WP_DEBUG mode.
private
log(string $message) : void
Parameters
- $message : string
-
Message.
may_fetch_on_read()
Whether rendering update data may fetch an uncached manifest.
private
may_fetch_on_read() : bool
Only for users who can act on updates, cron and WP-CLI. An anonymous front end request never fetches; installs, updates and the details modal always may (they pass $context 'action').
Return values
boolmember_by_folder()
Find a member by its folder name (the slug WordPress uses for it).
private
member_by_folder(string $folder) : array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}|null
Parameters
- $folder : string
-
Plugin folder.
Return values
array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}|nullmembers()
The JPKCom plugins this leader serves, keyed by plugin file.
private
members() : array<string, array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}>
Members are (a) every loaded 1.5+ copy from the registry and (b) every discovered JPKCom plugin whose code is NOT loaded in this request. A plugin that is loaded but not registered runs an older updater copy, which serves it against its own cache; the leader stays out of its way.
A member is "frozen" (only ever listed as up to date, never offered a package) when it is a version-control checkout, which an update would wipe, or when its folder is not named after its slug (e.g. jpkcom-foo-main from a GitHub source download): the release ZIP unpacks into jpkcom-foo/, so an update would delete the renamed folder and leave the active plugin missing.
The cache is tied to the registry, because copies register one by one during init and a transient read can happen in between.
Return values
array<string, array{slug: string, folder: string, file: string, version: string, manifest: string, frozen: bool}>package_url()
The manifest's download URL, if it is a release asset of this very plugin.
private
package_url(object $remote, string $slug) : string
Anything else is never handed to WordPress, so the checksum gate always recognises what this class emits.
Parameters
- $remote : object
-
Manifest.
- $slug : string
-
Plugin slug.
Return values
string —Validated URL or ''.
register()
Add this copy to the shared registry.
private
register() : void
registry()
Valid registry entries, in registration order.
private
registry() : array<int, array{v: string, id: int, file: string, slug: string, manifest: string, version: string}>
Malformed entries and entries that do not describe a JPKCom plugin (main
file
Return values
array<int, array{v: string, id: int, file: string, slug: string, manifest: string, version: string}>remember_failure()
Back off after a failed manifest fetch.
private
remember_failure(string $cache_key, bool $host_wide) : void
Keeps a short-lived marker so the next requests skip the remote call instead of re-running it (and its timeout) on every page load.
Parameters
- $cache_key : string
-
Manifest cache key.
- $host_wide : bool
-
Whether the failure concerns the host, not the plugin.
runs_old_copy()
Whether a JPKCom plugin's own pre-1.5 updater copy runs in this request.
private
runs_old_copy(string $file) : bool
Parameters
- $file : string
-
Plugin file.
Return values
booltext()
A manifest value as plain text, or the fallback when it is not a scalar.
private
text(mixed $value[, string $fallback = '' ]) : string
Manifests are remote data and are cached by every copy since 1.4.0; an object or array where a string belongs must not turn into a fatal error on every admin page.
Parameters
- $value : mixed
-
Manifest value.
- $fallback : string = ''
-
Value for a missing or non-scalar field.
Return values
stringurl()
A manifest value as a sanitised URL, or the fallback.
private
url(mixed $value[, string $fallback = '' ]) : string
Parameters
- $value : mixed
-
Manifest value.
- $fallback : string = ''
-
Value for a missing or non-string field.