Skip to content

fix: licensing environment type, inactive license status, and dead update packages (6.17.1) - #1734

Merged
jakejackson1 merged 1 commit into
hot-patch-6.17.1from
fix/licensing-environment-inactive-package-6.17
Sep 21, 2026
Merged

jakejackson1 merged 1 commit into
hot-patch-6.17.1from
fix/licensing-environment-inactive-package-6.17

Conversation

@jakejackson1

@jakejackson1 jakejackson1 commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Resolves #1726, resolves #1728, resolves #1730.

License and update requests now send the site's environment type from wp_get_environment_type(). The store can use it to leave staging, development and local sites out of activation counts.

An inactive license status now shows its own message ("This license key is not active. Please check your account or contact support.") instead of the generic unknown error. It also counts as unentitled, so a site that goes inactive withdraws any update package it shared across a Multisite network.

When a license key is valid but not activated for the current domain, the store still returns a download URL, but that URL fails with a 401. The URL is now dropped before it is cached. Such a site is no longer offered an update that fails halfway through installing, On a Multisite it borrows the working package another licensed site shared, in both the update row and the plugin details popup.

Try it

wp eval '
$updater = new GFPDF\Helper\Licensing\EDD_SL_Plugin_Updater( "https://example.com", "example/example.php", [] );
var_dump( $updater->get_version_api_params()["environment"] );

$updater->set_license_status( "site_inactive" );
$updater->set_version_info_cache( (object) [ "new_version" => "9.9", "package" => "https://example.com/dead" ] );
var_dump( $updater->get_cached_version_info()->package );
$updater->delete_version_info_cache();
'

The first dump prints your environment type (e.g. "local"), and the second prints an empty string. Without this change the key is missing and the dead URL is kept.

Test plan

  • Activating, checking and deactivating an add-on license sends environment in the request body (visible in the debug log)
  • A license API response of {"license":"inactive"} shows the new message on the License tab
  • An add-on license that is site_inactive shows "Automatic update is unavailable" on a single site instead of an update that fails
  • On a per-site-activated Multisite, a site_inactive site installs an update another licensed site shared
  • On that same site, "View version details" shows the Update Now button
  • The core Gravity PDF update still offers its package
  • Licensing tests pass on single-site and multisite
More info

Environment (#1726). environment is added to both Helper_Abstract_Addon::get_default_api_params() (activate, check, deactivate) and EDD_SL_Plugin_Updater::get_version_api_params() (get_version, plus the bulk check_license in Model_Settings). It falls back to production when wp_get_environment_type() is unavailable (WP < 5.5), matching EDD's stock updater. api.gravitypdf.com must forward the parameter to EDD for it to take effect.

Inactive (#1728). Added to Helper_Data::addon_license_responses() and EDD_SL_Plugin_Updater::UNENTITLED_LICENSE_STATUSES. Per the issue comment, the store may auto-activate and never return inactive. If it doesn't, these entries are harmless.

Dead package (#1730). This deliberately departs from the fix proposed in the issue comment (! is_license_active() in get_version_from_remote()), for three reasons:

  • The core plugin updater (gravity-pdf-updater.php) never sets a license status, so ! is_license_active() would strip every core update package.
  • error and rate_limit are not verdicts on the license, and must not throw away a working URL. Keying on UNENTITLED_LICENSE_STATUSES excludes them, the same reasoning that constant already documents.
  • Model_Settings::licensing_bulk_get_version_api_response() writes the other bundled add-ons' caches straight through set_version_info_cache(), bypassing get_version_from_remote(). The strip lives in the cache setter so both writers are covered. It edits the response object in place, so the value returned by get_version_from_remote() is cleaned too.

maybe_apply_network_package() is unchanged: its emptiness test is now correct. A cache written before a status change is already cleared by flush_update_cache().

Details modal. plugins_api_filter() used to read the per-site cache directly, skipping maybe_apply_network_package(). A site with no usable package of its own got no install/update button in the modal (WordPress only shows it when download_link is set), even though its update row borrowed the network package. This affected unlicensed sites before this PR too. The filter now calls get_repo_api_data(), removing its own copy of the cache-else-fetch logic, and a borrowed package also sets download_link. show_changelog() goes through the same filter, so it is covered as well.

Tests. New or extended: providerUnentitledLicenseStatus gains inactive, test_set_version_info_cache_drops_unentitled_package and test_set_version_info_cache_keeps_package_without_a_license_verdict (data providers), the network borrow test is parameterized with a site_inactive dead-URL case and asserts the modal's download_link, test_get_version_api_params_sends_environment_type, test_license_api_requests_send_environment_type, and an inactive block in test_schedule_license_check.

Full suite: single-site 1218 tests OK. Multisite has 1 error in test_is_non_active_multisite, which also fails on the untouched base branch in a full run and passes in isolation (order-dependent, pre-existing). PHPCS is clean.

@jakejackson1
jakejackson1 force-pushed the fix/licensing-environment-inactive-package-6.17 branch 2 times, most recently from 178c5a0 to 7e754cd Compare September 17, 2026 03:18
…date packages

License and update requests now declare wp_get_environment_type() and an explicit `url`, so the store can
exclude non-production sites from activation counts (#1726) and the URL recorded here is the URL the store
activated, rather than one inferred from the request's user agent.

`inactive` from the store means the key is valid, unexpired and not disabled, but EDD holds zero activated
sites for it: every site was removed from the customer's account, or the key was never activated anywhere.
Only `check_license` can return it — `activate_license` has its own error set — so it is always a verdict on
a key that was already saved. The old message ("not active, check your account or contact support")
described none of that and sent people to support; it now mirrors `site_inactive` and points at the fix. The
status also counts as unentitled when withdrawing a network-shared package (#1728).

The store hands a site_inactive license a package URL that 401s. set_version_info_cache() now blanks
`package` and `download_link` for any unentitled status, so the dead URL is never offered and a Multisite
site borrows the network package instead (#1730). It keys on UNENTITLED_LICENSE_STATUSES rather than
`! is_license_active()`: the core plugin updater never sets a status, and error/rate_limit are not verdicts
on the license. It lives in the cache setter because the bulk get_version response writes the other add-ons'
caches without going through get_version_from_remote().

plugins_api_filter() read the per-site cache directly, skipping maybe_apply_network_package(). On a per-site
Multisite, a site whose own package was blanked had no install/update button in the details modal while its
update row borrowed the network package. The filter now goes through get_repo_api_data(), and a borrowed
package also sets download_link, which the modal needs to show the button.

The scheduled and bulk checks now reconcile the URL the key is activated for. An active license proves the
key is activated for home_url(), so that URL is recorded; `inactive` / `site_inactive` against a *different*
recorded URL means the site was cloned or moved — production copied to staging — and the key is still
activated for the old URL, so it is activated for this one. The same URL on record is left alone: that is a
deliberate deactivation from the customer's account, and the message above asks them to resave if it wasn't.
A rejected attempt backs off for a week, matching the check cadence.

update_license_info() records the URL whenever it writes to the database and the store has confirmed the
key, covering the first save, the GPDF_LICENSE_KEY constant path and Access Pass propagation. Without that,
a site licensed today had nothing on record for up to a week, and sync_license_activation_url() reads an
empty record as "not evidence the URL changed" — so a clone during that window went undetected, the very
case the record exists to catch.

The settings save cannot use that funnel: it activates with $use_database = false because
Helper_Abstract_Options::settings_sanitize() ends with an array_merge() over a snapshot taken before the
filter runs, so a direct write during the save is silently reverted. The value is threaded through $input
instead, with license_{slug}_url registered as a hidden field so it survives the sanitize whitelist, exactly
as _status and _message already are. Whatever is posted for that field is discarded in favour of what is on
record: only an activation during this request proves the key is activated here, and a stale valid status on
a clone would otherwise overwrite the production URL and hide the move from the check that acts on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jakejackson1
jakejackson1 force-pushed the fix/licensing-environment-inactive-package-6.17 branch from 2eacaea to 8edfe6d Compare September 21, 2026 01:49
@jakejackson1
jakejackson1 merged commit 0d36ecd into hot-patch-6.17.1 Sep 21, 2026
13 checks passed
@jakejackson1
jakejackson1 deleted the fix/licensing-environment-inactive-package-6.17 branch September 21, 2026 04:42
jakejackson1 added a commit that referenced this pull request Sep 21, 2026
Ports the 6.17.1 release (6.17..6.17.1) to development:

- keep mPDF's cache folders through tmp cleanup, and keep font metrics for a week (#1731)
- stop concurrent PDFs failing with "Temporary files directory is not writable" (#1733)
- note a PDF left off a notification on the entry, linked to its settings and authored as the
  notification, with the Gravity PDF logo as its avatar (#1732, #1736)
- log generation errors with form/entry/PDF IDs instead of the whole object (#1732)
- licensing environment type, inactive license status and dead update packages (#1734)
- tag mPDF's log records with the form, entry and PDF they belong to (#1738)
- keep PDF URL paths and safe query args when redacting logs (#1737)
- retry license reactivation soon when the store gives no verdict (#1739)

Adapted to development: tests moved to tests/phpunit/integration on the shared TestCase,
Context_Logger uses the scoped GFPDF_Vendor\Psr\Log, the generation-error log context lives in
development's refactored Model_PDF::process_and_save_pdf(), and pdf_id defaults to '' for a
Helper_PDF built without a PDF ID. Adds the 6.17.1 changelog section. Left out: the version bump and the wp-env 11
CI change (development fixed the Debian 11 build its own way in #1724).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jakejackson1 added a commit that referenced this pull request Sep 21, 2026
Ports the 6.17.1 release (6.17..6.17.1) to development:

- keep mPDF's cache folders through tmp cleanup, and keep font metrics for a week (#1731)
- stop concurrent PDFs failing with "Temporary files directory is not writable" (#1733)
- note a PDF left off a notification on the entry, linked to its settings and authored as the
  notification, with the Gravity PDF logo as its avatar (#1732, #1736)
- log generation errors with form/entry/PDF IDs instead of the whole object (#1732)
- licensing environment type, inactive license status and dead update packages (#1734)
- tag mPDF's log records with the form, entry and PDF they belong to (#1738)
- keep PDF URL paths and safe query args when redacting logs (#1737)
- retry license reactivation soon when the store gives no verdict (#1739)

Adapted to development: tests moved to tests/phpunit/integration on the shared TestCase,
Context_Logger uses the scoped GFPDF_Vendor\Psr\Log, the generation-error log context lives in
development's refactored Model_PDF::process_and_save_pdf(), and pdf_id defaults to '' for a
Helper_PDF built without a PDF ID. Adds the 6.17.1 changelog section. Left out: the version bump.

Also takes 6.17.1's wp-env upgrade (^11.15.0, which repoints Debian 11 sources at archive.debian.org
itself) in place of #1724's tools/wp-env/patch-bullseye-apt.mjs, which the yarn wp-env scripts no longer run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant