Skip to content

Add generic script/iframe blocking until consent - #10

Closed
Castellon-ACM wants to merge 1 commit into
mainfrom
feature/script-iframe-blocking
Closed

Castellon-ACM wants to merge 1 commit into
mainfrom
feature/script-iframe-blocking

Conversation

@Castellon-ACM

Copy link
Copy Markdown
Contributor

Summary

Closes #5.

  • Adds includes/Frontend/ScriptBlocker.php, a new Frontend module that holds back arbitrary third-party <script src="..."> and <iframe src="..."> embeds (YouTube, Maps, social widgets, custom tracking snippets, etc.) until the visitor accepts the configured consent category — a generic complement to CookieNotice's existing Google Consent Mode v2 signaling, which only affects Consent-Mode-aware tags.
  • New settings field ("Script & Iframe Blocking") on the FrontConsent settings screen: a one-rule-per-line pattern|category textarea (a deliberately simple v1 — no existing repeatable-rows UI on this settings screen to match the style of). category reuses the existing analytics/marketing slugs CookieNotice already uses for tracking integrations, rather than inventing new ones.
  • Updated the two "Get FrontConsent PRO" upsell mentions of "generic script blocking" (now a Free feature per the issue) to instead point at the still-Pro "per-service placeholders" (YouTube/Maps/social embeds) feature.
  • New assets/cookie-notice/frontconsent-script-blocker.js, enqueued only when at least one rule is configured, which revives placeholders once their category is allowed, hooking into the same frcnCookieConsent event frontconsent-cookie-notice.js already dispatches.

Safety/performance considerations

  • Zero cost when unused: ob_start() is never called, and the revival script is never enqueued, when no rules are configured — a site that doesn't use this feature pays nothing extra.
  • Never buffers admin/AJAX/REST/cron responses — maybe_start_output_buffer() explicitly bails on is_admin(), wp_doing_ajax(), wp_doing_cron(), wp_is_json_request(), REST_REQUEST and XMLRPC_REQUEST.
  • Conservative, well-anchored regex-based rewriting (WordPress has no built-in safe tag-rewrite utility for this precise case): the tag-boundary pattern correctly stops at an opening tag's real closing > even when a quoted attribute value contains one, attribute extraction/removal handles both quote styles, unquoted values, and irregular whitespace, and self-closing tags keep their trailing / at the end of the tag rather than in the middle of the newly appended attributes.
  • Two real bugs found and fixed during development, both caught by writing a standalone functional check before the WP test environment was available (see Test plan):
    • The output-buffering short-circuit used a case-sensitive strpos($html, 'src'), silently skipping rewriting for any page whose markup happened to use uppercase tags/attributes (<SCRIPT SRC="...">). Fixed to stripos().
    • The attribute regexes used a plain \b before the attribute name, which also matches right after a hyphen — so a real-world data-src="..." lazy-load attribute (used by lazysizes and similar libraries) was wrongly matched as the actual src attribute. Fixed with a (?<![\w-]) negative lookbehind instead.
  • Never leaves a live tracking src: a blocked <script> has its src renamed to data-frcn-src and gets type="text/plain" (per the HTML spec, an unsupported script type is never fetched); a blocked <iframe> gets src="about:blank" rather than no src at all (which some browsers treat as reloading the parent document).
  • Client-side revival creates a genuinely new <script> element rather than mutating the placeholder in place — cloning a type="text/plain" script and changing its type does not make a browser execute it, per the HTML spec's "already started" flag.

Test plan

  • composer lint (phpcs) — passes.
  • composer phpstan — passes, no errors.
  • New tests/Unit/ScriptBlockerTest.php (script/iframe matching, non-matching, self-closing tags, attribute order/quoting variations, multiple tags per page, case-insensitivity, data-src regression, no-rules no-op, textarea round-trip) — logic additionally verified via a standalone PHP script exercising the same class outside the WordPress test bootstrap, since a WordPress+MySQL integration test environment could not be provisioned in this sandbox (no mysql/svn available, and installing them via Homebrew required building LLVM from source). The PHP syntax is valid (php -l) and both phpcs/phpstan pass over the new file.
  • New tests/js/script-blocker.test.js, added to package.json's test:script-blocker script (and covered by the existing test:js glob) — npm run test:js passes all 26 tests (6 new + 20 pre-existing), confirming no regressions.

Caveats

  • PHPUnit's own integration suite (composer test) could not be executed end-to-end in this environment due to the missing WordPress+MySQL test fixture — please run it in CI/locally to confirm; lint, phpstan, and the standalone functional check give reasonable confidence in the meantime.
  • The settings UI is a plain one-rule-per-line textarea rather than a JS-driven repeatable-rows widget, per the issue's own suggested v1 scope.

🤖 Generated with Claude Code

Introduce ScriptBlocker, a new Frontend module that holds back arbitrary
third-party <script src="..."> and <iframe src="..."> embeds (YouTube,
Maps, social widgets, custom tracking snippets) until the visitor accepts
the configured consent category — complementing CookieNotice's existing
Google Consent Mode v2 support, which only affects Consent-Mode-aware tags.

Server-side: a new "Script & Iframe Blocking" settings field stores a list
of match-pattern/category rules (one-rule-per-line "pattern|category"
textarea, a simple v1 given no existing repeatable-rows UI to match). When
at least one rule is configured, ScriptBlocker buffers the frontend HTML
(ob_start(), skipped entirely for admin/AJAX/REST/cron requests, and never
started at all when no rules exist) and rewrites matching opening tags to
inert placeholders via conservative, well-anchored regexes: <script> gets
type="text/plain" with its src moved to data-frcn-src, and <iframe> gets
its src swapped to "about:blank" with the real URL likewise moved to
data-frcn-src.

Client-side: a new frontconsent-script-blocker.js file (enqueued only when
rules are configured) revives placeholders once their category is allowed
— creating a brand-new <script> element (cloning a text/plain script and
changing its type would not make it execute) or restoring an <iframe>'s
real src — hooking into the same frcnCookieConsent event
frontconsent-cookie-notice.js already dispatches.

Test coverage: tests/Unit/ScriptBlockerTest.php covers matching/non-matching
scripts and iframes, self-closing tags, attribute order/quoting variations,
multiple tags on one page, case-insensitivity, and a data-src lazy-load
attribute correctly not being confused with the real src (a real bug found
and fixed during development, alongside a case-sensitivity bug in the
buffering short-circuit). tests/js/script-blocker.test.js covers script
revival, iframe src restoration, a rejected category being left alone, and
a per-category override extension point.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add generic script/iframe blocking until consent (Free-tier requirement from docs/plan.md)

2 participants