From 6a052b88b496e15718f61db8c2d1bc6f28283df3 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Sat, 20 Jun 2026 21:52:32 +0300 Subject: [PATCH] fix(html-embed): strip embeds at serve time on authenticated read paths (#28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the workspace htmlEmbed kill-switch. The public-share path already strips at serve time when the toggle is OFF, but the authenticated read paths (/info and /history/info) returned page/history content with embeds intact, so a disabled feature kept executing for in-workspace view-only viewers until the page was next saved. Now both paths resolve the workspace toggle and run stripHtmlEmbedNodes when it's OFF (fail-closed on a missing workspace), before any markdown/html format conversion. Admin-authored content only — completeness, not privilege escalation. Injects WorkspaceRepo into PageController. Co-Authored-By: Claude Opus 4.8 --- apps/server/src/core/page/page.controller.ts | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/server/src/core/page/page.controller.ts b/apps/server/src/core/page/page.controller.ts index 968480c9..28ec083e 100644 --- a/apps/server/src/core/page/page.controller.ts +++ b/apps/server/src/core/page/page.controller.ts @@ -39,6 +39,11 @@ import { } from '../casl/interfaces/space-ability.type'; import SpaceAbilityFactory from '../casl/abilities/space-ability.factory'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; +import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo'; +import { + isHtmlEmbedFeatureEnabled, + stripHtmlEmbedNodes, +} from '../../common/helpers/prosemirror/html-embed.util'; import { RecentPageDto } from './dto/recent-page.dto'; import { CreatedByUserDto } from './dto/created-by-user.dto'; import { DuplicatePageDto } from './dto/duplicate-page.dto'; @@ -63,6 +68,7 @@ export class PageController { constructor( private readonly pageService: PageService, private readonly pageRepo: PageRepo, + private readonly workspaceRepo: WorkspaceRepo, private readonly pageHistoryService: PageHistoryService, private readonly spaceAbility: SpaceAbilityFactory, private readonly pageAccessService: PageAccessService, @@ -92,6 +98,18 @@ export class PageController { const permissions = { canEdit, hasRestriction }; + if (page.content) { + const workspace = await this.workspaceRepo.findById(page.workspaceId); + if (!isHtmlEmbedFeatureEnabled(workspace?.settings)) { + // Kill-switch: when the workspace feature is OFF, never serve raw + // htmlEmbed nodes on the read path (mirrors the public-share strip), + // so disabling the feature is an immediate, total kill-switch and not + // dependent on the page being re-saved. Admin-authored content only. + // Fail-closed: a missing workspace resolves to OFF and is stripped. + page.content = stripHtmlEmbedNodes(page.content) as any; + } + } + if (dto.format && dto.format !== 'json' && page.content) { const contentOutput = dto.format === 'markdown' @@ -536,6 +554,16 @@ export class PageController { await this.pageAccessService.validateCanView(page, user); + if (history.content) { + const workspace = await this.workspaceRepo.findById(page.workspaceId); + if (!isHtmlEmbedFeatureEnabled(workspace?.settings)) { + // Kill-switch: history snapshots are an authenticated read path too, so + // strip htmlEmbed when the workspace feature is OFF (same as /info and + // the public-share path). Fail-closed on a missing workspace. + history.content = stripHtmlEmbedNodes(history.content) as any; + } + } + return history; }