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 && }
- {!!(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 && (
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) => {