diff --git a/api.planx.uk/modules/flows/findReplace/controller.ts b/api.planx.uk/modules/flows/findReplace/controller.ts index 73d7802a42..c64387dc64 100644 --- a/api.planx.uk/modules/flows/findReplace/controller.ts +++ b/api.planx.uk/modules/flows/findReplace/controller.ts @@ -4,12 +4,6 @@ import { z } from "zod"; import { ServerError } from "../../../errors"; import { findAndReplaceInFlow } from "./service"; import { FlowGraph } from "@opensystemslab/planx-core/types"; -import { JSDOM } from "jsdom"; -import createDOMPurify from "dompurify"; - -// Setup JSDOM and DOMPurify -const window = new JSDOM("").window; -const DOMPurify = createDOMPurify(window); interface FindAndReplaceResponse { message: string; @@ -23,12 +17,7 @@ export const findAndReplaceSchema = z.object({ }), query: z.object({ find: z.string(), - replace: z - .string() - .optional() - .transform( - (val) => val && DOMPurify.sanitize(val, { ADD_ATTR: ["target"] }), - ), + replace: z.string().optional(), }), }); diff --git a/api.planx.uk/modules/flows/findReplace/findReplace.test.ts b/api.planx.uk/modules/flows/findReplace/findReplace.test.ts index 1a3ee89927..962fc88744 100644 --- a/api.planx.uk/modules/flows/findReplace/findReplace.test.ts +++ b/api.planx.uk/modules/flows/findReplace/findReplace.test.ts @@ -265,279 +265,3 @@ describe("string replacement", () => { }); }); }); - -describe("HTML replacement", () => { - const originalHTML = ``; - const mockFlowData: Flow["data"] = { - _root: { - edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], - }, - "6dwuQp5xjA": { - data: { - text: "No", - }, - type: 200, - }, - "8AWcYxZgBw": { - data: { - fn: "property.constraints.planning", - text: "Is it monument inside a portal?", - }, - type: 100, - edges: ["AJnWX6O1xt", "6dwuQp5xjA"], - }, - AJnWX6O1xt: { - data: { - val: "designated.monument", - text: "Yes", - description: originalHTML, - }, - type: 200, - }, - Hfh8KuSzUq: { - data: { - val: "designated.monument", - text: "Yes", - description: originalHTML, - }, - type: 200, - }, - RRQwM2zAgy: { - data: { - fn: "property.constraints.planning", - text: "Is it a monument", - }, - type: 100, - edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], - }, - ft26KlH7Oy: { - data: { - text: "No", - }, - type: 200, - }, - vcTgmVQAre: { - data: { - text: "internal-portal-test", - }, - type: 300, - edges: ["8AWcYxZgBw"], - }, - QsEdip17H5: { - type: 310, - data: { - flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", - }, - }, - }; - - beforeEach(() => { - queryMock.mockQuery({ - name: "GetFlowData", - matchOnVariables: false, - data: { - flow: { - data: mockFlowData, - slug: "test", - }, - }, - }); - }); - - describe("Replacing unsafe HTML", () => { - const unsafeHTML = ""; - const safeHTML = ''; - - const replacedFlowData: Flow["data"] = { - _root: { - edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], - }, - "6dwuQp5xjA": { - data: { - text: "No", - }, - type: 200, - }, - "8AWcYxZgBw": { - data: { - fn: "property.constraints.planning", - text: "Is it monument inside a portal?", - }, - type: 100, - edges: ["AJnWX6O1xt", "6dwuQp5xjA"], - }, - AJnWX6O1xt: { - data: { - val: "monument", - text: "Yes", - description: safeHTML, - }, - type: 200, - }, - Hfh8KuSzUq: { - data: { - val: "monument", - text: "Yes", - description: safeHTML, - }, - type: 200, - }, - RRQwM2zAgy: { - data: { - fn: "property.constraints.planning", - text: "Is it a monument", - }, - type: 100, - edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], - }, - ft26KlH7Oy: { - data: { - text: "No", - }, - type: 200, - }, - vcTgmVQAre: { - data: { - text: "internal-portal-test", - }, - type: 300, - edges: ["8AWcYxZgBw"], - }, - QsEdip17H5: { - type: 310, - data: { - flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", - }, - }, - }; - - beforeEach(() => { - queryMock.mockQuery({ - name: "UpdateFlow", - matchOnVariables: false, - data: { - flow: { - data: replacedFlowData, - slug: "test", - }, - }, - }); - }); - - it("sanitises unsafe replace values", async () => { - await supertest(app) - .post(`/flows/2/search?find=${originalHTML}&replace=${unsafeHTML}`) - .set(auth) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ - message: - 'Found 2 matches of "" and replaced with ""', - matches: { - AJnWX6O1xt: { - data: { - description: '', - }, - }, - Hfh8KuSzUq: { - data: { - description: '', - }, - }, - }, - updatedFlow: replacedFlowData, - }); - }); - }); - }); - - describe("Replacing safe HTML", () => { - const replacementHTML = ``; - const replacedFlowData: Flow["data"] = { - _root: { - edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], - }, - "6dwuQp5xjA": { - data: { - text: "No", - }, - type: 200, - }, - "8AWcYxZgBw": { - data: { - fn: "property.constraints.planning", - text: "Is it monument inside a portal?", - }, - type: 100, - edges: ["AJnWX6O1xt", "6dwuQp5xjA"], - }, - AJnWX6O1xt: { - data: { - val: "monument", - text: "Yes", - description: replacementHTML, - }, - type: 200, - }, - Hfh8KuSzUq: { - data: { - val: "monument", - text: "Yes", - description: replacementHTML, - }, - type: 200, - }, - RRQwM2zAgy: { - data: { - fn: "property.constraints.planning", - text: "Is it a monument", - }, - type: 100, - edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], - }, - ft26KlH7Oy: { - data: { - text: "No", - }, - type: 200, - }, - vcTgmVQAre: { - data: { - text: "internal-portal-test", - }, - type: 300, - edges: ["8AWcYxZgBw"], - }, - QsEdip17H5: { - type: 310, - data: { - flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", - }, - }, - }; - - beforeEach(() => { - queryMock.mockQuery({ - name: "UpdateFlow", - matchOnVariables: false, - data: { - flow: { - data: replacedFlowData, - slug: "test", - }, - }, - }); - }); - - it("does not remove the 'target' attribute from anchors", async () => { - await supertest(app) - .post(`/flows/2/search?find=${originalHTML}&replace=${replacementHTML}`) - .set(auth) - .expect(200) - .then((res) => { - // Checking substring as DOMPurify rearranges attribute order in a non-deterministic manner - expect(res.body.message).toMatch(/target=["]\\?_blank\\?["]/); - }); - }); - }); -});