From 20144f461b34f7445f717184b4741cc2864a7794 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 26 Sep 2026 13:25:41 +0200 Subject: [PATCH 1/6] Turn Cookie Notice tab into a FrontConsent promo panel Cookie consent configuration moved to the new standalone FrontConsent plugin. The Cookie Notice settings tab no longer renders any config fields here; it explains the move and offers a one-click install of FrontConsent (or a "Manage in FrontConsent" link once it's active). The frontend banner keeps working during the transition and disables itself automatically once FrontConsent is detected (FRCN_VERSION). Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 6 +- .../Admin/CookieNoticeDeprecationNotice.php | 151 +++ includes/Admin/Settings.php | 960 +----------------- includes/Frontend/CookieNotice.php | 12 + includes/Plugin_Main.php | 5 + readme.txt | 5 +- .../CookieNoticeCacheInvalidationTest.php | 80 ++ tests/Unit/CookieNoticeIntegrationsTest.php | 227 ----- .../CookieNoticeSettingsSanitizationTest.php | 260 ----- tests/Unit/SettingsTest.php | 17 - 10 files changed, 300 insertions(+), 1423 deletions(-) create mode 100644 includes/Admin/CookieNoticeDeprecationNotice.php create mode 100644 tests/Unit/CookieNoticeCacheInvalidationTest.php delete mode 100644 tests/Unit/CookieNoticeIntegrationsTest.php delete mode 100644 tests/Unit/CookieNoticeSettingsSanitizationTest.php diff --git a/AGENTS.md b/AGENTS.md index 0aab82d..5b00eb9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,9 @@ Guidelines for AI coding agents working on this WordPress plugin. ## Project Overview -**FrontBlocks Site Tools** is a free, all-in-one WordPress plugin covering the everyday needs of a site — not just a block-extension add-on. It bundles Gutenberg/GeneratePress block enhancements alongside general-purpose site utilities: cookie consent, maintenance mode, image management (size control + WebP/AVIF delivery), social login, and more. New features do not need to be block-related to belong here — evaluate additions against "does a typical WordPress site commonly need this," not "is this a block." The PRO companion lives at `wp-content/plugins/frontblocks-pro/`. +**FrontBlocks Site Tools** is a free, all-in-one WordPress plugin covering the everyday needs of a site — not just a block-extension add-on. It bundles Gutenberg/GeneratePress block enhancements alongside general-purpose site utilities: maintenance mode, image management (size control + WebP/AVIF delivery), social login, and more. New features do not need to be block-related to belong here — evaluate additions against "does a typical WordPress site commonly need this," not "is this a block." The PRO companion lives at `wp-content/plugins/frontblocks-pro/`. + +**Cookie Notice is deprecated here and moving to FrontConsent** (`wp-content/plugins/frontconsent/`), a dedicated standalone plugin. It still ships and works in FrontBlocks during the transition (see `includes/Frontend/CookieNotice.php` and `includes/Admin/CookieNoticeDeprecationNotice.php`), but do not extend it further — new cookie-consent work belongs in FrontConsent's own `AGENTS.md`. FrontBlocks' module disables itself automatically once FrontConsent is active (`FRCN_VERSION` defined), and FrontConsent migrates the site's settings/stats on activation. - **PHP minimum:** 7.0 - **WordPress minimum:** 5.8 (required for the image-format-output filters used by Image Management) @@ -148,7 +150,7 @@ Every feature is a self-contained class following the same shape: Actions for making a release: - Update the Stable Tag in `readme.txt` and the plugin version in its header and `CCRMRE_VERSION` constant. -- Finalize the `= Unreleased =` changelog entries under the released version and create a new empty `= Unreleased =` section. +- Finalize the `= Unreleased =` changelog entries under the released version. - Create the matching GitHub release and tag. ## Test Enforcement diff --git a/includes/Admin/CookieNoticeDeprecationNotice.php b/includes/Admin/CookieNoticeDeprecationNotice.php new file mode 100644 index 0000000..a1afb3c --- /dev/null +++ b/includes/Admin/CookieNoticeDeprecationNotice.php @@ -0,0 +1,151 @@ +is_frontconsent_active() ) { + return; + } + + $options = get_option( 'frontblocks_settings', array() ); + if ( empty( $options['enable_cookie_notice'] ) ) { + return; + } + + $screen = get_current_screen(); + if ( ! $screen ) { + return; + } + + $allowed_screens = array( 'appearance_page_frontblocks-settings', 'dashboard', 'plugins' ); + if ( ! in_array( $screen->id, $allowed_screens, true ) ) { + return; + } + + if ( get_user_meta( get_current_user_id(), self::DISMISSED_META_KEY, true ) ) { + return; + } + + $install_url = wp_nonce_url( + self_admin_url( 'update.php?action=install-plugin&plugin=front-consent' ), + 'install-plugin_front-consent' + ); + ?> + + + 403 ) ); + } + + if ( ! current_user_can( 'install_plugins' ) ) { + wp_die( esc_html__( 'You do not have permission to do this.', 'frontblocks' ), '', array( 'response' => 403 ) ); + } + + update_user_meta( get_current_user_id(), self::DISMISSED_META_KEY, true ); + wp_die(); + } +} diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index a3d72dd..fabfc49 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -96,110 +96,6 @@ class Settings { */ private $option_maintenance_image = 'maintenance_image'; - /** - * Option key for the cookie notice feature. - * - * @var string - */ - private $option_enable_cookie_notice = 'enable_cookie_notice'; - - /** - * Option key for the cookie notice message. - * - * @var string - */ - private $option_cookie_notice_message = 'cookie_notice_message'; - - /** - * Option key for the cookie notice accept button label. - * - * @var string - */ - private $option_cookie_notice_accept_label = 'cookie_notice_accept_label'; - - /** - * Option key for the cookie notice reject button label. - * - * @var string - */ - private $option_cookie_notice_reject_label = 'cookie_notice_reject_label'; - - /** - * Option key for the cookie policy page ID. - * - * @var string - */ - private $option_cookie_notice_policy_page_id = 'cookie_notice_policy_page_id'; - - /** - * Option key for the cookie notice layout variant. - * - * @var string - */ - private $option_cookie_notice_layout = 'cookie_notice_layout'; - - /** - * Option key for the cookie notice boxed panel position. - * - * @var string - */ - private $option_cookie_notice_position = 'cookie_notice_position'; - - /** - * Option key for the cookie notice accent color. - * - * @var string - */ - private $option_cookie_notice_color = 'cookie_notice_color'; - - /** - * Option key for the cookie notice panel background color. - * - * @var string - */ - private $option_cookie_notice_bg_color = 'cookie_notice_bg_color'; - - /** - * Option key for the cookie notice panel corner rounding. - * - * @var string - */ - private $option_cookie_notice_radius = 'cookie_notice_radius'; - - /** - * Option key for the cookie notice expiration (in days). - * - * @var string - */ - private $option_cookie_notice_expiration_days = 'cookie_notice_expiration_days'; - - /** - * Legacy option key that used to store the Google Tag Manager container ID - * as a dedicated field. Superseded by 'gtm' records in the shared - * cookie_notice_tracking_integrations list; kept only so - * migrate_legacy_gtm_ga4_tracking_ids() can read and clear old values. - * - * @var string - */ - private $option_cookie_notice_gtm_id = 'cookie_notice_gtm_id'; - - /** - * Legacy option key that used to store the GA4 Measurement ID as a - * dedicated field. Superseded by 'ga4' records in the shared - * cookie_notice_tracking_integrations list; kept only so - * migrate_legacy_gtm_ga4_tracking_ids() can read and clear old values. - * - * @var string - */ - private $option_cookie_notice_ga4_id = 'cookie_notice_ga4_id'; - - /** - * Option key for the additional tracking integrations. - * - * @var string - */ - private $option_cookie_notice_tracking_integrations = 'cookie_notice_tracking_integrations'; - /** * Option key for popups feature. * @@ -370,7 +266,6 @@ public function __construct() { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); - add_action( 'admin_init', array( $this, 'migrate_legacy_gtm_ga4_tracking_ids' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) ); add_action( 'admin_head', array( $this, 'add_menu_icon_styles' ) ); } @@ -412,15 +307,6 @@ public function enqueue_admin_styles( $hook ) { FRBL_VERSION ); - // Reuse the real frontend banner styles so the settings-page preview - // renders pixel-identical to what visitors will actually see. - wp_enqueue_style( - 'frontblocks-cookie-notice', - FRBL_PLUGIN_URL . 'assets/cookie-notice/frontblocks-cookie-notice.css', - array(), - FRBL_VERSION - ); - wp_enqueue_media(); wp_add_inline_script( @@ -750,68 +636,6 @@ function updateScrollTopVisibility() { // Enqueue the WordPress media uploader for the maintenance background image field. wp_enqueue_media(); - // Separate, isolated inline script for the cookie notice fields, kept apart so a - // failure in another script can never prevent this one from running. - wp_add_inline_script( - 'jquery', - " - document.addEventListener('DOMContentLoaded', function() { - const cookieCheckbox = document.getElementById('enable_cookie_notice'); - const cookieWrapper = document.getElementById('cookie-notice-fields-wrapper'); - - if (cookieCheckbox && cookieWrapper) { - cookieCheckbox.addEventListener('change', function () { - cookieWrapper.style.display = cookieCheckbox.checked ? 'block' : 'none'; - }); - } - - const layoutSelect = document.getElementById('cookie_notice_layout'); - const positionSelect = document.getElementById('cookie_notice_position'); - const radiusSelect = document.getElementById('cookie_notice_radius'); - const positionWrapper = document.getElementById('cookie-notice-position-wrapper'); - const preview = document.getElementById('frbl-cookie-notice-preview'); - - if (layoutSelect && positionWrapper) { - layoutSelect.addEventListener('change', function () { - positionWrapper.style.display = layoutSelect.value === 'box' ? 'block' : 'none'; - }); - } - - function updatePreviewLayout() { - if (!preview) { - return; - } - - var layout = layoutSelect ? layoutSelect.value : 'bar'; - var position = positionSelect ? positionSelect.value : 'bottom-right'; - - preview.className = 'frbl-cookie-notice frbl-cookie-notice-preview frbl-cookie-notice--' + layout; - - if (layout === 'box') { - preview.className += ' frbl-cookie-notice--' + (position === 'bottom-left' ? 'left' : 'right'); - } - - if (radiusSelect) { - var radii = { none: '0', small: '12px', large: '24px' }; - preview.style.setProperty('--frbl-cookie-radius', radii[radiusSelect.value] || radii.small); - } - } - - if (layoutSelect) { - layoutSelect.addEventListener('change', updatePreviewLayout); - } - - if (positionSelect) { - positionSelect.addEventListener('change', updatePreviewLayout); - } - - if (radiusSelect) { - radiusSelect.addEventListener('change', updatePreviewLayout); - } - }); - " - ); - // Isolated inline script for the tabbed admin shell: tab switching, bulk // enable/disable, discard, and "unsaved changes" tracking on the save bar. wp_add_inline_script( @@ -1118,21 +942,9 @@ function () use ( $frblp_license ) { 'frontblocks_section_maintenance' ); - // Cookie Notice section (own full-width section: banner copy, layout, colors, GTM/GA4 and stats). - add_settings_section( - 'frontblocks_section_cookie_notice', - __( 'Cookie Notice', 'frontblocks' ), - array( $this, 'section_cookie_notice_callback' ), - $this->page_slug - ); - - add_settings_field( - $this->option_enable_cookie_notice, - __( 'Enable Cookie Notice', 'frontblocks' ), - array( $this, 'field_enable_cookie_notice' ), - $this->page_slug, - 'frontblocks_section_cookie_notice' - ); + // Cookie Notice is no longer a configurable section here — it moved to + // FrontConsent. Its settings tab (see render_cookie_notice_promo_tab()) + // is now a static promo panel, not backed by the Settings API. // PRO Features section. add_settings_section( @@ -1372,7 +1184,7 @@ public function render_page() { ), array( 'id' => 'cookies', - 'label' => __( 'Cookies', 'frontblocks' ), + 'label' => __( 'Cookie Notice', 'frontblocks' ), ), array( 'id' => 'google-signin', @@ -1537,8 +1349,6 @@ public function render_page() { render_cookie_notice_cache_notice(); ?> @@ -1594,7 +1404,7 @@ public function render_page() { @@ -2053,56 +1863,50 @@ private function section_maintenance_callback() { } /** - * Cookie Notice section callback. + * Render the Cookie Notice tab as a promo panel for FrontConsent. + * + * Cookie Notice configuration moved out of FrontBlocks entirely (see + * `includes/Frontend/CookieNotice.php` and + * `includes/Admin/CookieNoticeDeprecationNotice.php`) — this tab no + * longer renders any settings fields, only an explanation and an + * install/manage call to action, so site owners who click the tab + * looking for cookie settings find where they actually live now. * * @return void */ - private function section_cookie_notice_callback() { + private function render_cookie_notice_promo_tab() { + $is_frontconsent_active = defined( 'FRCN_VERSION' ); ?> -

- -

+
+
+

+

+
+
+
+

+ +

+

+ +

+ +

+ +

+ + + + + + + +

+ +

+ +
render_advanced_cookies_upsell(); - } - - /** - * Show what FrontBlocks PRO's Advanced Cookie Management adds on top of - * this banner. The feature's own settings (and the fields to configure - * it) live entirely in the PRO plugin — this is only a plain message, - * shown when PRO isn't active/licensed yet. - * - * @return void - */ - private function render_advanced_cookies_upsell() { - if ( frbl_is_pro_active() && $this->is_license_valid ) { - // Already unlocked: the PRO plugin renders its own settings section - // right below this one. - return; - } - - if ( ! frbl_is_pro_active() ) { - echo '
'; - echo '

' . esc_html__( 'Want per-category consent?', 'frontblocks' ) . '

'; - echo '

'; - printf( - /* translators: %s: FrontBlocks PRO link */ - esc_html__( '%s adds a "Customize" option so visitors can accept Analytics and Marketing cookies separately, with native Google Ads, Meta Pixel and Microsoft Clarity integrations that only load once accepted.', 'frontblocks' ), - 'FrontBlocks PRO' - ); - echo '

'; - echo '
'; - } else { - echo '
'; - echo '

'; - printf( - /* translators: %s: License section link */ - esc_html__( 'Advanced Cookie Management (per-category consent, Google Ads / Meta Pixel / Microsoft Clarity) is included with FrontBlocks PRO. Activate your license in the %s section below to unlock it.', 'frontblocks' ), - '' . esc_html__( 'License', 'frontblocks' ) . '' - ); - echo '

'; - echo '
'; - } } /** @@ -2125,7 +1929,7 @@ private function render_settings_section( $section ) { $is_callback_only = ! $has_fields && $section['callback']; // Check if this is a section that needs full width (rich fields, not a simple toggle grid). - $is_cpt_section = in_array( $section['id'], array( 'frontblocks_section_custom_post_types', 'frontblocks_section_maintenance', 'frontblocks_section_cookie_notice', 'frontblocks_section_image_management' ), true ); + $is_cpt_section = in_array( $section['id'], array( 'frontblocks_section_custom_post_types', 'frontblocks_section_maintenance', 'frontblocks_section_image_management' ), true ); // Show PRO CTA button before the Optional Features section. if ( 'frontblocks_section_features' === $section['id'] && ! $this->is_license_valid ) { @@ -2737,589 +2541,6 @@ class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounde option_enable_cookie_notice ] ?? false ); - $message = (string) ( $options[ $this->option_cookie_notice_message ] ?? '' ); - $accept_label = (string) ( $options[ $this->option_cookie_notice_accept_label ] ?? '' ); - $reject_label = (string) ( $options[ $this->option_cookie_notice_reject_label ] ?? '' ); - $policy_page_id = (int) ( $options[ $this->option_cookie_notice_policy_page_id ] ?? 0 ); - $layout = (string) ( $options[ $this->option_cookie_notice_layout ] ?? 'bar' ); - $position = (string) ( $options[ $this->option_cookie_notice_position ] ?? 'bottom-right' ); - $color = (string) ( $options[ $this->option_cookie_notice_color ] ?? '#687df9' ); - $bg_color = (string) ( $options[ $this->option_cookie_notice_bg_color ] ?? '#ffffff' ); - $radius = (string) ( $options[ $this->option_cookie_notice_radius ] ?? 'small' ); - $expiration = (int) ( $options[ $this->option_cookie_notice_expiration_days ] ?? 365 ); - $tracking_integrations = \FrontBlocks\Frontend\CookieNotice::get_tracking_integrations( $options ); - $site_kit_tags = $this->get_google_site_kit_managed_tags(); - $accepted_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_ACCEPTED, 0 ); - $rejected_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_REJECTED, 0 ); - $total_count = $accepted_count + $rejected_count; - $acceptance_pct = $total_count > 0 ? round( ( $accepted_count / $total_count ) * 100, 1 ) : 0; - ?> - - false, - 'ga4' => false, - ); - - if ( ! defined( 'GOOGLESITEKIT_VERSION' ) && ! class_exists( '\\Google\\Site_Kit\\Plugin' ) ) { - return $tags; - } - - $tag_manager_settings = get_option( 'googlesitekit_tagmanager_settings', array() ); - if ( is_array( $tag_manager_settings ) && ! empty( $tag_manager_settings['containerID'] ) && ( ! isset( $tag_manager_settings['useSnippet'] ) || $tag_manager_settings['useSnippet'] ) ) { - $tags['gtm'] = true; - } - - $analytics_settings = get_option( 'googlesitekit_analytics-4_settings', array() ); - if ( is_array( $analytics_settings ) && ! empty( $analytics_settings['measurementID'] ) && ( ! isset( $analytics_settings['useSnippet'] ) || $analytics_settings['useSnippet'] ) ) { - $tags['ga4'] = true; - } - - return $tags; - } - - /** - * One-time migration: move the retired dedicated GTM/GA4 ID fields into the - * shared cookie_notice_tracking_integrations list as {type, id} records, - * then clear the legacy option keys. - * - * Guarded by the legacy values themselves being non-empty, so this is a - * no-op on every run after the first: the legacy keys are unset as soon as - * they are migrated, and never written to again. - * - * @return void - */ - public function migrate_legacy_gtm_ga4_tracking_ids() { - $options = get_option( 'frontblocks_settings', array() ); - if ( ! is_array( $options ) ) { - return; - } - - $legacy = array( - $this->option_cookie_notice_gtm_id => 'gtm', - $this->option_cookie_notice_ga4_id => 'ga4', - ); - - $has_legacy_value = false; - foreach ( $legacy as $option_key => $type ) { - if ( '' !== (string) ( $options[ $option_key ] ?? '' ) ) { - $has_legacy_value = true; - break; - } - } - - if ( ! $has_legacy_value ) { - return; - } - - $integrations = \FrontBlocks\Frontend\CookieNotice::get_tracking_integrations( $options, true ); - - foreach ( $legacy as $option_key => $type ) { - $legacy_id = sanitize_text_field( (string) ( $options[ $option_key ] ?? '' ) ); - unset( $options[ $option_key ] ); - - if ( '' === $legacy_id ) { - continue; - } - - // A stored 'gtm'/'ga4' record already wins over the legacy value, - // which the settings page would have stopped displaying once the - // admin added an equivalent entry to the generic list. - $already_present = false; - foreach ( $integrations as $integration ) { - if ( $type === $integration['type'] ) { - $already_present = true; - break; - } - } - - if ( ! $already_present ) { - $integrations[] = array( - 'type' => $type, - 'id' => $legacy_id, - ); - } - } - - $options[ $this->option_cookie_notice_tracking_integrations ] = array_values( $integrations ); - - update_option( 'frontblocks_settings', $options ); - } - - /** - * Render a one-time cache notice after Cookie Notice settings are saved. - * - * @return void - */ - private function render_cookie_notice_cache_notice() { - $user_id = get_current_user_id(); - if ( ! $user_id ) { - return; - } - - $notice = get_transient( 'frbl_cookie_notice_cache_notice_' . $user_id ); - if ( ! $notice ) { - return; - } - - delete_transient( 'frbl_cookie_notice_cache_notice_' . $user_id ); - ?> -
-

- -

-
- option_enable_events, $this->option_enable_fluid_typography, $this->option_enable_maintenance, - $this->option_enable_cookie_notice, $this->option_enable_gutenberg, $this->option_enable_simple_prices_variable_products, $this->option_enable_after_add_to_cart, @@ -4052,97 +3272,7 @@ public function sanitize_settings( $value ) { $sanitized[ $key ] = sanitize_text_field( $val ); } elseif ( $this->option_maintenance_image === $key ) { $sanitized[ $key ] = absint( $val ); - } elseif ( $this->option_cookie_notice_message === $key ) { - $sanitized[ $key ] = sanitize_textarea_field( $val ); - } elseif ( in_array( $key, array( $this->option_cookie_notice_accept_label, $this->option_cookie_notice_reject_label ), true ) ) { - $sanitized[ $key ] = sanitize_text_field( $val ); - } elseif ( $this->option_cookie_notice_policy_page_id === $key ) { - $sanitized[ $key ] = absint( $val ); - } elseif ( $this->option_cookie_notice_layout === $key ) { - $sanitized[ $key ] = in_array( $val, array( 'bar', 'box', 'popup' ), true ) ? $val : 'bar'; - } elseif ( $this->option_cookie_notice_position === $key ) { - $sanitized[ $key ] = in_array( $val, array( 'bottom-right', 'bottom-left' ), true ) ? $val : 'bottom-right'; - } elseif ( $this->option_cookie_notice_color === $key ) { - $hex_color = sanitize_hex_color( $val ); - $sanitized[ $key ] = $hex_color ? $hex_color : '#687df9'; - } elseif ( $this->option_cookie_notice_bg_color === $key ) { - $hex_color = sanitize_hex_color( $val ); - $sanitized[ $key ] = $hex_color ? $hex_color : '#ffffff'; - } elseif ( $this->option_cookie_notice_radius === $key ) { - $sanitized[ $key ] = in_array( $val, array( 'none', 'small', 'large' ), true ) ? $val : 'small'; - } elseif ( $this->option_cookie_notice_expiration_days === $key ) { - $days = absint( $val ); - $sanitized[ $key ] = $days > 0 ? min( $days, 730 ) : 365; - } - } - - if ( array_key_exists( 'cookie_notice_tracking_integration_code', $value ) || array_key_exists( 'cookie_notice_tracking_remove', $value ) ) { - $tracking_integrations = \FrontBlocks\Frontend\CookieNotice::get_tracking_integrations( $current_options, true ); - $remove_types = isset( $value['cookie_notice_tracking_remove'] ) && is_array( $value['cookie_notice_tracking_remove'] ) ? array_map( 'sanitize_key', $value['cookie_notice_tracking_remove'] ) : array(); - $tracking_integrations = array_values( - array_filter( - $tracking_integrations, - static function ( $integration ) use ( $remove_types ) { - return ! in_array( $integration['type'], $remove_types, true ); - } - ) - ); - - $raw_code = (string) ( $value['cookie_notice_tracking_integration_code'] ?? '' ); - $detected = \FrontBlocks\Frontend\CookieNotice::detect_tracking_snippet( $raw_code ); - - if ( null === $detected && '' !== trim( $raw_code ) ) { - add_settings_error( - 'frontblocks_settings', - 'frbl_cookie_notice_tracking_unrecognized', - sprintf( - /* translators: %s: contact page URL. */ - esc_html__( 'The tracking code was not recognized and was not saved. Need support for this tool? Contact us at %s.', 'frontblocks' ), - 'close.technology/contacto' - ), - 'error' - ); } - - if ( $detected ) { - $tracking_integrations = array_values( - array_filter( - $tracking_integrations, - static function ( $integration ) use ( $detected ) { - return $integration['type'] !== $detected['type']; - } - ) - ); - $tracking_integrations[] = array( - 'type' => $detected['type'], - 'id' => sanitize_text_field( $detected['id'] ), - ); - } - - // Defensive re-validation for the native gtm/ga4 types: guards against a - // malformed record ever reaching the stored array outside the normal - // detect_tracking_snippet() path (e.g. a hand-edited option value). - $tracking_integrations = array_values( - array_filter( - array_map( - function ( $integration ) { - if ( 'gtm' === $integration['type'] ) { - $integration['id'] = preg_match( '/^GTM-[A-Z0-9]+$/', $integration['id'] ) ? $integration['id'] : ''; - } elseif ( 'ga4' === $integration['type'] ) { - $integration['id'] = preg_match( '/^G-[A-Z0-9]+$/', $integration['id'] ) ? $integration['id'] : ''; - } - return $integration; - }, - $tracking_integrations - ), - static function ( $integration ) { - return '' !== $integration['id']; - } - ) - ); - - $sanitized[ $this->option_cookie_notice_tracking_integrations ] = $tracking_integrations; - unset( $sanitized['cookie_notice_tracking_type'], $sanitized['cookie_notice_tracking_id'] ); } // Ensure mutual exclusion: if both description options are enabled, keep only the last one changed. diff --git a/includes/Frontend/CookieNotice.php b/includes/Frontend/CookieNotice.php index 2eb613a..d7f0ae5 100644 --- a/includes/Frontend/CookieNotice.php +++ b/includes/Frontend/CookieNotice.php @@ -192,9 +192,21 @@ private function settings_changed( $old_options, $new_options ) { /** * Check if the Cookie Notice module is enabled. * + * Always disabled once FrontConsent is active: FrontConsent is the + * dedicated cookie-consent plugin this module is being retired in favor + * of, and its own Migration class already flips + * frontblocks_settings['enable_cookie_notice'] to false on activation — + * this check is what keeps both banners from ever rendering together in + * the narrow window before that migration has run (or if a site's admin + * hand-edits the option back afterward). + * * @return bool */ private function is_enabled() { + if ( defined( 'FRCN_VERSION' ) ) { + return false; + } + $options = get_option( 'frontblocks_settings', array() ); return (bool) ( $options['enable_cookie_notice'] ?? false ); } diff --git a/includes/Plugin_Main.php b/includes/Plugin_Main.php index 40597ec..094dfdb 100644 --- a/includes/Plugin_Main.php +++ b/includes/Plugin_Main.php @@ -84,6 +84,11 @@ private function load_modules() { require_once FRBL_PLUGIN_PATH . 'includes/Admin/RedundantPlugins.php'; } new Admin\RedundantPlugins(); + + if ( ! class_exists( 'FrontBlocks\Admin\CookieNoticeDeprecationNotice' ) ) { + require_once FRBL_PLUGIN_PATH . 'includes/Admin/CookieNoticeDeprecationNotice.php'; + } + new Admin\CookieNoticeDeprecationNotice(); } // Image Management admin UI (settings section + bulk actions). Loaded diff --git a/readme.txt b/readme.txt index 95a73b1..b1ec63e 100644 --- a/readme.txt +++ b/readme.txt @@ -181,8 +181,8 @@ Register and manage events directly from WordPress. Enable the Events feature fr **Language Recommendation Banner:** Display a smart banner that detects the visitor's browser language and suggests switching to the matching version of the site. Enable the banner from the FrontBlocks settings page. The banner only appears when the site has a translated version matching the user's browser language, and respects user dismissal via a cookie. -**Cookie Notice:** -Show a lightweight, configurable cookie consent banner with Accept and Reject actions. Choose between a full-width bottom bar, a boxed panel (bottom-left or bottom-right) or a centered popup, and customize the message, button labels, cookie policy page, accent color and cookie expiration. Google Tag Manager and/or GA4 are only requested and loaded after a visitor accepts — never before — and returning visitors who already accepted get the scripts on normal page load. The settings page also shows a simple accepted/rejected acceptance-rate stat (admins are excluded from the count). Implements Google Consent Mode v2, so it also holds back tracking from other analytics/ads plugins that respect it — including **Google Site Kit** — until the visitor decides. +**Cookie Notice (moving to FrontConsent):** +The Cookie Notice module is being extracted into its own dedicated free plugin, **FrontConsent** (https://wordpress.org/plugins/front-consent/), so cookie consent is easier to find and keeps improving on its own. It keeps working here during the transition, but if you have it enabled you'll see a notice offering a one-click install of FrontConsent, which automatically migrates your existing settings and stats and turns this module off. New sites should install FrontConsent directly instead of enabling Cookie Notice here. **Advanced Cookie Management (FrontBlocks PRO):** Extend Cookie Notice with separate Necessary, Analytics and Marketing preferences, a customizable preferences dialog and a reusable trigger that lets visitors update their choices later. Google Ads, Meta Pixel and Microsoft Clarity only run after the relevant category is accepted. It also supports the official Meta Pixel for WordPress plugin by holding its Pixel and Conversions API signals until Marketing consent. @@ -225,6 +225,7 @@ More information in the [FrontBlocks PRO](https://close.technology/en/wordpress- == Changelog == = Unreleased = +* Deprecated: Cookie Notice is being extracted into its own dedicated plugin, FrontConsent. This module keeps working for now; sites with it enabled see an admin notice offering a one-click install of FrontConsent, which migrates settings/stats automatically and disables this module. = 1.5.4 = * Added: Column Link controls for native WordPress Column blocks — make an entire column clickable, optionally open the destination in a new tab, and preserve the behaviour of links and buttons placed inside it. diff --git a/tests/Unit/CookieNoticeCacheInvalidationTest.php b/tests/Unit/CookieNoticeCacheInvalidationTest.php new file mode 100644 index 0000000..4e339cc --- /dev/null +++ b/tests/Unit/CookieNoticeCacheInvalidationTest.php @@ -0,0 +1,80 @@ +cookie_notice = new CookieNotice(); + } + + /** + * Verify that cache integrations are notified after a Cookie Notice change. + * + * @return void + */ + public function test_cookie_notice_update_fires_extension_action() { + $received = array(); + $callback = static function ( $old_options, $new_options ) use ( &$received ) { + $received = array( $old_options, $new_options ); + }; + + add_action( 'frbl_cookie_notice_settings_updated', $callback, 10, 2 ); + + $old_options = array( 'enable_cookie_notice' => false ); + $new_options = array( 'enable_cookie_notice' => true ); + update_option( 'frontblocks_settings', $old_options ); + update_option( 'frontblocks_settings', $new_options ); + + remove_action( 'frbl_cookie_notice_settings_updated', $callback, 10 ); + + $this->assertSame( $old_options, $received[0] ); + $this->assertSame( $new_options, $received[1] ); + } + + /** + * Verify that unrelated settings do not trigger cache invalidation hooks. + * + * @return void + */ + public function test_unrelated_settings_update_does_not_fire_cookie_notice_action() { + $was_called = false; + $callback = static function () use ( &$was_called ) { + $was_called = true; + }; + + add_action( 'frbl_cookie_notice_settings_updated', $callback ); + + $this->cookie_notice->handle_frontblocks_settings_updated( + array( 'enable_events' => false ), + array( 'enable_events' => true ), + 'frontblocks_settings' + ); + + remove_action( 'frbl_cookie_notice_settings_updated', $callback ); + + $this->assertFalse( $was_called ); + } +} diff --git a/tests/Unit/CookieNoticeIntegrationsTest.php b/tests/Unit/CookieNoticeIntegrationsTest.php deleted file mode 100644 index 9a396e7..0000000 --- a/tests/Unit/CookieNoticeIntegrationsTest.php +++ /dev/null @@ -1,227 +0,0 @@ -settings = new CookieNotice(); - } - - /** - * Verify that cache integrations are notified after a Cookie Notice change. - * - * @return void - */ - public function test_cookie_notice_update_fires_extension_action() { - $received = array(); - $callback = static function ( $old_options, $new_options ) use ( &$received ) { - $received = array( $old_options, $new_options ); - }; - - add_action( 'frbl_cookie_notice_settings_updated', $callback, 10, 2 ); - - $old_options = array( 'enable_cookie_notice' => false ); - $new_options = array( 'enable_cookie_notice' => true ); - update_option( 'frontblocks_settings', $old_options ); - update_option( 'frontblocks_settings', $new_options ); - - remove_action( 'frbl_cookie_notice_settings_updated', $callback, 10 ); - - $this->assertSame( $old_options, $received[0] ); - $this->assertSame( $new_options, $received[1] ); - } - - /** - * Verify that unrelated settings do not trigger cache invalidation hooks. - * - * @return void - */ - public function test_unrelated_settings_update_does_not_fire_cookie_notice_action() { - $was_called = false; - $callback = static function () use ( &$was_called ) { - $was_called = true; - }; - - add_action( 'frbl_cookie_notice_settings_updated', $callback ); - - $this->settings->handle_frontblocks_settings_updated( - array( 'enable_events' => false ), - array( 'enable_events' => true ), - 'frontblocks_settings' - ); - - remove_action( 'frbl_cookie_notice_settings_updated', $callback ); - - $this->assertFalse( $was_called ); - } - - /** - * Verify that a matching active GTM4WP container is detected. - * - * @return void - */ - public function test_gtm4wp_conflict_is_detected_when_matching_container_is_enabled() { - if ( ! defined( 'GTM4WP_OPTIONS' ) ) { - define( 'GTM4WP_OPTIONS', 'gtm4wp-options' ); - } - - update_option( - 'gtm4wp-options', - array( - 'gtm-code' => 'GTM-ABC123', - 'gtm-code-placement' => 0, - ) - ); - - $method = new ReflectionMethod( Settings::class, 'is_gtm4wp_container_loading' ); - $method->setAccessible( true ); - $settings = new Settings(); - - $this->assertTrue( $method->invoke( $settings, 'GTM-ABC123' ) ); - $this->assertFalse( $method->invoke( $settings, 'GTM-OTHER' ) ); - } - - /** - * Verify that GTM4WP placement off does not produce a conflict. - * - * @return void - */ - public function test_gtm4wp_conflict_is_not_reported_when_container_code_is_off() { - if ( ! defined( 'GTM4WP_OPTIONS' ) ) { - define( 'GTM4WP_OPTIONS', 'gtm4wp-options' ); - } - - update_option( - 'gtm4wp-options', - array( - 'gtm-code' => 'GTM-ABC123', - 'gtm-code-placement' => 3, - ) - ); - - $method = new ReflectionMethod( Settings::class, 'is_gtm4wp_container_loading' ); - $method->setAccessible( true ); - $settings = new Settings(); - - $this->assertFalse( $method->invoke( $settings, 'GTM-ABC123' ) ); - } - - /** - * Verify that legacy dedicated GTM/GA4 ids are moved into the shared - * tracking-integrations list, and the legacy keys are cleared. - * - * @return void - */ - public function test_migration_moves_legacy_gtm_and_ga4_ids_into_the_shared_list() { - update_option( - 'frontblocks_settings', - array( - 'cookie_notice_gtm_id' => 'GTM-LEGACY1', - 'cookie_notice_ga4_id' => 'G-LEGACY123', - ) - ); - - ( new Settings() )->migrate_legacy_gtm_ga4_tracking_ids(); - - $options = get_option( 'frontblocks_settings' ); - - $this->assertArrayNotHasKey( 'cookie_notice_gtm_id', $options ); - $this->assertArrayNotHasKey( 'cookie_notice_ga4_id', $options ); - $this->assertSame( - array( - array( 'type' => 'gtm', 'id' => 'GTM-LEGACY1' ), - array( 'type' => 'ga4', 'id' => 'G-LEGACY123' ), - ), - $options['cookie_notice_tracking_integrations'] - ); - } - - /** - * A site with no legacy values must not gain an empty integrations key - * out of nowhere, since the migration is meant to be a no-op there. - * - * @return void - */ - public function test_migration_is_a_no_op_when_no_legacy_value_is_present() { - update_option( 'frontblocks_settings', array( 'enable_cookie_notice' => true ) ); - - ( new Settings() )->migrate_legacy_gtm_ga4_tracking_ids(); - - $options = get_option( 'frontblocks_settings' ); - - $this->assertArrayNotHasKey( 'cookie_notice_tracking_integrations', $options ); - } - - /** - * If the admin already added an equivalent entry to the generic list - * before the migration runs, the legacy value must not override it. - * - * @return void - */ - public function test_migration_does_not_override_an_existing_gtm_entry() { - update_option( - 'frontblocks_settings', - array( - 'cookie_notice_gtm_id' => 'GTM-LEGACY1', - 'cookie_notice_tracking_integrations' => array( - array( 'type' => 'gtm', 'id' => 'GTM-CURRENT' ), - ), - ) - ); - - ( new Settings() )->migrate_legacy_gtm_ga4_tracking_ids(); - - $options = get_option( 'frontblocks_settings' ); - - $this->assertArrayNotHasKey( 'cookie_notice_gtm_id', $options ); - $this->assertSame( - array( array( 'type' => 'gtm', 'id' => 'GTM-CURRENT' ) ), - $options['cookie_notice_tracking_integrations'] - ); - } - - /** - * Running the migration a second time (e.g. on a later admin_init) must - * not error or duplicate anything now that the legacy keys are gone. - * - * @return void - */ - public function test_migration_only_runs_once() { - update_option( 'frontblocks_settings', array( 'cookie_notice_gtm_id' => 'GTM-LEGACY1' ) ); - - $settings = new Settings(); - $settings->migrate_legacy_gtm_ga4_tracking_ids(); - $settings->migrate_legacy_gtm_ga4_tracking_ids(); - - $options = get_option( 'frontblocks_settings' ); - - $this->assertSame( - array( array( 'type' => 'gtm', 'id' => 'GTM-LEGACY1' ) ), - $options['cookie_notice_tracking_integrations'] - ); - } -} diff --git a/tests/Unit/CookieNoticeSettingsSanitizationTest.php b/tests/Unit/CookieNoticeSettingsSanitizationTest.php deleted file mode 100644 index de5b88f..0000000 --- a/tests/Unit/CookieNoticeSettingsSanitizationTest.php +++ /dev/null @@ -1,260 +0,0 @@ -settings = new Settings(); - - // sanitize_settings() rejects the call outright (returning the existing - // option untouched) without a valid nonce — every test below needs one - // present to actually exercise the sanitization logic under test. - $_POST['_wpnonce'] = wp_create_nonce( 'frontblocks_settings-options' ); - } - - public function tear_down() { - unset( $_POST['_wpnonce'] ); - parent::tear_down(); - } - - public function test_enable_cookie_notice_is_cast_to_bool() { - $sanitized = $this->settings->sanitize_settings( array( 'enable_cookie_notice' => '1' ) ); - $this->assertTrue( $sanitized['enable_cookie_notice'] ); - } - - public function test_enable_cookie_notice_defaults_to_false_when_absent() { - // Simulates an unchecked checkbox: the key is simply missing from $_POST. - $sanitized = $this->settings->sanitize_settings( array() ); - $this->assertFalse( $sanitized['enable_cookie_notice'] ); - } - - public function test_message_is_sanitized_as_textarea_field() { - $sanitized = $this->settings->sanitize_settings( - array( 'cookie_notice_message' => "Line one\nLine two " ) - ); - - $this->assertStringNotContainsString( ''; - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => $snippet ) ); - - $this->assertSame( - array( - array( - 'type' => 'clientify_analytics_plus', - 'id' => 'TestPixel1', - ), - ), - $sanitized['cookie_notice_tracking_integrations'] - ); - $this->assertArrayNotHasKey( 'cookie_notice_tracking_integration_code', $sanitized ); - } - - public function test_unrecognized_tracking_code_is_rejected_and_reports_an_error() { - $before_count = $this->count_tracking_notice_errors(); - - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => '' ) ); - - $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] ); - $this->assertSame( $before_count + 1, $this->count_tracking_notice_errors(), 'Expected exactly one new admin notice for the unrecognized tracking snippet.' ); - } - - public function test_blank_tracking_code_leaves_the_integration_list_empty_without_an_error() { - $before_count = $this->count_tracking_notice_errors(); - - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => '' ) ); - - $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] ); - $this->assertSame( $before_count, $this->count_tracking_notice_errors(), 'A blank snippet must not add an admin notice.' ); - } - - public function test_tracking_integration_can_be_removed() { - update_option( - 'frontblocks_settings', - array( - 'cookie_notice_tracking_integrations' => array( - array( 'type' => 'brevo', 'id' => 'brevo-key' ), - ), - ) - ); - - $sanitized = $this->settings->sanitize_settings( - array( - 'cookie_notice_tracking_integration_code' => '', - 'cookie_notice_tracking_remove' => array( 'brevo' ), - ) - ); - - $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] ); - } - - /** - * Count how many "unrecognized tracking snippet" admin notices are - * currently queued, so tests can assert a delta instead of an absolute - * count — WordPress's settings-errors list is a process-wide global that - * earlier tests (in this file or elsewhere) may have already added to. - * - * @return int - */ - private function count_tracking_notice_errors() { - $count = 0; - - foreach ( get_settings_errors( 'frontblocks_settings' ) as $error ) { - if ( 'frbl_cookie_notice_tracking_unrecognized' === $error['code'] ) { - ++$count; - } - } - - return $count; - } - - public function test_expiration_days_is_capped_at_730() { - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_expiration_days' => '99999' ) ); - $this->assertSame( 730, $sanitized['cookie_notice_expiration_days'] ); - } - - public function test_expiration_days_of_zero_falls_back_to_365() { - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_expiration_days' => '0' ) ); - $this->assertSame( 365, $sanitized['cookie_notice_expiration_days'] ); - } - - public function test_valid_gtm_id_pasted_through_the_generic_field_is_preserved_uppercased() { - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'gtm-abc123' ) ); - - $this->assertSame( - array( array( 'type' => 'gtm', 'id' => 'GTM-ABC123' ) ), - $sanitized['cookie_notice_tracking_integrations'] - ); - } - - public function test_malformed_gtm_id_is_rejected() { - $before_count = $this->count_tracking_notice_errors(); - - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'not-a-gtm-id' ) ); - - $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] ); - $this->assertSame( $before_count + 1, $this->count_tracking_notice_errors() ); - } - - public function test_gtm_snippet_replaces_a_previously_saved_gtm_entry() { - update_option( - 'frontblocks_settings', - array( - 'cookie_notice_tracking_integrations' => array( - array( 'type' => 'gtm', 'id' => 'GTM-OLD1234' ), - ), - ) - ); - - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'GTM-NEW5678' ) ); - - $this->assertSame( - array( array( 'type' => 'gtm', 'id' => 'GTM-NEW5678' ) ), - $sanitized['cookie_notice_tracking_integrations'] - ); - } - - public function test_gtm_entry_can_be_removed_through_the_generic_list() { - update_option( - 'frontblocks_settings', - array( - 'cookie_notice_tracking_integrations' => array( - array( 'type' => 'gtm', 'id' => 'GTM-ABC123' ), - ), - ) - ); - - $sanitized = $this->settings->sanitize_settings( - array( - 'cookie_notice_tracking_integration_code' => '', - 'cookie_notice_tracking_remove' => array( 'gtm' ), - ) - ); - - $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] ); - } -} diff --git a/tests/Unit/SettingsTest.php b/tests/Unit/SettingsTest.php index a1df605..b32743a 100644 --- a/tests/Unit/SettingsTest.php +++ b/tests/Unit/SettingsTest.php @@ -121,21 +121,6 @@ public function test_maintenance_image_is_cast_to_a_non_negative_integer() { $this->assertSame( 5, $sanitized['maintenance_image'] ); } - public function test_ga4_id_is_preserved_when_valid_and_uppercased() { - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'g-abc123' ) ); - - $this->assertSame( - array( array( 'type' => 'ga4', 'id' => 'G-ABC123' ) ), - $sanitized['cookie_notice_tracking_integrations'] - ); - } - - public function test_ga4_id_is_rejected_when_malformed() { - $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'not-a-valid-id' ) ); - - $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] ); - } - public function test_deactivate_short_description_and_move_content_are_mutually_exclusive() { // Both were already true in storage (should not normally happen, but // the sanitizer must still resolve to a single winner deterministically): @@ -271,9 +256,7 @@ public function test_enqueue_admin_styles_enqueues_assets_on_the_settings_page_h $this->settings->enqueue_admin_styles( 'appearance_page_frontblocks-settings' ); $this->assertTrue( wp_style_is( 'frontblocks-admin-settings', 'enqueued' ) ); - $this->assertTrue( wp_style_is( 'frontblocks-cookie-notice', 'enqueued' ) ); wp_dequeue_style( 'frontblocks-admin-settings' ); - wp_dequeue_style( 'frontblocks-cookie-notice' ); } } From 34f347764926525eb89d1b8ea7c4c69d10d5a120 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 26 Sep 2026 13:48:34 +0200 Subject: [PATCH 2/6] Move Advanced Cookie Management PRO description to FrontConsent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That feature moved with the rest of cookie consent — its readme description now lives in FrontConsent's own readme.txt instead of being duplicated here. Co-Authored-By: Claude Sonnet 5 --- readme.txt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/readme.txt b/readme.txt index b1ec63e..ba1ef94 100644 --- a/readme.txt +++ b/readme.txt @@ -181,11 +181,8 @@ Register and manage events directly from WordPress. Enable the Events feature fr **Language Recommendation Banner:** Display a smart banner that detects the visitor's browser language and suggests switching to the matching version of the site. Enable the banner from the FrontBlocks settings page. The banner only appears when the site has a translated version matching the user's browser language, and respects user dismissal via a cookie. -**Cookie Notice (moving to FrontConsent):** -The Cookie Notice module is being extracted into its own dedicated free plugin, **FrontConsent** (https://wordpress.org/plugins/front-consent/), so cookie consent is easier to find and keeps improving on its own. It keeps working here during the transition, but if you have it enabled you'll see a notice offering a one-click install of FrontConsent, which automatically migrates your existing settings and stats and turns this module off. New sites should install FrontConsent directly instead of enabling Cookie Notice here. - -**Advanced Cookie Management (FrontBlocks PRO):** -Extend Cookie Notice with separate Necessary, Analytics and Marketing preferences, a customizable preferences dialog and a reusable trigger that lets visitors update their choices later. Google Ads, Meta Pixel and Microsoft Clarity only run after the relevant category is accepted. It also supports the official Meta Pixel for WordPress plugin by holding its Pixel and Conversions API signals until Marketing consent. +**Cookie Notice (moved to FrontConsent):** +Cookie consent — including the Advanced Cookie Management upsell (per-category consent, Google Ads/Meta Pixel/Microsoft Clarity) — moved out of FrontBlocks entirely into its own dedicated free plugin, **FrontConsent** (https://wordpress.org/plugins/front-consent/), so it's easier to find and keeps improving on its own. The Cookie Notice module keeps working here during the transition, but if you have it enabled you'll see a notice offering a one-click install of FrontConsent, which automatically migrates your existing settings and stats and turns this module off. New sites should install FrontConsent directly instead of enabling Cookie Notice here. **WooCommerce Features:** Included features for WooCommerce FrontBlocks PRO. @@ -225,7 +222,7 @@ More information in the [FrontBlocks PRO](https://close.technology/en/wordpress- == Changelog == = Unreleased = -* Deprecated: Cookie Notice is being extracted into its own dedicated plugin, FrontConsent. This module keeps working for now; sites with it enabled see an admin notice offering a one-click install of FrontConsent, which migrates settings/stats automatically and disables this module. +* Deprecated: Cookie Notice is being extracted into its own dedicated plugin, FrontConsent — including the Advanced Cookie Management PRO upsell, which now lives in FrontConsent's own readme/settings. This module keeps working for now; sites with it enabled see an admin notice offering a one-click install of FrontConsent, which migrates settings/stats automatically and disables this module. The Cookie Notice settings tab no longer shows configuration fields — only an install/manage link. = 1.5.4 = * Added: Column Link controls for native WordPress Column blocks — make an entire column clickable, optionally open the destination in a new tab, and preserve the behaviour of links and buttons placed inside it. From b2a38883a965a689567c2ced168cf818bc762d01 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 26 Sep 2026 13:54:13 +0200 Subject: [PATCH 3/6] Move Cookie Notice unit/JS tests to FrontConsent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tests exercised FrontBlocks\Frontend\CookieNotice directly and had no dependency on anything removed from Settings.php in the previous commit — they now live in FrontConsent, ported to its own namespace, option names and hook prefixes, since that's where ongoing development of this code happens. Co-Authored-By: Claude Sonnet 5 --- package.json | 1 - .../CookieNoticeCacheInvalidationTest.php | 80 ---- tests/Unit/CookieNoticeConfigCallbackTest.php | 142 -------- tests/Unit/CookieNoticeContrastTest.php | 97 ----- tests/Unit/CookieNoticePolicyPageTest.php | 137 ------- .../Unit/CookieNoticeTrackingSnippetTest.php | 341 ------------------ tests/js/cookie-notice-injection.test.js | 207 ----------- 7 files changed, 1005 deletions(-) delete mode 100644 tests/Unit/CookieNoticeCacheInvalidationTest.php delete mode 100644 tests/Unit/CookieNoticeConfigCallbackTest.php delete mode 100644 tests/Unit/CookieNoticeContrastTest.php delete mode 100644 tests/Unit/CookieNoticePolicyPageTest.php delete mode 100644 tests/Unit/CookieNoticeTrackingSnippetTest.php delete mode 100644 tests/js/cookie-notice-injection.test.js diff --git a/package.json b/package.json index 7a2d8bd..b62cf56 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "build:columns-same-height": "babel assets/columns-same-height/frontblocks-columns-same-height-option.jsx --out-file assets/columns-same-height/frontblocks-columns-same-height-option.js", "build:column-link": "babel assets/column-link/frontblocks-column-link-option.jsx --out-file assets/column-link/frontblocks-column-link-option.js", "test:column-link": "node --test tests/js/column-link-frontend.test.js", - "test:cookie-notice": "node --test tests/js/cookie-notice-injection.test.js", "test:table-of-contents": "node --test tests/js/table-of-contents-frontend.test.js", "test:accessibility-utils": "node --test tests/js/accessibility-utils.test.js", "test:carousel-accessibility": "node --test tests/js/carousel-accessibility.test.js", diff --git a/tests/Unit/CookieNoticeCacheInvalidationTest.php b/tests/Unit/CookieNoticeCacheInvalidationTest.php deleted file mode 100644 index 4e339cc..0000000 --- a/tests/Unit/CookieNoticeCacheInvalidationTest.php +++ /dev/null @@ -1,80 +0,0 @@ -cookie_notice = new CookieNotice(); - } - - /** - * Verify that cache integrations are notified after a Cookie Notice change. - * - * @return void - */ - public function test_cookie_notice_update_fires_extension_action() { - $received = array(); - $callback = static function ( $old_options, $new_options ) use ( &$received ) { - $received = array( $old_options, $new_options ); - }; - - add_action( 'frbl_cookie_notice_settings_updated', $callback, 10, 2 ); - - $old_options = array( 'enable_cookie_notice' => false ); - $new_options = array( 'enable_cookie_notice' => true ); - update_option( 'frontblocks_settings', $old_options ); - update_option( 'frontblocks_settings', $new_options ); - - remove_action( 'frbl_cookie_notice_settings_updated', $callback, 10 ); - - $this->assertSame( $old_options, $received[0] ); - $this->assertSame( $new_options, $received[1] ); - } - - /** - * Verify that unrelated settings do not trigger cache invalidation hooks. - * - * @return void - */ - public function test_unrelated_settings_update_does_not_fire_cookie_notice_action() { - $was_called = false; - $callback = static function () use ( &$was_called ) { - $was_called = true; - }; - - add_action( 'frbl_cookie_notice_settings_updated', $callback ); - - $this->cookie_notice->handle_frontblocks_settings_updated( - array( 'enable_events' => false ), - array( 'enable_events' => true ), - 'frontblocks_settings' - ); - - remove_action( 'frbl_cookie_notice_settings_updated', $callback ); - - $this->assertFalse( $was_called ); - } -} diff --git a/tests/Unit/CookieNoticeConfigCallbackTest.php b/tests/Unit/CookieNoticeConfigCallbackTest.php deleted file mode 100644 index 1df5fda..0000000 --- a/tests/Unit/CookieNoticeConfigCallbackTest.php +++ /dev/null @@ -1,142 +0,0 @@ -cookie_notice = new CookieNotice(); - - update_option( 'frontblocks_settings', array( 'enable_cookie_notice' => true ) ); - $_COOKIE['frbl_cookie_consent'] = 'accepted'; - - // wp_send_json_success() only routes through the interceptable - // wp_die() below when the request is treated as an Ajax one; filtered - // rather than defining the DOING_AJAX constant so it cannot leak into - // any other test running later in the same process. - add_filter( 'wp_doing_ajax', '__return_true' ); - add_filter( 'wp_die_ajax_handler', array( $this, 'get_die_handler' ) ); - } - - public function tear_down() { - remove_filter( 'wp_die_ajax_handler', array( $this, 'get_die_handler' ) ); - remove_filter( 'wp_doing_ajax', '__return_true' ); - unset( $_COOKIE['frbl_cookie_consent'] ); - delete_option( 'frontblocks_settings' ); - parent::tear_down(); - } - - /** - * A wp_die() handler that throws instead of terminating the process, the - * same technique WP core's own WP_Ajax_UnitTestCase uses to make - * wp_send_json_success()/wp_die() testable. - * - * @return callable - */ - public function get_die_handler() { - return static function ( $message ) { - throw new Exception( is_scalar( $message ) ? (string) $message : 'die' ); - }; - } - - /** - * Invoke get_config_callback() and decode its JSON response. - * - * @return array - */ - private function get_config() { - ob_start(); - - try { - $this->cookie_notice->get_config_callback(); - } catch ( Exception $e ) { - unset( $e ); - } - - $output = ob_get_clean(); - - return json_decode( $output, true ); - } - - public function test_gtm_and_ga4_entries_are_surfaced_as_dedicated_response_keys() { - update_option( - 'frontblocks_settings', - array( - 'enable_cookie_notice' => true, - 'cookie_notice_tracking_integrations' => array( - array( 'type' => 'gtm', 'id' => 'GTM-ABC1234' ), - array( 'type' => 'ga4', 'id' => 'G-ABC1234567' ), - ), - ) - ); - - $response = $this->get_config(); - - $this->assertSame( 'GTM-ABC1234', $response['data']['gtmId'] ); - $this->assertSame( 'G-ABC1234567', $response['data']['ga4Id'] ); - } - - public function test_gtm_and_ga4_are_excluded_from_the_generic_tracking_integrations_list() { - update_option( - 'frontblocks_settings', - array( - 'enable_cookie_notice' => true, - 'cookie_notice_tracking_integrations' => array( - array( 'type' => 'gtm', 'id' => 'GTM-ABC1234' ), - array( 'type' => 'ga4', 'id' => 'G-ABC1234567' ), - array( 'type' => 'brevo', 'id' => 'brevo-key' ), - ), - ) - ); - - $response = $this->get_config(); - $types = wp_list_pluck( $response['data']['trackingIntegrations'], 'type' ); - - $this->assertNotContains( 'gtm', $types ); - $this->assertNotContains( 'ga4', $types ); - $this->assertContains( 'brevo', $types ); - } - - public function test_gtm_id_is_suppressed_when_google_site_kit_manages_the_tag() { - if ( ! defined( 'GOOGLESITEKIT_VERSION' ) ) { - define( 'GOOGLESITEKIT_VERSION', '1.0.0' ); - } - - update_option( - 'frontblocks_settings', - array( - 'enable_cookie_notice' => true, - 'cookie_notice_tracking_integrations' => array( - array( 'type' => 'gtm', 'id' => 'GTM-ABC1234' ), - ), - ) - ); - update_option( - 'googlesitekit_tagmanager_settings', - array( - 'containerID' => 'GTM-ABC1234', - 'useSnippet' => true, - ) - ); - - $response = $this->get_config(); - - $this->assertSame( '', $response['data']['gtmId'] ); - - delete_option( 'googlesitekit_tagmanager_settings' ); - } -} diff --git a/tests/Unit/CookieNoticeContrastTest.php b/tests/Unit/CookieNoticeContrastTest.php deleted file mode 100644 index fbcaa8b..0000000 --- a/tests/Unit/CookieNoticeContrastTest.php +++ /dev/null @@ -1,97 +0,0 @@ -assertSame( '#000000', CookieNotice::get_readable_text_color( '#687df9' ) ); - } - - /** - * A near-black accent should get white text — the opposite case from the - * default accent, confirming the helper picks per-color, not a fixed answer. - */ - public function test_very_dark_accent_resolves_to_white_text() { - $this->assertSame( '#ffffff', CookieNotice::get_readable_text_color( '#111827' ) ); - } - - /** - * A mid-gray accent (#767676) is a near-tie borderline case (black wins - * against it at 4.623:1 vs white's 4.542:1) — a good regression guard - * against off-by-one comparisons between "tested" and "returned" colors, - * or between >= and > in the white_contrast/black_contrast comparison. - */ - public function test_borderline_gray_accent_resolves_to_black_text() { - $this->assertSame( '#000000', CookieNotice::get_readable_text_color( '#767676' ) ); - } - - /** - * The default accent (#687df9) only reaches ~3.57:1 contrast against - * white — below the 4.5:1 threshold — so it must fall back to the dark - * neutral for text/links on the white panel, not be used verbatim. - */ - public function test_default_accent_falls_back_to_dark_neutral_on_white() { - $this->assertSame( '#111827', CookieNotice::get_readable_on_white_color( '#687df9' ) ); - } - - /** - * A very light accent (near-white) can't reach 4.5:1 against a white - * panel no matter what, so it must fall back to the dark neutral instead - * of being used verbatim as illegible link text. - */ - public function test_light_accent_falls_back_to_dark_neutral_on_white() { - $this->assertSame( '#111827', CookieNotice::get_readable_on_white_color( '#f5f5f5' ) ); - } - - /** - * Malformed input (not a valid hex color) must not throw or warn — it - * should degrade to treating the color as black, same as hex_to_rgb()'s - * own documented fallback. - */ - public function test_malformed_color_does_not_throw() { - $this->assertSame( '#ffffff', CookieNotice::get_readable_text_color( 'not-a-color' ) ); - } - - /** - * 3-digit shorthand hex colors must expand correctly, not be misread as - * 6-digit ones. - */ - public function test_shorthand_hex_color_is_expanded_correctly() { - // #fff (white) should behave identically to #ffffff. - $this->assertSame( - CookieNotice::get_readable_text_color( '#ffffff' ), - CookieNotice::get_readable_text_color( '#fff' ) - ); - } - - /** - * Each corner-rounding preset must map to its own distinct CSS length. - */ - public function test_radius_presets_resolve_to_their_own_css_value() { - $this->assertSame( '0px', CookieNotice::get_radius_value( 'none' ) ); - $this->assertSame( '12px', CookieNotice::get_radius_value( 'small' ) ); - $this->assertSame( '24px', CookieNotice::get_radius_value( 'large' ) ); - } - - /** - * An unknown preset (e.g. stale data from before an option was renamed) - * must degrade to the 'small' default rather than emitting an invalid or - * empty CSS value. - */ - public function test_unknown_radius_preset_falls_back_to_small() { - $this->assertSame( CookieNotice::get_radius_value( 'small' ), CookieNotice::get_radius_value( 'not-a-real-preset' ) ); - } -} diff --git a/tests/Unit/CookieNoticePolicyPageTest.php b/tests/Unit/CookieNoticePolicyPageTest.php deleted file mode 100644 index f8d5969..0000000 --- a/tests/Unit/CookieNoticePolicyPageTest.php +++ /dev/null @@ -1,137 +0,0 @@ -policy_page_id = self::factory()->post->create( - array( - 'post_type' => 'page', - 'post_title' => 'Cookie Policy', - 'post_status' => 'publish', - ) - ); - - update_option( - 'frontblocks_settings', - array( - 'enable_cookie_notice' => true, - 'cookie_notice_policy_page_id' => $this->policy_page_id, - ) - ); - - $this->cookie_notice = new CookieNotice(); - } - - private function render_banner_html() { - ob_start(); - $this->cookie_notice->render_banner(); - return ob_get_clean(); - } - - /** - * The banner must never render on the page the admin configured as the - * cookie policy page — otherwise a popup layout would immediately cover - * the very content the visitor is trying to read before deciding. - */ - public function test_banner_is_suppressed_on_the_configured_policy_page() { - $this->go_to( get_permalink( $this->policy_page_id ) ); - - $this->assertSame( '', $this->render_banner_html() ); - } - - /** - * The banner must still render on every other page — suppression is - * scoped to exactly the configured page, not a global kill switch. - */ - public function test_banner_still_renders_on_other_pages() { - $other_page_id = self::factory()->post->create( array( 'post_type' => 'page' ) ); - $this->go_to( get_permalink( $other_page_id ) ); - - $this->assertStringContainsString( 'id="frbl-cookie-notice"', $this->render_banner_html() ); - } - - /** - * With no policy page configured at all, nothing should ever be suppressed. - */ - public function test_no_policy_page_configured_means_no_suppression_anywhere() { - update_option( - 'frontblocks_settings', - array( 'enable_cookie_notice' => true ) - ); - $cookie_notice = new CookieNotice(); - - $this->go_to( get_permalink( $this->policy_page_id ) ); - - ob_start(); - $cookie_notice->render_banner(); - $html = ob_get_clean(); - - $this->assertStringContainsString( 'id="frbl-cookie-notice"', $html ); - } - - /** - * The banner must render already invisible/off-screen by default — the - * 'frbl-cookie-notice--init' class is what a returning, already-decided - * visitor never sees removed, avoiding the flash frontblocks-cookie-notice.js - * used to cause by hiding a banner that started out visible. - */ - public function test_banner_renders_with_the_init_class_by_default() { - $other_page_id = self::factory()->post->create( array( 'post_type' => 'page' ) ); - $this->go_to( get_permalink( $other_page_id ) ); - - $html = $this->render_banner_html(); - - $this->assertMatchesRegularExpression( '/class="[^"]*\bfrbl-cookie-notice--init\b[^"]*"/', $html ); - } - - /** - * A no-JS visitor must still see the banner: the printed