=== Patch Gap – Update Coverage & Licence Auditor ===
Contributors: lukeaxiomflow, freemius
Tags: security, updates, license, premium plugins, maintenance
Requires at least: 6.2
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 1.0.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Find the plugins and themes nothing is updating. A lapsed licence makes a premium plugin go silent — and silence looks just like "up to date".

== Description ==

**Your updates page says everything is fine. It might be lying to you.**

WordPress.org plugins get a safety net: WordPress checks them for updates automatically, and security plugins warn you when one is abandoned.

**Premium plugins get nothing.**

When a premium plugin's licence expires, its key goes missing, it was hand-uploaded from a zip, or its vendor quietly disappears, the plugin's updater stops answering. And here is the dangerous part: a plugin that *cannot* be updated does not show a warning. It shows **nothing at all** — which looks identical to a plugin that is perfectly up to date.

So the plugin sits there, at the version it was when the licence lapsed, and never receives another security patch. Your dashboard stays green. Your fleet-management tool stays green. Nobody tells you.

Patch Gap closes that blind spot. It forces every update source on the site to answer, then reports **who — if anyone — is actually patching each plugin and theme**.

= What it tells you =

Every plugin and theme is sorted into one of these:

* **Covered — WordPress.org.** WordPress checks it for you.
* **Covered — vendor updater active.** A licensed premium plugin whose updater is registered and responding. Working correctly.
* **GAP — updater present but silent.** The component ships licence/updater code (Freemius, EDD Software Licensing, Plugin Update Checker, WooCommerce API Manager…) but registered no update source. Its licence has almost certainly expired, its key is missing, or it was installed outside the vendor's channel. **It will never be patched again.**
* **GAP — declares an update source that never answers.** It advertises an Update URI, but nothing responds. The vendor may be gone.
* **Abandoned upstream.** Still on WordPress.org, but not updated in years — or closed entirely.
* **No updater (bespoke).** Custom or in-house code with no update source and no licence — expected, so Patch Gap lists it without treating it as a problem.

That last one matters: a naive "is it in the update list?" check would scream about every custom plugin an agency has ever written. Patch Gap looks at four independent signals before it calls something a gap, so it flags the components that are genuinely unpatchable — not the ones that are simply bespoke.

= Why this is worth your attention =

Patchstack's 2026 security report attributes **29% of all valid WordPress vulnerability reports to premium and freemium components**, and a majority of those are the kind attackers automate at scale. Premium components are simultaneously the ones nothing version-checks.

= Also included, free =

* **Licence roster** — the licence states Patch Gap can read from your own site (expired, active, expiry dates), without contacting any vendor.
* **Weekly re-check** with an email the moment a component *loses* its update source (that is usually the week a licence expired — the moment you can still fix it cheaply).
* **Site Health integration** and a dashboard summary.
* **WP-CLI**: `wp patch-gap scan` — including `--format=json`.
* Dismiss any finding with a note — useful as an audit record ("this one is deliberate").

The free plugin is complete for auditing a site: nothing above is limited, metered, or unlocked by payment.

= Read-only, by design =

Patch Gap **never** updates, deactivates, rolls back, or modifies any plugin, theme, or setting. It is strictly read-only: it asks every update source whether it is listening, and reports the answer.

**[Patch Gap Pro](https://buildsbyluke.com/plugins/patch-gap)** adds the things agencies need on top: a white-label client-facing coverage report (PDF/print), a licence-renewal roster you can invoice against, Slack/webhook alerts on new gaps, and a REST coverage endpoint for fleet dashboards. Sold and licensed through Freemius (see "External services").

== External services ==

**The audit itself runs on your own server.** Licence information is read from your own site's options and is **never** transmitted anywhere.

**WordPress.org (api.wordpress.org)**

* A scan asks WordPress to run its normal update check — the same one your site already runs on a schedule. WordPress core contacts api.wordpress.org exactly as it always does, and any premium plugin's own updater contacts its own vendor exactly as it always does (that is the mechanism Patch Gap observes; it adds no data of its own to those requests).
* Patch Gap additionally queries the public WordPress.org Plugin API (api.wordpress.org) to check whether a WordPress.org-hosted plugin has been abandoned or closed upstream. Only the plugin's public slug is sent — nothing about your site, its users, or its configuration. Responses are cached for a day, and a circuit breaker stops these lookups entirely if wp.org is unreachable. [WordPress.org privacy policy](https://wordpress.org/about/privacy/).

**Freemius (freemius.com), who sell and license the Pro version**

Freemius is contacted in four situations. Every one of them is something you click. It is never contacted in the background.

1. **If you opt in on the activation screen** (or start a trial / activate a licence). Sent to api.freemius.com: your site URL, your WordPress and PHP versions, and the email address of the account you activate with. Skipping the opt-in is a first-class choice and the plugin is fully functional without it.
2. **If you open the "Upgrade" page** under the plugin's menu. Your own server (not your browser) asks api.freemius.com for current plan prices, sending your site URL. The page itself loads no third-party scripts — everything it renders is served from this plugin's own folder. The payment SDK's bundled pricing script used to inject Google Analytics and a remote checkout script into wp-admin on that page; this build removes both, along with the SDK's remaining remote references (all modifications are listed under "Source code" below).
3. **If you open the "Contact Us" page**, which is Freemius's hosted support form (wp.freemius.com). The link carries your site URL and your WordPress login URL so the form knows which site you are writing about.
4. **If you click a plan to buy**, you go to Freemius's hosted checkout (checkout.freemius.com). It receives your site URL, site name, WordPress and PHP versions, and your WordPress administrator email address (pre-filled, sent whether or not you complete the purchase). Freemius is the merchant of record. Their checkout page loads its own third-party scripts (at the time of writing: Stripe, PayPal, Google Tag Manager, and Freemius's own assets); we do not control that list. If you never click a plan, none of it loads.

Terms: https://freemius.com/terms/ — Privacy: https://freemius.com/privacy/

Installing, activating, skipping the opt-in, scanning, using every screen, deactivating (the SDK's deactivation-feedback dialog is switched off in this build, so deactivation is one click and sends nothing), WordPress's update cycle and all scheduled tasks complete without contacting Freemius at all. Free updates come from WordPress.org like any other plugin.

== Source code ==

This plugin's own PHP is unminified and readable in the plugin folder.

The one exception to "unminified" is the third-party payment SDK in `vendor/freemius/`, which ships pre-minified. Those files are the Freemius WordPress SDK, published under the GPL, and their unminified source and build tooling are public:

* Freemius WordPress SDK: https://github.com/Freemius/wordpress-sdk
* The pricing screen bundled at `vendor/freemius/assets/js/pricing/freemius-pricing.js`: https://github.com/Freemius/pricing-page

This plugin ships four deliberate modifications to that SDK, all in `assets/js/pricing/freemius-pricing.js`, all removing remote references from wp-admin. Three are marked with a "Patch Gap:" comment at the patch site; the loader-image swap (3.) is a one-string replacement:

1. The `appendScripts()` method is emptied. Upstream it injects `https://www.google-analytics.com/analytics.js` and `https://checkout.freemius.com/checkout.js` into wp-admin when the pricing screen renders.
2. The Google Analytics pageview tracker is stubbed out. Upstream it reports pricing-page views to Freemius's Analytics property whenever a GA object is already present in wp-admin, stamping them with the Freemius user id.
3. The remote loading spinner (`img.freemius.com/blue-loader.gif`) is replaced with an inline data-URI equivalent.
4. Testimonial photos (Gravatar/remote) are forced to the bundled placeholder image, so the pricing page renders without any remote image requests.

== Frequently Asked Questions ==

= Why doesn't WordPress warn me about this already? =

Because WordPress cannot tell the difference between "this plugin has no update available" and "nothing will ever offer this plugin an update again". Both look like silence. Patch Gap distinguishes them by asking *who is responsible* for each component, rather than *is there an update right now*.

= Will it flag my custom plugins? =

It will list them, but it will not treat them as a problem. Custom code with no updater and no licence is classified as "bespoke" — informational only. Patch Gap only raises a gap when a component was clearly *built* to receive vendor updates and then didn't register one.

= Does it need my licence keys? =

No. It never asks for, stores, or transmits a licence key. It reads whatever licence status your plugins already saved locally, and — more importantly — it detects silent updaters even when no licence information exists at all.

= Can it break my site? =

It is strictly read-only: it never changes, updates, deactivates, or rolls back any plugin, theme, or setting, so there is nothing it can change that could break. A scan only triggers the same update check WordPress itself runs on a schedule.

= Does it send my data anywhere? =

No. The audit runs on your own server, and licence information never leaves your site. The only outbound requests are listed under "External services": WordPress.org's public plugin API (plugin slugs only), and — only when you click them — the Freemius screens that power the optional paid upgrade.

= How does it decide a plugin has a "silent updater"? =

It reads WordPress's own update transient (the same list your dashboard uses) to see which components a source is actually updating, and — for components that are updating nothing — it does a read-only scan of their code for the fingerprints of the common updater/licensing libraries (EDD Software Licensing, Plugin Update Checker, Freemius, WooCommerce API Manager, and hand-rolled update-transient hooks). A component that ships one of those libraries was built to receive vendor updates, so if nothing is updating it, its licence has almost certainly lapsed. Those fingerprints are stored as plain reference data in `data/updater-signatures.json`. Patch Gap ships no updater of its own and never serves or installs updates — it only reads and reports.

= Does it replace Wordfence / MainWP / ManageWP? =

No — it fills a hole they leave. Wordfence's own documentation states it does not version-check plugins outside the WordPress.org repository. Fleet tools show updates that vendors *offer*; if a vendor's updater is silent, they show nothing. Patch Gap is the piece that notices the silence.

= What does uninstalling remove? =

Everything the plugin created: its findings table, settings, cached lookups, and scheduled task. Deactivating (without uninstalling) keeps your findings and dismissal notes.

== Screenshots ==

1. The coverage report: which components nobody is patching, and why
2. Licence roster with expiry dates
3. Site Health integration
4. Settings

== Changelog ==

= 1.0.2 =
* Maintenance release: the licensing and update service behind the Pro version moved to a new account. No functional changes.

= 1.0.1 =
* Fixed: the licence roster could not read Freemius-issued licences. The reader looked for a flat "licenses" key in the SDK's fs_accounts option; real Freemius SDKs store licences under "all_licenses", keyed by module id (and map modules in "id_slug_type_path_map"). Verified against the Freemius SDK source. Coverage classification was unaffected — this only restores the licence status/expiry column for Freemius-licensed plugins.


= 1.0.0 =
* Initial release: update-coverage classification (WordPress.org / vendor-updater / silent-updater / declared-but-unanswered / abandoned / bespoke), licence-state introspection, weekly re-check with new-gap alerts, dismiss-with-note workflow, Site Health test, dashboard widget, WP-CLI with JSON output.

== Upgrade Notice ==

= 1.0.0 =
First release.
