diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index 0a41be1b8c..71e3bd9d23 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -45,6 +45,7 @@ "classnames": "^2.3.2", "core-js": "^3.31.0", "date-fns": "^2.30.0", + "dompurify": "^3.0.6", "dotenv": "^16.3.1", "formik": "^2.4.2", "graphql": "^16.8.1", @@ -116,6 +117,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", + "@types/dompurify": "^3.0.5", "@types/draft-js": "^0.11.12", "@types/jest": "^27.5.2", "@types/jest-axe": "^3.5.5", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index f51f8eb03c..7221962afb 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -137,6 +137,9 @@ dependencies: date-fns: specifier: ^2.30.0 version: 2.30.0 + dompurify: + specifier: ^3.0.6 + version: 3.0.6 dotenv: specifier: ^16.3.1 version: 16.3.1 @@ -346,6 +349,9 @@ devDependencies: '@testing-library/user-event': specifier: ^14.4.3 version: 14.4.3(@testing-library/dom@8.20.1) + '@types/dompurify': + specifier: ^3.0.5 + version: 3.0.5 '@types/draft-js': specifier: ^0.11.12 version: 0.11.12 @@ -4641,7 +4647,7 @@ packages: '@babel/runtime': 7.23.2 '@emotion/is-prop-valid': 1.2.1 '@mui/types': 7.2.9(@types/react@18.2.20) - '@mui/utils': 5.14.5(react@18.2.0) + '@mui/utils': 5.14.18(@types/react@18.2.20)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.20 clsx: 2.0.0 @@ -7333,6 +7339,12 @@ packages: resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==} dev: true + /@types/dompurify@3.0.5: + resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} + dependencies: + '@types/trusted-types': 2.0.6 + dev: true + /@types/draft-js@0.11.12: resolution: {integrity: sha512-J/e4QYz8wCXvPpiCaiKcJrtLo65px4nnfFVZ/0EKHoKnQ4nWdzXwGHOQLIePAJM+Ho4V9/mb4Nhw4v/08y98jQ==} dependencies: @@ -9643,7 +9655,7 @@ packages: dev: false /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} /concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -10497,6 +10509,10 @@ packages: dev: false optional: true + /dompurify@3.0.6: + resolution: {integrity: sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==} + dev: false + /domutils@1.7.0: resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} dependencies: @@ -13665,7 +13681,7 @@ packages: resolution: {integrity: sha512-Qczi5xnTNjkhcIB0Yy75Txt+Ez51xdhOxsukN7awzq2auZQGPHcQrJ623PZj0ECDEMOk2soxWx05EXdXGd1CbA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: - chalk: 4.1.0 + chalk: 4.1.2 jest-diff: 27.5.1 jest-get-type: 27.5.1 pretty-format: 27.5.1 diff --git a/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Public.tsx b/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Public.tsx index e11537ca0e..7d87ddb32d 100644 --- a/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Public.tsx +++ b/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Public.tsx @@ -307,7 +307,7 @@ const InteractiveFileListItem = (props: FileListItemProps) => { aria-haspopup="dialog" size="small" > - Help + Info )} setOpen(false)}> diff --git a/editor.planx.uk/src/@planx/components/Notice/Public.tsx b/editor.planx.uk/src/@planx/components/Notice/Public.tsx index b7a1dc6195..8117dcf3c9 100644 --- a/editor.planx.uk/src/@planx/components/Notice/Public.tsx +++ b/editor.planx.uk/src/@planx/components/Notice/Public.tsx @@ -91,10 +91,7 @@ const NoticeComponent: React.FC = (props) => { policyRef={props.policyRef} howMeasured={props.howMeasured} /> - + diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/Card.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/Card.tsx index 363c820661..bcf9dfe52b 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/Card.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/Card.tsx @@ -24,6 +24,7 @@ export const contentFlowSpacing = (theme: Theme): React.CSSProperties => ({ const InnerContainer = styled(Box)(({ theme }) => ({ maxWidth: "100%", + position: "relative", "& > * + *": { ...contentFlowSpacing(theme), }, diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/QuestionHeader.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/QuestionHeader.tsx index 0f656c63cc..fe5d974714 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/QuestionHeader.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/QuestionHeader.tsx @@ -11,7 +11,7 @@ import { DESCRIPTION_TEXT } from "../constants"; import MoreInfo from "./MoreInfo"; import MoreInfoSection from "./MoreInfoSection"; -const HelpButtonMinWidth = "75px"; +const HelpButtonMinWidth = "70px"; interface IQuestionHeader { title?: string; @@ -30,7 +30,7 @@ const Description = styled(Box)(({ theme }) => ({ const TitleWrapper = styled(Box)(({ theme }) => ({ width: theme.breakpoints.values.formWrap, - maxWidth: `calc(100% - ${HelpButtonMinWidth})`, + maxWidth: `calc(100% - (${HelpButtonMinWidth} + 4px))`, [theme.breakpoints.up("contentWrap")]: { maxWidth: "100%", }, @@ -45,21 +45,20 @@ const HelpButtonWrapper = styled(Box)(({ theme }) => ({ display: "flex", justifyContent: "stretch", width: HelpButtonMinWidth, - top: theme.spacing(6), - right: 0, - [theme.breakpoints.up("sm")]: { - top: theme.spacing(6.5), - }, + top: "-4px", + right: "-6px", + pointerEvents: "none", [theme.breakpoints.up("md")]: { - top: theme.spacing(8.5), - width: "110px", + width: "80px", + top: 0, + right: 0, }, [theme.breakpoints.up("lg")]: { - width: "140px", + width: "100px", }, "#embedded-browser &": { + top: "-60px", width: "80px", - top: theme.spacing(13), }, })); @@ -68,20 +67,16 @@ export const HelpButton = styled(Button)(({ theme }) => ({ position: "sticky", right: 0, minHeight: "44px", - padding: "0.35em 1em", + padding: "0.35em 0.5em", alignSelf: "flex-start", - borderRadius: "50px 0 0 50px", minWidth: "100%", boxShadow: "none", - backgroundColor: theme.palette.text.primary, fontSize: "1.125em", - filter: "drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.5))", - [theme.breakpoints.up("md")]: { - padding: "0.35em 1em 0.35em 0.5em", - }, + filter: "drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.5))", + pointerEvents: "auto", [theme.breakpoints.up("lg")]: { minHeight: "48px", - fontSize: "1.375em", + fontSize: "1.25em", top: theme.spacing(1), }, "#embedded-browser &": { @@ -112,7 +107,7 @@ const QuestionHeader: React.FC = ({ return ( <> - + {title && ( = ({ )} + {!!(info || policyRef || howMeasured) && ( + + + Help + + + )} setOpen(false)}> {info && info !== emptyContent ? ( @@ -160,21 +171,6 @@ const QuestionHeader: React.FC = ({ {img && question} - {!!(info || policyRef || howMeasured) && ( - - - Help - - - )} ); }; diff --git a/editor.planx.uk/src/components/Header.tsx b/editor.planx.uk/src/components/Header.tsx index 5a2fd1aba7..8e112c4ebf 100644 --- a/editor.planx.uk/src/components/Header.tsx +++ b/editor.planx.uk/src/components/Header.tsx @@ -249,7 +249,7 @@ const Breadcrumbs: React.FC = () => { )} {route.data.flow && ( <> - {useStore.getState().canUserEditTeam(route.data.team) ? ( + {useStore.getState().canUserEditTeam(team.slug) ? ( ) : ( @@ -400,9 +400,10 @@ const EditorToolbar: React.FC<{ }> = ({ headerRef, route }) => { const { navigate } = useNavigation(); const [open, setOpen] = useState(false); - const [togglePreview, user] = useStore((state) => [ + const [togglePreview, user, team] = useStore((state) => [ state.togglePreview, state.getUser(), + state.getTeam(), ]); const handleClose = () => { @@ -506,7 +507,7 @@ const EditorToolbar: React.FC<{ )} {/* Only show global settings link from top-level admin view */} - {!route.data.flow && !route.data.team && ( + {!route.data.flow && !team.slug && ( navigate("/global-settings")}> Global Settings diff --git a/editor.planx.uk/src/pages/Preview/ResumePage.tsx b/editor.planx.uk/src/pages/Preview/ResumePage.tsx index 370d0501a6..6edda24386 100644 --- a/editor.planx.uk/src/pages/Preview/ResumePage.tsx +++ b/editor.planx.uk/src/pages/Preview/ResumePage.tsx @@ -16,6 +16,7 @@ import { ApplicationPath, SendEmailPayload } from "types"; import Input from "ui/Input"; import InputLabel from "ui/InputLabel"; import InputRow from "ui/InputRow"; +import { removeSessionIdSearchParamWithoutReloading } from "utils"; import { object, string } from "yup"; import ReconciliationPage from "./ReconciliationPage"; @@ -215,7 +216,14 @@ const ResumePage: React.FC = () => { getInitialEmailValue(route.url.query.email), ); const [paymentRequest, setPaymentRequest] = useState(); - const sessionId = useCurrentRoute().url.query.sessionId; + + // Read the sessionId from the url to validate against + const sessionId = route.url.query.sessionId; + + // As the sessionId has been extracted it can now be removed to avoid + // unnecessarily exposing it + removeSessionIdSearchParamWithoutReloading(); + const [reconciliationResponse, setReconciliationResponse] = useState(); diff --git a/editor.planx.uk/src/pages/Preview/SaveAndReturn.test.tsx b/editor.planx.uk/src/pages/Preview/SaveAndReturn.test.tsx index 87a154a4c6..6c3e5bd5ac 100644 --- a/editor.planx.uk/src/pages/Preview/SaveAndReturn.test.tsx +++ b/editor.planx.uk/src/pages/Preview/SaveAndReturn.test.tsx @@ -67,7 +67,7 @@ describe("Save and Return component", () => { expect(results).toHaveNoViolations(); }); - it("stores the sessionId as part of the URL once an email has been submitted", async () => { + it("does not store the sessionId as part of the URL once an email has been submitted", async () => { const children = ; const { user } = setup(); @@ -89,7 +89,7 @@ describe("Save and Return component", () => { expect(screen.getByText("Testing 123")).toBeInTheDocument(); }); - expect(window.location.href).toContain(`sessionId=${sessionId}`); + expect(window.location.href).not.toContain(`sessionId=${sessionId}`); }); }); diff --git a/editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx b/editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx index 431c52d5a1..eaab65c3a4 100644 --- a/editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx +++ b/editor.planx.uk/src/pages/Preview/SaveAndReturn.tsx @@ -84,20 +84,10 @@ const SaveAndReturn: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const isEmailCaptured = Boolean(useStore((state) => state.saveToEmail)); - const sessionId = useStore((state) => state.sessionId); const isContentPage = useCurrentRoute()?.data?.isContentPage; - // Setting the URL search param "sessionId" will route the user to ApplicationPath.Resume - // Without this the user will need to click the magic link in their email after a refresh - const allowResumeOnBrowserRefresh = () => { - const url = new URL(window.location.href); - url.searchParams.set("sessionId", sessionId); - window.history.pushState({}, document.title, url); - }; - const handleSubmit = (email: string) => { useStore.setState({ saveToEmail: email }); - allowResumeOnBrowserRefresh(); }; return ( diff --git a/editor.planx.uk/src/theme.ts b/editor.planx.uk/src/theme.ts index 6f45b54611..097259b5c7 100644 --- a/editor.planx.uk/src/theme.ts +++ b/editor.planx.uk/src/theme.ts @@ -258,9 +258,13 @@ const getThemeOptions = (primaryColor: string): ThemeOptions => { }, outlined: { borderWidth: "2px 2px 3px", - borderColor: "currentcolor", + borderColor: palette.primary.main, + color: palette.text.primary, + backgroundColor: palette.common.white, "&:hover": { borderWidth: "2px 2px 3px", + backgroundColor: palette.primary.dark, + color: palette.common.white, }, }, }, diff --git a/editor.planx.uk/src/ui/ReactMarkdownOrHtml.tsx b/editor.planx.uk/src/ui/ReactMarkdownOrHtml.tsx index edba86f90d..fa104e4391 100644 --- a/editor.planx.uk/src/ui/ReactMarkdownOrHtml.tsx +++ b/editor.planx.uk/src/ui/ReactMarkdownOrHtml.tsx @@ -1,5 +1,6 @@ import Box from "@mui/material/Box"; import { styled, Theme } from "@mui/material/styles"; +import DOMPurify from "dompurify"; import React from "react"; import ReactMarkdown from "react-markdown"; import { FONT_WEIGHT_SEMI_BOLD, linkStyle } from "theme"; @@ -56,7 +57,7 @@ export default function ReactMarkdownOrHtml(props: { return ( ); diff --git a/editor.planx.uk/src/utils.ts b/editor.planx.uk/src/utils.ts index 754cc1902f..edf1aa191c 100644 --- a/editor.planx.uk/src/utils.ts +++ b/editor.planx.uk/src/utils.ts @@ -62,3 +62,9 @@ export const removeSessionIdSearchParam = () => { window.history.pushState({}, document.title, currentURL); window.location.reload(); }; + +export const removeSessionIdSearchParamWithoutReloading = () => { + const currentURL = new URL(window.location.href); + currentURL.searchParams.delete("sessionId"); + window.history.replaceState({}, document.title, currentURL); +}; diff --git a/sharedb.planx.uk/package.json b/sharedb.planx.uk/package.json index 98c64ac37f..21ee0ff6d1 100644 --- a/sharedb.planx.uk/package.json +++ b/sharedb.planx.uk/package.json @@ -4,9 +4,11 @@ "private": true, "dependencies": { "@teamwork/websocket-json-stream": "^2.0.0", + "dompurify": "^3.0.6", + "jsdom": "^23.0.0", "jsonwebtoken": "^8.5.1", "pg": "^8.11.3", - "sharedb": "^3.3.1", + "sharedb": "^4.1.1", "ws": "^8.14.2" }, "scripts": { diff --git a/sharedb.planx.uk/pnpm-lock.yaml b/sharedb.planx.uk/pnpm-lock.yaml index c437ee6d45..56b44669ef 100644 --- a/sharedb.planx.uk/pnpm-lock.yaml +++ b/sharedb.planx.uk/pnpm-lock.yaml @@ -13,6 +13,12 @@ dependencies: '@teamwork/websocket-json-stream': specifier: ^2.0.0 version: 2.0.0 + dompurify: + specifier: ^3.0.6 + version: 3.0.6 + jsdom: + specifier: ^23.0.0 + version: 23.0.0 jsonwebtoken: specifier: '>=9.0.0' version: 9.0.1 @@ -20,8 +26,8 @@ dependencies: specifier: ^8.11.3 version: 8.11.3 sharedb: - specifier: ^3.3.1 - version: 3.3.1 + specifier: ^4.1.1 + version: 4.1.1 ws: specifier: ^8.14.2 version: 8.14.2 @@ -37,14 +43,25 @@ packages: resolution: {integrity: sha512-SCEM44hjNyxYwrtyJrjlHmeTd9RJlZr04BAMbHSBhdW0M2IXv0SC+4XeuRXPiY7U7pJ0W8TSUwVP/28MV/ds0w==} dev: false + /agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /arraydiff@0.1.3: resolution: {integrity: sha512-t0OgO06uolEcMUvV8+yHc9Pc9pazh8wi/Dtyok/sQwvcr8iFV+P86IfAzK7upUDhI4oavhVREMY7iSWtm38LeA==} dev: false - /async@2.6.4: - resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} - dependencies: - lodash: 4.17.21 + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false /buffer-equal-constant-time@1.0.1: @@ -56,6 +73,28 @@ packages: engines: {node: '>=4'} dev: false + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /cssstyle@3.0.0: + resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} + engines: {node: '>=14'} + dependencies: + rrweb-cssom: 0.6.0 + dev: false + + /data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + dev: false + /dateformat@3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} dev: true @@ -64,6 +103,31 @@ packages: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} dev: true + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /dompurify@3.0.6: + resolution: {integrity: sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==} + dev: false + /dynamic-dedupe@0.3.0: resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} dependencies: @@ -76,8 +140,13 @@ packages: safe-buffer: 5.2.1 dev: false - /fast-deep-equal@2.0.1: - resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: false /filewatcher@3.0.1: @@ -86,6 +155,15 @@ packages: debounce: 1.2.1 dev: true + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} dev: true @@ -110,6 +188,40 @@ packages: resolution: {integrity: sha512-zpImx2GoKXy42fVDSEad2BPKuSQdLcqsCYa48K3zHSzM/ugWuYjLDr8IXxpVuL7uCLHw56eaiLxCRthhOzf5ug==} dev: false + /html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + dependencies: + whatwg-encoding: 3.1.1 + dev: false + + /http-proxy-agent@7.0.0: + resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /https-proxy-agent@7.0.2: + resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /is-core-module@2.12.1: resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} dependencies: @@ -122,6 +234,10 @@ packages: hasBin: true dev: true + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -133,6 +249,42 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /jsdom@23.0.0: + resolution: {integrity: sha512-cbL/UCtohJguhFC7c2/hgW6BeZCNvP7URQGnx9tSJRYKCdnfbfWOrtuLTMfiB2VxKsx5wPHVsh/J0aBy9lIIhQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + cssstyle: 3.0.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.2 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.14.2 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /jsonwebtoken@9.0.1: resolution: {integrity: sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==} engines: {node: '>=12', npm: '>=6'} @@ -168,10 +320,26 @@ packages: dependencies: yallist: 4.0.0 + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: false @@ -202,6 +370,10 @@ packages: which: 2.0.2 dev: true + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: false + /ot-json0@1.1.0: resolution: {integrity: sha512-wf5fci7GGpMYRDnbbdIFQymvhsbFACMHtxjivQo5KgvAHlxekyfJ9aPsRr6YfFQthQkk4bmsl5yESrZwC/oMYQ==} dev: false @@ -210,6 +382,12 @@ packages: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} dev: false + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: false + /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true @@ -300,6 +478,23 @@ packages: xtend: 4.0.2 dev: false + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: false + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: false + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + /resolve@1.22.2: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} hasBin: true @@ -309,10 +504,25 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + dev: false + /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: false + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /semver@7.5.3: resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} engines: {node: '>=10'} @@ -320,12 +530,12 @@ packages: dependencies: lru-cache: 6.0.0 - /sharedb@3.3.1: - resolution: {integrity: sha512-gPLKUFZX7FsrZ4AonyWtoslYbT9d+82yttH4dvsFTwwh4tNx8L7hXW9oKTWfqx3APhk0VApAN64ObT969cvCTA==} + /sharedb@4.1.1: + resolution: {integrity: sha512-BeRQkAFQ65pRgo9k9rFsUL2CecOdSpSUBaAIU/8qT4TnMjJLz/t1RcbrgoeVvCgWqYoPdW3b1rB33WLGgzlGaQ==} dependencies: arraydiff: 0.1.3 - async: 2.6.4 - fast-deep-equal: 2.0.1 + async: 3.2.5 + fast-deep-equal: 3.1.3 hat: 0.0.3 ot-json0: 1.1.0 dev: false @@ -344,11 +554,76 @@ packages: engines: {node: '>= 0.4'} dev: true + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + + /tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + dependencies: + punycode: 2.3.1 + dev: false + + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true dev: true + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: false + + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: false + + /whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + dependencies: + iconv-lite: 0.6.3 + dev: false + + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + dev: false + + /whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -370,6 +645,15 @@ packages: optional: true dev: false + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: false + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/sharedb.planx.uk/server.js b/sharedb.planx.uk/server.js index 438252922d..1a13948a02 100644 --- a/sharedb.planx.uk/server.js +++ b/sharedb.planx.uk/server.js @@ -4,6 +4,8 @@ const jwt = require("jsonwebtoken"); const ShareDB = require("sharedb"); const WebSocketJSONStream = require("@teamwork/websocket-json-stream"); const PostgresDB = require("./sharedb-postgresql"); +const createDOMPurify = require('dompurify'); +const { JSDOM } = require('jsdom'); const { PORT = 8000, JWT_SECRET, PG_URL } = process.env; assert(JWT_SECRET); @@ -16,6 +18,10 @@ const sharedb = new ShareDB({ }), }); +// Setup JSDOM and DOMPurify +const window = new JSDOM("").window; +const DOMPurify = createDOMPurify(window); + // Register middleware hooks // Get userId from request on initial connection, register to agent @@ -34,12 +40,41 @@ sharedb.use("commit", (context, done) => { try { const { op, agent } = context; op.m.uId = agent.connectSession.userId; + op.op = op.op.map(sanitiseOperation); } catch (e) { console.error("Error committing to ShareDB: ", e); }; done(); }); +/** + * @description Sanitise operations which insert or update nodes + */ +function sanitiseOperation(op) { + const isInsertOrUpdate = "oi" in op; + if (isInsertOrUpdate) { + op.oi = sanitise(op.oi); + }; + return op; +} + +/** + * @description Recursively traverse updated data in order to find string values, and then sanitise these by calling DOMPurify. Input could be an entire node, or a single property of a node, depending on the operation. + */ +function sanitise(input) { + if ((input && typeof input === "string") || input instanceof String) { + return DOMPurify.sanitize(input); + } else if ((input && typeof input === "object") || input instanceof Object) { + return Object.entries(input).reduce((acc, [k, v]) => { + v = sanitise(v); + acc[k] = v; + return acc; + }, input); + } else { + return input; + } +} + const wss = new Server({ port: PORT, verifyClient: (info, cb) => {