Skip to content

[8.4-stable] feat(csp): Remove inline script from the preview frame - #4372

Merged
tvdeyen merged 8 commits into
8.4-stablefrom
backport/8.4-stable/pr-4334
Oct 16, 2026
Merged

tvdeyen merged 8 commits into
8.4-stablefrom
backport/8.4-stable/pr-4334

Conversation

@alchemycms-bot

Copy link
Copy Markdown

Backport

This will backport the following commits from main to 8.4-stable:

Questions ?

Please refer to the Backport tool documentation

The bundle opened by reading a bare Alchemy global, which only resolved
because the preview partial rendered an inline script to define it. That
forced host applications running their own content security policy to
allow an inline script on every frontend page.

Nothing outside this file ever used the element selector it hung off that
global, so module level state serves it just as well. Without a namespace
object to attach methods to, plain functions over module state replace the
object literal and the shared this it existed to provide.

The preview bundle ships to the frontend, where none of the globals the
admin setup file defines exist, so its spec runs as its own test project
rather than borrowing the admin environment.

(cherry picked from commit 8ab4a89)
With the bundle no longer reading a global, the inline script the partial
rendered has nothing left to define: its only payload was a locale that
nothing reads, since the admin resolves the current locale from the html
lang attribute. Removing it takes an inline script off every frontend page
of host applications that render the alchemy/edit_mode partial.

(cherry picked from commit 5dbe047)
The preview script had no coverage at all, so nothing caught a break in
the message protocol it shares with the admin window. These examples drive
the real iframe on the page edit screen and assert both directions:
clicking an element in the preview selects its editor, and clicking an
editor selects the element in the preview.

Exercising the built bundle in a browser also guards the assumption that
the preview frame needs no inline script to boot.

(cherry picked from commit fb60855)
The preview script is render blocking and host layouts decide where the
alchemy/edit_mode partial goes, so any page content rendered below it did
not exist yet when the script collected its elements. Those elements were
silently not selectable in the preview frame.

Waiting for DOMContentLoaded fixes that, guarded by a readyState check
because the event has already fired when the script loads late. The dummy
layout now renders the partial above its content, so the system specs
cover the arrangement that used to break.

(cherry picked from commit 030a52a)
The preview wrote a selected class onto host application markup, but
nothing styles or reads it: the script only used it to remember which
element to leave alone on hover. Worse, deselecting removed the class from
every preview element, stripping a selected class the host application may
have put there for its own purposes. A module level reference keeps that
state without touching the host's markup.

(cherry picked from commit 70e1a46)
The click listener sat on each element in the bubble phase, so handlers
attached to links inside an element ran first. Client side routers like
vue-router bind their click handler directly to the link and only skip
navigation once the event was default prevented, which it was not yet,
so clicking a link in the preview navigated the frame away.

A single click listener on the window in the capture phase runs before
any handler in the page. Capturing on the elements themselves would have
reached the outermost of nested elements first, so the listener resolves
the innermost element from the event target instead. Interactive content
inside an element no longer receives clicks in the preview, consistent
with links already being inert there.

(cherry picked from commit f598bfe)
Capturing every click inside an element kept routers from navigating, but
it also starved the page's own interactive content: tabs, accordions and
carousel controls inside an element stopped reacting in the preview,
where they used to work and select their element at the same time.

Navigation only starts from links. RouterLink, NuxtLink and React
Router's Link all render an anchor and navigate from its click handler,
so only clicks inside an anchor are taken in the capture phase. Every
other click reaches the page first and selects its element on the way out
of the bubble phase, as before. Links rendered through the custom slot of
RouterLink or NuxtLink without an anchor are not intercepted.

(cherry picked from commit d0577eb)
Every click inside an element was default prevented after the page had
handled it, so native behavior like toggling details or checking a
checkbox never happened in the preview. Only navigation needs blocking.

Clicks on links and on form submit buttons navigate the frame away, so
those are taken in the capture phase as before. Every other click now
only selects its element and keeps its default behavior. A button only
counts as a submit button when it belongs to a form, since a button
without a type reports submit even when it submits nothing.

(cherry picked from commit c1fa5f3)
@alchemycms-bot
alchemycms-bot Bot requested a review from a team as a code owner October 16, 2026 11:27
@alchemycms-bot alchemycms-bot Bot added the enhancement New feature or enhancement label Oct 16, 2026
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.42%. Comparing base (f6911c6) to head (53270f5).

Additional details and impacted files
@@             Coverage Diff             @@
##           8.4-stable    #4372   +/-   ##
===========================================
  Coverage       98.42%   98.42%           
===========================================
  Files             369      369           
  Lines            9749     9749           
===========================================
  Hits             9595     9595           
  Misses            154      154           
Flag Coverage Δ
mariadb 97.03% <ø> (ø)
mysql 95.94% <ø> (ø)
postgresql 95.94% <ø> (ø)
sqlite 95.94% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tvdeyen
tvdeyen merged commit 3ffe370 into 8.4-stable Oct 16, 2026
28 checks passed
@tvdeyen
tvdeyen deleted the backport/8.4-stable/pr-4334 branch October 16, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant