Initial FrontConsent plugin: cookie consent banner extracted from FrontBlocks - #1
Conversation
…ntBlocks Standalone free plugin providing the cookie consent banner (Accept/Reject, Google Consent Mode v2, GTM/GA4 and other tracking integrations gated behind consent) previously bundled inside FrontBlocks' Cookie Notice module. Includes a one-time migration that copies settings and stats from FrontBlocks' frontblocks_settings option on activation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
- phpstan.neon.dist + tests/phpstan-bootstrap.php (level 1, WordPress stubs) - .phplint.yml - .github/workflows/phplint.yml and phpunit.yml (target main) - phpunit.xml.dist, tests/bootstrap.php, bin/install-wp-tests.sh - Ported Cookie Notice unit/JS tests from FrontBlocks (namespace, option names and hook prefixes updated to FrontConsent's own) - phpcbf formatting fix in Settings.php Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b90c60fbac
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Add a persistent "Cookie preferences" reopen trigger so a visitor can
withdraw an acceptance or replace a rejection at any time, instead of
the decision being permanent once made (readme.txt already claimed
this was possible).
- Migration: convert FrontBlocks' retired cookie_notice_gtm_id /
cookie_notice_ga4_id fields into the shared
cookie_notice_tracking_integrations list during migration — copying
them verbatim left them dead, since CookieNotice never reads those
two keys directly.
- Fix activation redirect to the actual registered settings URL
(options-general.php, since the page is added via add_options_page())
instead of a nonexistent admin.php one.
- Compare Site Kit's configured GTM/GA4 ID against FrontConsent's own
before suppressing it, in both the AJAX config endpoint and the
settings page UI — a different Site Kit tag no longer hides/drops a
distinct one configured here.
- Replace the default banner message's implied-consent wording ("by
browsing this website, you agree...") with one that asks for an
affirmative decision, matching docs/plan.md's own AEPD requirement.
- Set Google Consent Mode's default state from the registered JS file
too, not only the inline wp_head script, so a strict CSP that blocks
the unnonced inline script no longer leaves Consent Mode without a
default entirely.
- Add regression tests for all of the above (Migration, Site Kit ID
comparison, reopen trigger) plus JS coverage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02063eb8f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Require Migration.php and Frontend/CookieNotice.php directly in Plugin_Main, instead of relying on vendor/autoload.php — the plugin ships no runtime Composer dependencies, so that autoloader is only ever present in a local dev checkout, never in an installable plugin zip. Without this, activation fataled on any such install. - Send the accept/reject consent update through gtag() (queuing the same arguments-object shape gtag() itself produces), instead of pushing a plain array onto dataLayer directly, which a running Consent Mode-aware tag does not process as a consent command. - Let a visitor reopen consent from the cookie policy page: the reopen trigger's click handler now navigates to the home page there instead of reloading in place, since the banner never renders on the policy page itself. - Clear the deferred activation-redirect flag on a bulk/network activation bail, instead of leaving it to redirect an unrelated later admin request. - Fix get_readable_on_white_color() to test contrast against the panel's actual configured background instead of always assuming white, so a dark background with a light accent no longer falls back to a dark neutral that would be nearly invisible there. - Add regression tests for all of the above (PHP + JS). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a30ce20de
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Run the CSP-fallback Consent Mode default immediately at script parse time (not gated by DOMContentLoaded) and enqueue the script in <head> (in_footer: false) instead of the footer — otherwise it ran far too late to matter under a CSP that blocks the inline wp_head script. - Apply a stale-consent override after any per-category override state, not before — an add-on reporting stale consent must always win over a (possibly stale) granted override, matching the inline PHP script's own ordering. - Claim the one-time migration guard atomically via add_option() instead of a plain get_option()/update_option() check-then-act pair, so two concurrent first requests can no longer both run the migration and double-count migrate_stats()'s additive counters. - Reveal the "Cookie preferences" reopen trigger immediately after an in-page accept/reject, not only on the next page load. - Construct Frontend\CookieNotice (which registers the cache-purge and frcn_cookie_notice_settings_updated hooks) before Migration::maybe_run() writes frontconsent_settings for the first time, so those hooks are registered when the migration's own write happens. - Add regression tests for all of the above (PHP + JS). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7104480028
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Convert both wp_head inline <script> tags (Consent Mode default and the tracking bootstrap) to wp_add_inline_script() attached to a source-less registered handle, instead of raw <script> echoes. - Convert both <noscript><style> fallback blocks to wp_add_inline_style() the same way. - Replace the inline cookie-icon SVG markup (previously echoed with a phpcs:ignore) with an external SVG file loaded via CSS mask-image, so the icon still picks up the configured accent color through currentColor-equivalent background-color, without ever echoing raw SVG markup from PHP. - Add an "External services" section to readme.txt documenting Google Tag Manager/GA4, Clientify Analytics, Brevo and ChatGPT Ads (OpenAI), with what's sent, when, and links to each provider's terms/privacy policy. - Remove the dead Donate link (404) and add the submitting account to Contributors. - Update JS test regex extraction to match the new PHP string-concat script bodies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9baafa0a3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Run Migration::maybe_run() before constructing Frontend\CookieNotice, instead of after — its constructor reads is_enabled() once, synchronously, so constructing it first meant the very first request after activation saw the pre-migration (disabled) state and never registered the frontend hooks for that request. Migration now calls CookieNotice::handle_settings_changed() directly for its own write (both methods made public static), instead of relying on CookieNotice's own option hooks to have been registered in time. - Add a real no-JS <form> fallback for Accept/Reject: the buttons were previously type="button" with JS-only click handlers, so without JavaScript neither control did anything, permanently blocking content in the box/popup layouts for a no-JS visitor. They now also submit a form to admin-post.php (log_consent_form_callback(), processed by the newly extracted, directly testable process_consent_form_submission()) which sets the same cookie server-side and redirects back; JS continues to intercept the click and handle it entirely client-side when it can run. - Use an atomic SQL addition for migrate_stats() instead of a plain get_option()/update_option() round trip, so a decision logged by a concurrent visitor between migration's read and write is no longer silently overwritten. - Always set the consent cookie's path to '/' instead of COOKIEPATH — on an install where Home URL and Site URL have different paths, COOKIEPATH (Home URL-derived) scoped the cookie to a path the admin-ajax.php request (Site URL-derived) fell outside of, so the browser omitted it and configured tracking never loaded after consent. - Skip the deferred activation redirect during admin AJAX requests (wp_doing_ajax()) — an unrelated background request like Heartbeat could otherwise consume the flag before the activating administrator ever saw the settings redirect. - Add regression tests for all of the above. Not changed (dismissed in review thread, with reasoning): - actions/checkout@v7 in phplint.yml: the tag exists and CI has been passing with it; kept as-is. - Binary Consent Mode "denied" not fully blocking an independently loaded Site Kit tag: inherent to how Google Consent Mode Advanced mode works for any integration, not something this plugin's binary accept/reject can change without actively intercepting network requests — the documented differentiator of the planned Pro tier. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 757b1623f5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Set the no-JS form's consent cookie via a hand-built Set-Cookie header() instead of setcookie()'s options-array form, which requires PHP 7.3+ — this plugin's own declared minimum is PHP 7.0, so that form silently failed to store anything on 7.0-7.2. The header string itself is built in a separately testable method (build_consent_cookie_header_value()). - Drop the nonce check on the no-JS form entirely: the banner it's embedded in is cache-neutral HTML (rendered identically for every visitor of a URL), so any nonce baked into it goes stale the moment a full-page cache keeps that page around, and unlike the AJAX path a no-JS visitor has no way to fetch a fresh one first. Same accepted risk profile as the already-unauthenticated get_config_callback(). - Post the no-JS form's action to a frontend-origin-forced admin-post.php URL (get_frontend_origin_admin_url(), generalized from get_ajax_url()) instead of a raw admin_url() call, so the cookie set on submission isn't scoped to a different backend host that a redirect back to the frontend host would never see again. - Run the existing tests/js/ suite in CI (a separate js-tests job), instead of it existing only as a local npm script nothing invoked. - Scope the admin settings preview CSS rules under .frcn-settings-wrapper, matching every other rule in the stylesheet, so they can't leak into markup emitted elsewhere in wp-admin. - Update/add tests for all of the above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9b12a8ce95
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // and Site URL have different paths, COOKIEPATH would scope | ||
| // the cookie to a path admin-ajax.php falls outside of, and | ||
| // the browser would silently omit it from that request. | ||
| 'cookiePath' => '/', |
There was a problem hiding this comment.
Isolate consent cookies per WordPress installation
When two independent WordPress installations share a hostname under different paths, such as /shop/ and /blog/, this root-scoped cookie is sent to both sites and both use the same non-multisite name, frcn_cookie_consent. Accepting on one installation can therefore make the other suppress its banner and load its configured trackers without a decision there; use an installation-specific cookie name or another scope that remains shared with the AJAX endpoint without colliding with sibling installations.
AGENTS.md reference: AGENTS.md:L7-L7
Useful? React with 👍 / 👎.
| // Without JS, nothing would ever remove '--init' (see the class list | ||
| // above), so the banner would stay invisible forever — this resets it | ||
| // back to plain visible/static for a no-JS visitor. | ||
| $noscript_css = '#frcn-cookie-notice.frcn-cookie-notice--init { opacity: 1; pointer-events: auto; transform: none; }'; |
There was a problem hiding this comment.
Keep the no-JS banner dismissed after a decision
When JavaScript is disabled, submitting the new fallback form stores the decision and redirects, but this unconditional <noscript> rule reveals the cache-neutral banner again and no server-side rendering path consumes the cookie. Consequently every subsequent non-policy page shows the controls again after either decision, making the fallback appear not to persist and repeatedly occupying content; fresh evidence beyond the earlier inert-control report is that the form now successfully stores a decision that this no-JS rendering path never honors.
Useful? React with 👍 / 👎.
| // phpcs:ignore WordPress.Security.NonceVerification.Missing -- deliberately unauthenticated, see this method's own docblock above. | ||
| $decision = isset( $_POST['frcn_decision'] ) ? sanitize_key( wp_unslash( $_POST['frcn_decision'] ) ) : ''; | ||
|
|
||
| if ( in_array( $decision, array( 'accepted', 'rejected' ), true ) ) { |
There was a problem hiding this comment.
Restore CSRF protection for no-JS consent submissions
Because this public admin-post.php handler accepts the decision without a nonce or equivalent origin-bound token, an external site can auto-submit frcn_decision=accepted; the target-origin response then sets the consent cookie and redirects, so a later visit can load tracking even though the visitor never chose Accept. The current revision removed the stale embedded nonce entirely rather than obtaining a fresh token through an uncached step, and it also lets forged requests inflate the aggregate counters; restore CSRF protection for this state-changing form.
AGENTS.md reference: AGENTS.md:L83-L83
Useful? React with 👍 / 👎.
| if (typeof frcnCookieNotice !== 'undefined') { | ||
| setConsentModeDefault(); |
There was a problem hiding this comment.
Make the CSP fallback independent of inline localization
Under the documented strict-CSP scenario where same-origin external scripts are allowed but unnonced inline scripts are blocked, the frcnCookieNotice assignment emitted by wp_localize_script() is blocked just like the PHP bootstrap. This guard therefore skips setConsentModeDefault(), and init() later returns for the same missing object, leaving independently loaded Consent Mode-aware tags without the default-denied state; fresh evidence after the earlier CSP fixes is that the external fallback still depends on inline-localized data before it can run.
AGENTS.md reference: AGENTS.md:L7-L7
Useful? React with 👍 / 👎.
Summary
docs/plan.mdfor the full product plan).FrontConsent\namespace andfrontconsent_settingsoption.includes/Migration.php: one-time migration on activation that copies Cookie Notice settings and accepted/rejected stats from FrontBlocks'frontblocks_settingsoption, and disables FrontBlocks' own banner so the two never overlap.AGENTS.md/CLAUDE.md,readme.txt/readme.md,phpcs.xml.dist,composer.jsonscaffolded following the same conventions as FrontBlocks/its other CLOSE plugins.Companion change in FrontBlocks: closemarketing/frontblocks#314 (Cookie Notice tab there now promotes installing this plugin instead of showing config fields).
Test plan
php -lon all PHP filescomposer lint/composer phpstan— not run here yet (freshvendor/installed locally); please run in CIcomposer test— no WordPress test environment available in this environment🤖 Generated with Claude Code