All files / src/pages/AcquirerPortal/LegalDocuments/LegalDocumentDetail/components DocumentViewer.tsx

85.18% Statements 23/27
71.42% Branches 15/21
100% Functions 6/6
95.83% Lines 23/24

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