Skip to content

Commit

Permalink
fix: clean up props and change onConnected to onResult
Browse files Browse the repository at this point in the history
  • Loading branch information
yapyuyou committed Apr 17, 2024
1 parent 542f8f0 commit 44a60d7
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 92 deletions.
2 changes: 1 addition & 1 deletion example/application/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ const Viewer: React.FunctionComponent<ViewerProps> = ({ document }): React.React
`}
>
{isSvg ? (
<SvgRenderer document={document.document} svgRef={svgRef} forceV2={true} />
<SvgRenderer document={document.document} ref={svgRef} />
) : (
<FrameConnector
source={document.frameSource}
Expand Down
9 changes: 2 additions & 7 deletions src/components/renderer/SvgRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ describe("svgRenderer component", () => {
global.fetch = jest.fn().mockResolvedValue(mockResponse);
const svgRef = React.createRef<HTMLIFrameElement>();

const { findByTitle } = render(
<SvgRenderer document={v2WithSvgUrlAndDigestMultibase} ref={svgRef} forceV2={true} />
);
const { findByTitle } = render(<SvgRenderer document={v2WithSvgUrlAndDigestMultibase} ref={svgRef} />);

const iFrame = await findByTitle("Svg Renderer Frame");
const srcdocContent = (iFrame as HTMLIFrameElement).srcdoc;
Expand Down Expand Up @@ -86,11 +84,8 @@ describe("svgRenderer component", () => {
it("should render v4 doc with modified SVG when no digestMultibase", async () => {
global.fetch = jest.fn().mockResolvedValue(tamperedMockResponse);
const svgRef = React.createRef<HTMLIFrameElement>();
const svgUrl = v4WithOnlyTamperedEmbeddedSvg.renderMethod.id;

const { findByTitle } = render(
<SvgRenderer svgData={svgUrl} document={v4WithOnlyTamperedEmbeddedSvg} ref={svgRef} />
);
const { findByTitle } = render(<SvgRenderer document={v4WithOnlyTamperedEmbeddedSvg} ref={svgRef} />);

const iFrame = await findByTitle("Svg Renderer Frame");
const srcdocContent = (iFrame as HTMLIFrameElement).srcdoc;
Expand Down
136 changes: 52 additions & 84 deletions src/components/renderer/SvgRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { v2, utils } from "@govtechsg/open-attestation";
import React, { CSSProperties, useEffect, useImperativeHandle, useRef, useState } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { Sha256 } from "@aws-crypto/sha256-browser";
Expand All @@ -9,15 +8,12 @@ const handlebars = require("handlebars");

interface SvgRendererProps {
document: any; // TODO: Update to OpenAttestationDocument
svgData?: string;
style?: CSSProperties;
className?: string;
sandbox?: string;
onConnected?: () => void; // Optional call method to call once svg is loaded
forceV2?: boolean;
onResult?: (result: DisplayResult) => void;
}

const EMBEDDED_DOCUMENT = "[Embedded SVG]";
const EMBEDDED_IN_DOCUMENT = "[Embedded SVG]";

enum DisplayResult {
OK = 0,
Expand All @@ -27,111 +23,84 @@ enum DisplayResult {
}

const SvgRenderer = React.forwardRef<HTMLIFrameElement, SvgRendererProps>(
({ document, svgData, style, className, sandbox = "allow-same-origin", onConnected, forceV2 = false }, ref) => {
const [buffer, setBuffer] = useState<ArrayBuffer>();
({ document, style, className, onResult }, ref) => {
const [svgFetchedData, setFetchedSvgData] = useState<string>("");
const [source, setSource] = useState<string>("");
const [toDisplay, setToDisplay] = useState<DisplayResult>(DisplayResult.OK);
const svgRef = useRef<HTMLIFrameElement>(null);
useImperativeHandle(ref, () => svgRef.current as HTMLIFrameElement);

let docAsAny: any;
if (forceV2 && utils.isRawV2Document(docAsAny)) {
docAsAny = document as v2.OpenAttestationDocument;
} else {
docAsAny = document as any; // TODO: update type to v4.OpenAttestationDocument
}
const fetchSvg = async (svgInDoc: string) => {
try {
const response = await fetch(svgInDoc);
if (!response.ok) {
throw new Error("Failed to fetch remote SVG");
}
const blob = await response.blob();
const res = await blob.arrayBuffer();
return res;
} catch (error) {
setSvgDataAndTriggerCallback(DisplayResult.CONNECTION_ERROR);
}
};

// Step 1: Fetch svg data if needed
useEffect(() => {
if (!("renderMethod" in docAsAny)) {
setToDisplay(DisplayResult.DEFAULT);
if (!("renderMethod" in document)) {
setSvgDataAndTriggerCallback(DisplayResult.DEFAULT);
return;
}

const svgInDoc = docAsAny.renderMethod.id;
const svgInDoc = document.renderMethod.id;
const urlPattern = /^https?:\/\/.*\.svg$/;
const isSvgUrl = urlPattern.test(svgInDoc);

if (svgData) {
// Case 1: Svg data is pre-fetched and passed as a prop
const textEncoder = new TextEncoder();
const svgArrayBuffer = textEncoder.encode(svgData).buffer;
setBuffer(svgArrayBuffer);
setSource(isSvgUrl ? svgInDoc : EMBEDDED_DOCUMENT); // In case svg data is passed over despite being embedded
} else if (isSvgUrl) {
// Case 2: Fetch svg data from url in document
const fetchSvg = async () => {
try {
const response = await fetch(svgInDoc);
if (!response.ok) {
throw new Error("Failed to fetch remote SVG");
}
const blob = await response.blob();
setBuffer(await blob.arrayBuffer());
} catch (error) {
setToDisplay(DisplayResult.CONNECTION_ERROR);
if (isSvgUrl) {
fetchSvg(svgInDoc).then((buffer) => {
if (!buffer) return;

const digestMultibaseInDoc = document.renderMethod.digestMultibase;
const svgUint8Array = new Uint8Array(buffer ?? []);
const decoder = new TextDecoder();
const svgText = decoder.decode(svgUint8Array);

if (digestMultibaseInDoc) {
const hash = new Sha256();
hash.update(svgUint8Array);
hash.digest().then((shaDigest) => {
const recomputedDigestMultibase = "z" + bs58.encode(shaDigest); // manually prefix with 'z' as per https://w3c-ccg.github.io/multibase/#mh-registry
if (recomputedDigestMultibase === digestMultibaseInDoc) {
setSvgDataAndTriggerCallback(DisplayResult.OK, svgText);
} else {
setSvgDataAndTriggerCallback(DisplayResult.DIGEST_ERROR);
}
});
} else {
setSvgDataAndTriggerCallback(DisplayResult.OK, svgText);
}
};
fetchSvg();
});
setSource(svgInDoc);
} else {
// Case 3: Display embedded svg data directly from document
setSvgDataAndTriggerCallback(svgInDoc);
setSource(EMBEDDED_DOCUMENT);
setSvgDataAndTriggerCallback(DisplayResult.OK, svgInDoc);
setSource(EMBEDDED_IN_DOCUMENT);
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [document]);

const setSvgDataAndTriggerCallback = (svgToSet: string) => {
const setSvgDataAndTriggerCallback = (result: DisplayResult, svgToSet = "") => {
setFetchedSvgData(svgToSet);
setToDisplay(DisplayResult.OK);
setToDisplay(result);
setTimeout(() => {
updateIframeHeight();
if (typeof onConnected === "function") {
onConnected();
if (typeof onResult === "function") {
onResult(result);
}
}, 200); // wait for 200ms before manually updating the height
};

// Step 2: Recompute and compare the digestMultibase if present, if not proceed to use the svg template
useEffect(() => {
if (!buffer) return;

const digestMultibaseInDoc = docAsAny.renderMethod.digestMultibase;
const svgUint8Array = new Uint8Array(buffer ?? []);
const decoder = new TextDecoder();
const text = decoder.decode(svgUint8Array);

if (digestMultibaseInDoc) {
const hash = new Sha256();
hash.update(svgUint8Array);
hash.digest().then((shaDigest) => {
const recomputedDigestMultibase = "z" + bs58.encode(shaDigest); // manually prefix with 'z' as per https://w3c-ccg.github.io/multibase/#mh-registry
if (recomputedDigestMultibase === digestMultibaseInDoc) {
setSvgDataAndTriggerCallback(text);
} else {
setToDisplay(DisplayResult.DIGEST_ERROR);
}
});
} else {
setSvgDataAndTriggerCallback(text);
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [buffer]);

// Step 3: Compile final svg
const renderTemplate = (template: string, document: any) => {
if (template.length === 0) return "";
if (forceV2 && utils.isRawV2Document(document)) {
const v2doc = document as v2.OpenAttestationDocument;
const compiledTemplate = handlebars.compile(template);
return compiledTemplate(v2doc);
} else {
const v4doc = document;
const compiledTemplate = handlebars.compile(template);
return compiledTemplate(v4doc.credentialSubject);
}
const compiledTemplate = handlebars.compile(template);
return document.credentialSubject ? compiledTemplate(document.credentialSubject) : compiledTemplate(document);
};

const compiledSvgData = `data:image/svg+xml,${encodeURIComponent(renderTemplate(svgFetchedData, document))}`;
Expand All @@ -148,14 +117,14 @@ const SvgRenderer = React.forwardRef<HTMLIFrameElement, SvgRendererProps>(
<head></head>
<body style="margin: 0; display: flex; justify-content: center; align-items: center;">
${renderToStaticMarkup(
<>{svgFetchedData ? <img src={compiledSvgData} alt="SVG document image" /> : <></>}</>
<>{svgFetchedData !== "" ? <img src={compiledSvgData} alt="SVG document image" /> : <></>}</>
)}
</body>
</html>`;

switch (toDisplay) {
case DisplayResult.DEFAULT:
return <NoTemplate document={docAsAny} handleObfuscation={() => null} />;
return <NoTemplate document={document} handleObfuscation={() => null} />;
case DisplayResult.CONNECTION_ERROR:
return <ConnectionFailureTemplate document={document} source={source} />;
case DisplayResult.DIGEST_ERROR:
Expand All @@ -169,7 +138,6 @@ const SvgRenderer = React.forwardRef<HTMLIFrameElement, SvgRendererProps>(
width="100%"
srcDoc={iframeContent}
ref={svgRef}
sandbox={sandbox}
/>
);
default:
Expand Down

0 comments on commit 44a60d7

Please sign in to comment.