Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 60x 43x 60x 60x 60x 1x 1x 1x 1x 60x 43x 43x 3x 1x 1x 1x 2x 43x 117x 43x 43x 43x 117x | import { useMemo } from "react";
import { Box } from "@mui/material";
import {
ContentState,
EditorState,
RawDraftContentState,
convertFromRaw,
} from "draft-js";
import { ReadOnlyEditor } from "@components/PaymentForm";
import {
alignmentInnerDivSx,
alignmentWrapperSx,
buildBlockStyleFn,
documentHeadingSx,
listStyleSx,
} from "@shared/HFInputs/HFRichInput/blockStyle";
import { tableBlockRenderFunc } from "@shared/HFInputs/HFRichInput/TableBlock";
const isRawDraftContent = (value: unknown): value is RawDraftContentState =>
!!value &&
typeof value === "object" &&
Array.isArray((value as RawDraftContentState).blocks);
// Same block-data → class contract as the editor (HFRichInput) — without it,
// published aligned content renders unaligned. See blockStyle.ts.
const blockStyleFn = buildBlockStyleFn();
// Allow only safe schemes plus relative/anchor links. Published content is
// authored by acquirer admins but rendered to public/unauthenticated visitors
// and merchants, so a `javascript:`/`data:` href would be a stored-XSS vector.
const SAFE_URL_SCHEME = /^(https?:|mailto:|tel:)/i;
const sanitizeUrl = (url: unknown): string => {
Iif (typeof url !== "string") return "";
const trimmed = url.trim();
Iif (trimmed.startsWith("/") || trimmed.startsWith("#")) return trimmed;
return SAFE_URL_SCHEME.test(trimmed) ? trimmed : "";
};
// Strip dangerous hrefs out of LINK entities before the content is rendered.
const sanitizeRawContent = (
raw: RawDraftContentState,
): RawDraftContentState => {
Iif (!raw.entityMap) return raw;
const entityMap = Object.fromEntries(
Object.entries(raw.entityMap).map(([key, entity]) => {
if (entity?.type === "LINK" && entity.data) {
const data = entity.data as { url?: unknown; href?: unknown };
const safeUrl = sanitizeUrl(data.url ?? data.href);
return [key, { ...entity, data: { ...entity.data, url: safeUrl } }];
}
return [key, entity];
}),
);
return { ...raw, entityMap };
};
interface DocumentViewerProps {
content: unknown;
}
export default function DocumentViewer({ content }: DocumentViewerProps) {
const editorState = useMemo(() => {
try {
Eif (isRawDraftContent(content)) {
return EditorState.createWithContent(
convertFromRaw(sanitizeRawContent(content)),
);
}
} catch {
// fall through to an empty document
}
return EditorState.createWithContent(ContentState.createFromText(""));
}, [content]);
return (
<Box
sx={{
wordBreak: "break-word",
"& .rdw-paragraph-small, & .rdw-paragraph-small *": {
fontSize: "12px",
},
// The viewer renders both on surfaces that never load
// react-draft-wysiwyg's stylesheet (wrapper rules) and on surfaces
// where it IS loaded and Draft.css beats the inherited wrapper
// alignment (inner-div rules) — see blockStyle.ts.
...alignmentWrapperSx,
...alignmentInnerDivSx,
// The same heading scale the editor renders, so a published document
// matches what its author saw. See blockStyle.ts.
...documentHeadingSx,
// List normalization the editor gets from TextEditor.css (not loaded
// here) — one clean bullet per item, aligned with the text, instead of
// Draft.css's stray native disc marker. See blockStyle.ts.
...listStyleSx,
}}
>
<ReadOnlyEditor
editorState={editorState}
// @ts-ignore blockStyleFn forwards to the underlying draft-js Editor (not in react-draft-wysiwyg-next's EditorProps)
blockStyleFn={blockStyleFn}
// Fee-schedule tables (atomic blocks with a TABLE entity). Cell values
// render as text nodes, so they need no URL-style sanitization.
customBlockRenderFunc={tableBlockRenderFunc}
/>
</Box>
);
}
|