diff --git a/example/application/container.tsx b/example/application/container.tsx index 05e302c..fda302a 100644 --- a/example/application/container.tsx +++ b/example/application/container.tsx @@ -223,7 +223,7 @@ const Viewer: React.FunctionComponent = ({ document }): React.React `} > {isSvg ? ( - + ) : ( { global.fetch = jest.fn().mockResolvedValue(mockResponse); const svgRef = React.createRef(); - const { findByTitle } = render( - - ); + const { findByTitle } = render(); const iFrame = await findByTitle("Svg Renderer Frame"); const srcdocContent = (iFrame as HTMLIFrameElement).srcdoc; @@ -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(); - const svgUrl = v4WithOnlyTamperedEmbeddedSvg.renderMethod.id; - const { findByTitle } = render( - - ); + const { findByTitle } = render(); const iFrame = await findByTitle("Svg Renderer Frame"); const srcdocContent = (iFrame as HTMLIFrameElement).srcdoc; diff --git a/src/components/renderer/SvgRenderer.tsx b/src/components/renderer/SvgRenderer.tsx index b71b18b..6507198 100644 --- a/src/components/renderer/SvgRenderer.tsx +++ b/src/components/renderer/SvgRenderer.tsx @@ -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"; @@ -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, @@ -27,111 +23,84 @@ enum DisplayResult { } const SvgRenderer = React.forwardRef( - ({ document, svgData, style, className, sandbox = "allow-same-origin", onConnected, forceV2 = false }, ref) => { - const [buffer, setBuffer] = useState(); + ({ document, style, className, onResult }, ref) => { const [svgFetchedData, setFetchedSvgData] = useState(""); const [source, setSource] = useState(""); const [toDisplay, setToDisplay] = useState(DisplayResult.OK); const svgRef = useRef(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))}`; @@ -148,14 +117,14 @@ const SvgRenderer = React.forwardRef( ${renderToStaticMarkup( - <>{svgFetchedData ? SVG document image : <>} + <>{svgFetchedData !== "" ? SVG document image : <>} )} `; switch (toDisplay) { case DisplayResult.DEFAULT: - return null} />; + return null} />; case DisplayResult.CONNECTION_ERROR: return ; case DisplayResult.DIGEST_ERROR: @@ -169,7 +138,6 @@ const SvgRenderer = React.forwardRef( width="100%" srcDoc={iframeContent} ref={svgRef} - sandbox={sandbox} /> ); default: