fix(html-embed): strip embeds at serve time on authenticated read paths (#28)

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 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-20 21:52:32 +03:00
parent b53b0c651e
commit 6a052b88b4

View File

@@ -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;
}