unix time when first shown (drives 'expires_after'). */ const STARTED_OPTION = 'flrt_notice_started'; /** Shared AJAX action / nonce for dismissing any notice. */ const DISMISS_ACTION = 'flrt_dismiss_notice'; /** * Preview mode — for fine-tuning notice text and appearance during development. * * When true: every notice is shown on every admin page (trigger, expiry and * dismissal state are ignored) and the "X" only hides it client-side, so it * reappears on reload. Keep false in production. */ const PREVIEW_MODE = false; public function __construct() { add_action( 'admin_init', [ $this, 'detectUpdate' ] ); add_action( 'admin_init', [ $this, 'maybeAutoDismiss' ] ); add_action( 'admin_notices', [ $this, 'renderAll' ] ); add_action( 'wp_ajax_' . self::DISMISS_ACTION, [ $this, 'ajaxDismiss' ] ); } /** * Single source of truth for admin notices. Add one array entry per notice. * * @return array[] */ protected function notices() { return [ [ 'id' => 'security-1922', 'type' => 'warning', 'free_only' => false, // true = Free only; false = show in both Free and PRO 'trigger' => 'update', 'expires_after' => DAY_IN_SECONDS, 'auto_dismiss' => function () { // Auto-hide once the user opens the Color Swatches (Experimental) settings tab. return isset( $_GET['page'], $_GET['tab'] ) && sanitize_key( wp_unslash( $_GET['page'] ) ) === 'filters-settings' && sanitize_key( wp_unslash( $_GET['tab'] ) ) === 'experimental'; }, 'message' => function () { $settings_url = admin_url( 'edit.php?post_type=' . FLRT_FILTERS_SET_POST_TYPE . '&page=filters-settings&tab=experimental' ); return sprintf( /* translators: 1: opening tag to the plugin settings page, 2: closing tag. */ wp_kses( __( 'Thank you for updating Filter Everything! This release includes a security update related to how Color swatches are rendered. Everything should work fine, but just in case, please check how your Color swatches look in the filters on your site\'s pages. You can see the list of Color swatches you use on %1$sthis settings page%2$s.', 'filter-everything' ), [ 'strong' => [], 'a' => [ 'href' => [] ] ] ), '', '' ); }, ], ]; } /** * Records that an existing install was updated, so 'update' notices fire. * Fresh installs are pre-stamped in Plugin::activate() and match here. */ public function detectUpdate() { if ( self::PREVIEW_MODE ) { return; } $stored = get_option( self::VERSION_OPTION, false ); if ( $stored === FLRT_PLUGIN_VER ) { return; } update_option( self::UPDATED_OPTION, FLRT_PLUGIN_VER ); update_option( self::VERSION_OPTION, FLRT_PLUGIN_VER ); } /** * Runs each notice's optional 'auto_dismiss' condition (e.g. "the user opened * the relevant settings page") and dismisses it permanently when it matches. */ public function maybeAutoDismiss() { if ( self::PREVIEW_MODE ) { return; } foreach ( $this->notices() as $notice ) { if ( empty( $notice['id'] ) || empty( $notice['auto_dismiss'] ) || ! is_callable( $notice['auto_dismiss'] ) ) { continue; } if ( $this->isDismissed( $notice['id'] ) ) { continue; } if ( call_user_func( $notice['auto_dismiss'] ) ) { $this->markDismissed( $notice['id'] ); } } } public function renderAll() { foreach ( $this->notices() as $notice ) { $this->maybeRender( $notice ); } } protected function maybeRender( array $notice ) { $notice = array_merge( [ 'id' => '', 'type' => 'info', 'message' => '', 'trigger' => 'always', 'free_only' => false, 'capability' => flrt_plugin_user_caps(), 'dismissible' => true, ], $notice ); if ( $notice['id'] === '' ) { return; } if ( $notice['free_only'] && defined( 'FLRT_FILTERS_PRO' ) && FLRT_FILTERS_PRO ) { return; } if ( $notice['capability'] && ! current_user_can( $notice['capability'] ) ) { return; } if ( ! self::PREVIEW_MODE ) { if ( $this->isDismissed( $notice['id'] ) ) { return; } if ( ! $this->triggerPasses( $notice['trigger'] ) ) { return; } if ( $this->hasExpired( $notice ) ) { return; } } $message = is_callable( $notice['message'] ) ? call_user_func( $notice['message'] ) : $notice['message']; if ( $message === '' ) { return; } $notice_class = 'flrt-notice-' . sanitize_html_class( $notice['id'] ); if ( function_exists( 'wp_admin_notice' ) ) { // Modern WordPress notice API (WP 6.4+). wp_admin_notice( $message, [ 'type' => $notice['type'], 'dismissible' => (bool) $notice['dismissible'], 'additional_classes' => [ 'flrt-admin-notice', $notice_class ], ] ); } else { // Fallback for WordPress < 6.4 ($message is already escaped above). printf( '
%4$s