fix(html-embed): execute embeds on public shares; toggle is server-side kill switch
The html-embed feature toggle was enforced CLIENT-side in the NodeView (reads settings.htmlEmbed from the logged-in workspace), so an anonymous public-share viewer — who has no workspace context — always saw it as OFF and got a placeholder instead of the executing embed. That broke the whole point (a tracker must run for anonymous visitors). Make it server-authoritative: - share.service prepareContentForShare (the single path both share-content flows use) strips htmlEmbed from served content when the workspace toggle is OFF; both callers (updatePublicAttachments host page + lookupTransclusionForShare) resolve the toggle once and pass it. Fail-closed: missing workspace -> OFF -> stripped. - NodeView executes whatever it was served in read-only/share mode (shouldExecute = !editor.isEditable || htmlEmbedEnabled); the disabled placeholder now only shows in the editable editor when OFF. Net: anonymous share + toggle ON -> server serves the (admin-authored) embed -> it executes for everyone; toggle OFF -> stripped server-side from every share-content path (true kill switch); a non-admin embed can never be served (save-path strip). No XSS regression in the editable editor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,16 @@ export default function HtmlEmbedView(props: NodeViewProps) {
|
||||
const workspace = useAtomValue(workspaceAtom);
|
||||
const htmlEmbedEnabled = workspace?.settings?.htmlEmbed === true;
|
||||
|
||||
// Execution policy split by editor mode:
|
||||
// - READ-ONLY / public-share view: the SERVER already decided whether to
|
||||
// include the embed (it strips htmlEmbed from shared content when the
|
||||
// workspace toggle is OFF). An anonymous viewer has no workspace and thus
|
||||
// reads `htmlEmbedEnabled` as false, so we must NOT gate execution on it
|
||||
// here — we execute exactly the `source` the server chose to serve.
|
||||
// - EDITABLE editor (admin authoring): keep gating on the per-workspace
|
||||
// toggle so an admin sees the inert placeholder when the feature is OFF.
|
||||
const shouldExecute = !editor.isEditable || htmlEmbedEnabled;
|
||||
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [draft, setDraft] = useState<string>(source || "");
|
||||
@@ -72,12 +82,12 @@ export default function HtmlEmbedView(props: NodeViewProps) {
|
||||
// feature toggle is OFF we clear the container and inject/execute nothing.
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return;
|
||||
if (htmlEmbedEnabled) {
|
||||
if (shouldExecute) {
|
||||
renderRawHtml(contentRef.current, source || "");
|
||||
} else {
|
||||
contentRef.current.innerHTML = "";
|
||||
}
|
||||
}, [source, htmlEmbedEnabled]);
|
||||
}, [source, shouldExecute]);
|
||||
|
||||
const openEditor = useCallback(() => {
|
||||
setDraft(source || "");
|
||||
@@ -116,9 +126,12 @@ export default function HtmlEmbedView(props: NodeViewProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!htmlEmbedEnabled ? (
|
||||
// Feature disabled for this workspace: never inject/execute the source.
|
||||
// Show a neutral placeholder so an existing embed is visibly inert.
|
||||
{!shouldExecute ? (
|
||||
// Feature disabled for this workspace AND we're in the editable editor:
|
||||
// never inject/execute the source. Show a neutral placeholder so an
|
||||
// existing embed is visibly inert for the authoring admin. Read-only /
|
||||
// share viewers never hit this branch (`shouldExecute` is always true
|
||||
// there) — they execute exactly the source the server chose to serve.
|
||||
<div className={classes.htmlEmbedPlaceholder}>
|
||||
<IconCode size={18} />
|
||||
<Text size="sm">
|
||||
|
||||
Reference in New Issue
Block a user