diff --git a/src/LicenseHandler.php b/src/LicenseHandler.php index d8ed7d8..8bba7a4 100644 --- a/src/LicenseHandler.php +++ b/src/LicenseHandler.php @@ -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 { @@ -59,7 +59,7 @@ class LicenseHandler { * * @var string */ - private $version = '2.1.2'; + private $version = '2.1.3'; /** * WP Override flag @@ -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' ) ); } @@ -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; @@ -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. * @@ -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). @@ -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; } diff --git a/src/changelog.txt b/src/changelog.txt index cb920e0..2075cac 100644 --- a/src/changelog.txt +++ b/src/changelog.txt @@ -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. diff --git a/src/pluginUpdater.php b/src/pluginUpdater.php index d45af77..6b846e2 100644 --- a/src/pluginUpdater.php +++ b/src/pluginUpdater.php @@ -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' ) ); } @@ -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' ) ); } diff --git a/src/shared/class-global-license-key-registry.php b/src/shared/class-global-license-key-registry.php index 5ef7165..32b1f06 100644 --- a/src/shared/class-global-license-key-registry.php +++ b/src/shared/class-global-license-key-registry.php @@ -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 ); } /** @@ -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', @@ -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 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 $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. * diff --git a/src/shared/class-hub-manager.php b/src/shared/class-hub-manager.php index 96ad689..d6f7840 100644 --- a/src/shared/class-hub-manager.php +++ b/src/shared/class-hub-manager.php @@ -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; } @@ -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; }