Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions src/LicenseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* - Uses the Hub_Manager cache for zero-extra-API-call license checks.
* - Auto-migrates legacy per-plugin keys from addon settings.
*
* @version 2.1.2
* @version 2.1.3
*/
class LicenseHandler {

Expand Down Expand Up @@ -59,7 +59,7 @@ class LicenseHandler {
*
* @var string
*/
private $version = '2.1.2';
private $version = '2.1.3';

/**
* WP Override flag
Expand Down Expand Up @@ -299,6 +299,7 @@ public function initialize_paddlepress_client( $field_setting = null ) {

if ( $has_access ) {
remove_action( 'admin_notices', array( $this, 'action_admin_notices' ) );
$this->remove_license_notice();
} else {
add_action( 'admin_notices', array( $this, 'action_admin_notices' ) );
}
Expand Down Expand Up @@ -439,6 +440,14 @@ private function auto_migrate_discovered_key( $key ) {
* @return void
*/
public function action_admin_notices() {
// Defensive: sticky GF notices can outlive an old handler version. If
// the Hub now grants this addon access, consume any stale notice instead
// of re-adding it.
if ( class_exists( '\GravityWP\Shared\Hub_Manager' ) && \GravityWP\Shared\Hub_Manager::has_access( $this->_addon_slug ) ) {
$this->remove_license_notice();
return;
}

$global_settings_url = admin_url( 'admin.php?page=gravitywp' );
$hub_url = $global_settings_url; // Single page now.
$site_slug = $this->_addon_class::get_instance()->gwp_site_slug;
Expand Down Expand Up @@ -466,6 +475,37 @@ public function action_admin_notices() {
GFCommon::add_dismissible_message( $message, $key, 'warning', false, true );
}

/**
* Remove stale Gravity Forms sticky license notices for this addon.
*
* Older bundled handlers stored notices as sticky GF dismissible messages.
* Those messages persist until explicitly dismissed/removed, so a site can
* keep seeing "license has not been activated" after a Global License Key
* starts covering the addon. Call this whenever access is confirmed.
*
* @since 2.1.3
* @return void
*/
public function remove_license_notice() {
if ( ! class_exists( 'GFCommon' ) || empty( $this->_addon_slug ) ) {
return;
}

GFCommon::remove_dismissible_message( $this->_addon_slug . '_license_message_notice' );

// Also remove canonical/alternate keys when the current GF addon slug is
// a legacy PaddlePress download_tag (e.g. gravitywpapiconnector) but a
// newer UI uses github_name/canonical slug (e.g. gravitywp-api-connector).
if ( class_exists( '\GravityWP\Shared\Hub_Manager' ) ) {
$plugin = \GravityWP\Shared\Hub_Manager::get_plugin_data( $this->_addon_slug );
foreach ( array( 'slug', 'download_tag', 'github_name' ) as $field ) {
if ( ! empty( $plugin[ $field ] ) && is_string( $plugin[ $field ] ) ) {
GFCommon::remove_dismissible_message( $plugin[ $field ] . '_license_message_notice' );
}
}
}
}

/**
* Define plugin settings fields.
*
Expand Down Expand Up @@ -617,7 +657,7 @@ public function license_feedback( $value, $field ) {
// Prefer Hub_Manager check (supports both key types).
if ( class_exists( '\GravityWP\Shared\Hub_Manager' ) ) {
if ( \GravityWP\Shared\Hub_Manager::has_access( $this->_addon_slug ) ) {
GFCommon::remove_dismissible_message( $this->_addon_slug . '_license_message_notice' );
$this->remove_license_notice();
return true;
}
// If no key at all, return null (neutral state), else false (red).
Expand All @@ -637,7 +677,7 @@ public function license_feedback( $value, $field ) {
}

if ( $this->_license_handler && $this->_license_handler->gwp_is_valid( true, $key ) ) {
GFCommon::remove_dismissible_message( $this->_addon_slug . '_license_message_notice' );
$this->remove_license_notice();
return true;
}

Expand Down
4 changes: 4 additions & 0 deletions src/changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
= 2.1.3 =
- FIX: Stale "Your … license has not been activated" admin notices now disappear automatically once a valid license key covers the plugin. They were stored as sticky Gravity Forms messages (gform_sticky_admin_messages) by older bundled handler versions and nothing ever removed them. Covered notices are now suppressed at display time and purged from the database.
- FIX: License lookups by legacy addon slug (e.g. 'gravitywpapiconnector') failed because the hub matcher only compared the canonical slug. Hub_Manager::get_plugin_data() now also matches download_tag and github_name, so has_access()/notice removal work for legacy-slug add-ons.

= 2.1.2 =
- only version bump.

Expand Down
6 changes: 6 additions & 0 deletions src/pluginUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,9 @@ public function check_update_license( $_transient_data ) {

if ( $has_access ) {
remove_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
if ( is_object( $this->handler_class ) && method_exists( $this->handler_class, 'remove_license_notice' ) ) {
$this->handler_class->remove_license_notice();
}
} else {
add_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
}
Expand All @@ -404,6 +407,9 @@ public function check_update_license( $_transient_data ) {

if ( $this->gwp_is_valid( false, $license_key ) ) {
remove_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
if ( is_object( $this->handler_class ) && method_exists( $this->handler_class, 'remove_license_notice' ) ) {
$this->handler_class->remove_license_notice();
}
} else {
add_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
}
Expand Down
169 changes: 167 additions & 2 deletions src/shared/class-global-license-key-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ public static function init( $version, $base_dir = '' ) {
add_action( 'admin_menu', array( self::class, 'add_admin_menu' ), 98 );
add_action( 'admin_init', array( self::class, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_assets' ) );

// Hide + purge stale "license has not been activated" sticky GF
// notices for plugins the current license covers. The filter
// suppresses them at read time (old bundled handlers ≤2.0.x
// re-add them on every admin load); the admin_init sweep cleans
// the stored option itself.
add_filter( 'option_gform_sticky_admin_messages', array( self::class, 'filter_covered_license_notices' ) );
add_action( 'admin_init', array( self::class, 'cleanup_stale_license_notices' ), 20 );
}

/**
Expand All @@ -75,14 +83,30 @@ public static function init( $version, $base_dir = '' ) {
public static function add_admin_menu() {
$page_title = __( 'GravityWP', 'gravitywp-license-handler' );
$menu_title = 'GravityWP';
$capability = 'gform_full_access';
$callback = array( self::class, 'render_page' );

// Detect if Gravity Forms is loaded and accessible.
global $admin_page_hooks;
$gf_active = ( isset( $admin_page_hooks['gf_edit_forms'] ) || class_exists( '\GFForms' ) );

if ( $gf_active && current_user_can( 'gform_full_access' ) ) {
// With the Members plugin active, GF only auto-grants gform_full_access
// to administrators whose role has NO granular GF caps, so a hardcoded
// gform_full_access check hides the menu for admins with granular caps.
// Resolve a capability the current user actually holds instead.
$caps = array( 'gform_full_access','gravityforms_view_addons' );

if ( $gf_active && class_exists( '\GFCommon' ) ) {
$user_can = \GFCommon::current_user_can_any( $caps );
$capability = \GFCommon::current_user_can_which( $caps );
if ( '' === $capability ) {
$capability = 'gform_full_access';
}
} else {
$user_can = current_user_can( 'manage_options' );
$capability = 'manage_options';
}

if ( $gf_active && $user_can ) {
// Preferred: nest under Gravity Forms menu.
add_submenu_page(
'gf_edit_forms',
Expand Down Expand Up @@ -313,6 +337,147 @@ public static function sanitize_plugin_keys( $raw ) {
return $clean;
}

/**
* Suffix used by every license-handler version for its sticky GF
* "license has not been activated" admin notice key.
*
* @var string
*/
const LICENSE_NOTICE_SUFFIX = '_license_message_notice';

/**
* Filter callback for option_gform_sticky_admin_messages.
*
* Strips `{slug}_license_message_notice` entries for plugins the hub
* reports as covered (has_access). Suppression happens at read time,
* so stale notices vanish no matter which (possibly old, bundled)
* handler copy wrote them or keeps re-adding them. Because Gravity
* Forms' Dismissable_Messages::add() does a read-modify-write on the
* same option, subsequent writes also persist the cleaned array.
*
* Coverage comes from the CACHED hub data only — never triggers an
* HTTP request inside an option filter.
*
* @since 2.1.3
* @param mixed $value The option value.
* @return mixed Filtered value.
*/
public static function filter_covered_license_notices( $value ) {
if ( empty( $value ) || ! is_array( $value ) ) {
return $value;
}

$covered = self::get_covered_notice_slugs();
if ( empty( $covered ) ) {
return $value;
}

return self::strip_covered_license_entries( $value, $covered );
}

/**
* Remove covered license notices from the stored option itself.
*
* The display filter above keeps them invisible; this sweep keeps the
* gform_sticky_admin_messages option clean so stale entries don't pile
* up in the database. Runs on admin_init and after every fresh hub
* fetch (see Hub_Manager::fetch_and_cache()).
*
* Only entries whose plugin the hub reports with has_access are
* removed — uncovered or unknown plugins keep their notice.
*
* @since 2.1.3
* @return void
*/
public static function cleanup_stale_license_notices() {
$covered = self::get_covered_notice_slugs();
if ( empty( $covered ) ) {
return;
}

// Suspend our own display filter for the whole read-and-write:
// reading through it would make the sweep a no-op (entries already
// stripped), and update_option() compares against a get_option()
// read — filtered, the old value would equal the cleaned one and
// the write would be skipped, leaving the stored option dirty.
$filter_cb = array( self::class, 'filter_covered_license_notices' );
$had_filter = remove_filter( 'option_gform_sticky_admin_messages', $filter_cb );

$sticky = get_option( 'gform_sticky_admin_messages', array() );

if ( is_array( $sticky ) && ! empty( $sticky ) ) {
$cleaned = self::strip_covered_license_entries( $sticky, $covered );
if ( count( $cleaned ) !== count( $sticky ) ) {
update_option( 'gform_sticky_admin_messages', $cleaned );
}
}

if ( $had_filter ) {
add_filter( 'option_gform_sticky_admin_messages', $filter_cb );
}
}

/**
* Build the set of slugs whose plugins the license currently covers.
*
* Reads the hub cache option directly (no Hub_Manager method calls,
* no HTTP) so it is safe inside option filters and immune to
* mixed-version class loading. Includes every identifier variant a
* notice key may have been created under: canonical slug, legacy
* download_tag and github_name.
*
* @since 2.1.3
* @return array<string,bool> Lowercased slug => true.
*/
private static function get_covered_notice_slugs() {
$cache = get_option( 'gravitywp_hub_cache', false );
$plugins = ( ! empty( $cache['data']['plugins'] ) && is_array( $cache['data']['plugins'] ) )
? $cache['data']['plugins']
: array();

$covered = array();
foreach ( $plugins as $plugin ) {
if ( empty( $plugin['has_access'] ) ) {
continue;
}
foreach ( array( 'slug', 'download_tag', 'github_name' ) as $field ) {
if ( ! empty( $plugin[ $field ] ) && is_string( $plugin[ $field ] ) ) {
$covered[ strtolower( $plugin[ $field ] ) ] = true;
}
}
}

return $covered;
}

/**
* Strip covered `{slug}_license_message_notice` entries from a sticky
* messages array.
*
* @since 2.1.3
* @param array $messages Sticky messages (key => message).
* @param array<string,bool> $covered Lowercased covered slugs.
* @return array Filtered messages.
*/
private static function strip_covered_license_entries( $messages, $covered ) {
$suffix_len = strlen( self::LICENSE_NOTICE_SUFFIX );

foreach ( array_keys( $messages ) as $key ) {
if ( ! is_string( $key ) || strlen( $key ) <= $suffix_len ) {
continue;
}
if ( substr( $key, -$suffix_len ) !== self::LICENSE_NOTICE_SUFFIX ) {
continue;
}
$slug = strtolower( substr( $key, 0, -$suffix_len ) );
if ( isset( $covered[ $slug ] ) ) {
unset( $messages[ $key ] );
}
}

return $messages;
}

/**
* Enqueue CSS and JS assets.
*
Expand Down
36 changes: 34 additions & 2 deletions src/shared/class-hub-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,17 @@ private static function fetch_and_cache() {
// download_tag → canonical slug mapping and can move the keys.
self::normalize_plugin_keys_to_canonical( $data );

// Purge stale "license has not been activated" sticky GF notices
// for plugins this license now covers. Old bundled handlers left
// them behind in gform_sticky_admin_messages and nothing else
// removes them. Guarded: an older Registry class (without the
// method) may have won the class_exists race in mixed-version
// installs.
if ( class_exists( '\GravityWP\Shared\Global_License_Key_Registry' )
&& method_exists( '\GravityWP\Shared\Global_License_Key_Registry', 'cleanup_stale_license_notices' ) ) {
Global_License_Key_Registry::cleanup_stale_license_notices();
}

return $data;
}

Expand Down Expand Up @@ -805,17 +816,38 @@ public static function get_all_plugins() {
/**
* Get data for a specific plugin by slug.
*
* @param string $slug Plugin slug (download_tag).
* Hub entries carry three identifiers: the canonical `slug`
* (= github_name), the legacy PaddlePress `download_tag`, and
* `github_name` itself. Several GF addon classes still report the
* legacy download_tag as their slug (e.g. 'gravitywpapiconnector'
* vs canonical 'gravitywp-api-connector'), so a canonical-only match
* would miss them. Canonical `slug` matches take priority.
*
* @param string $slug Plugin slug (canonical, download_tag or github_name).
* @return array|false Plugin data or false if not found.
*/
public static function get_plugin_data( $slug ) {
if ( ! is_string( $slug ) || '' === $slug ) {
return false;
}

$plugins = self::get_all_plugins();
$needle = strtolower( $slug );

foreach ( $plugins as $plugin ) {
if ( isset( $plugin['slug'] ) && $plugin['slug'] === $slug ) {
if ( isset( $plugin['slug'] ) && strtolower( (string) $plugin['slug'] ) === $needle ) {
return $plugin;
}
}

foreach ( $plugins as $plugin ) {
foreach ( array( 'download_tag', 'github_name' ) as $field ) {
if ( ! empty( $plugin[ $field ] ) && is_string( $plugin[ $field ] ) && strtolower( $plugin[ $field ] ) === $needle ) {
return $plugin;
}
}
}

return false;
}

Expand Down