From be1afc7778f4ee9026cb641d331270a6d8de3a95 Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Wed, 10 Apr 2024 15:47:51 +0200 Subject: [PATCH] fix: Use linkifyjs instead, add noreferrer --- client/package-lock.json | 22 ++++-- client/package.json | 3 +- client/src/components/Card/Tasks.jsx | 4 +- client/src/components/Card/Tasks.module.scss | 3 +- .../src/components/CardModal/Tasks/Item.jsx | 33 +++------ client/src/components/Linkify.jsx | 68 +++++++++++++++++++ 6 files changed, 100 insertions(+), 33 deletions(-) create mode 100644 client/src/components/Linkify.jsx diff --git a/client/package-lock.json b/client/package-lock.json index 2e4ebf8..b1e6907 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,7 +10,6 @@ "classnames": "^2.3.2", "date-fns": "^2.30.0", "dequal": "^2.0.3", - "dompurify": "^3.1.0", "easymde": "^2.18.0", "history": "^5.3.0", "i18next": "^23.7.6", @@ -18,6 +17,8 @@ "initials": "^3.1.2", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", + "linkify-react": "^4.1.3", + "linkifyjs": "^4.1.3", "lodash": "^4.17.21", "nanoid": "^5.0.3", "node-sass": "^9.0.0", @@ -7017,11 +7018,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dompurify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.0.tgz", - "integrity": "sha512-yoU4rhgPKCo+p5UrWWWNKiIq+ToGqmVVhk0PmMYBK4kRsR3/qhemNFL8f6CFmBd4gMwm3F4T7HBoydP5uY07fA==" - }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -11917,6 +11913,20 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-react": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz", + "integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==", + "peerDependencies": { + "linkifyjs": "^4.0.0", + "react": ">= 15.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", + "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==" + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", diff --git a/client/package.json b/client/package.json index 7fb0164..3f3c4e1 100755 --- a/client/package.json +++ b/client/package.json @@ -63,7 +63,6 @@ "classnames": "^2.3.2", "date-fns": "^2.30.0", "dequal": "^2.0.3", - "dompurify": "^3.1.0", "easymde": "^2.18.0", "history": "^5.3.0", "i18next": "^23.7.6", @@ -71,6 +70,8 @@ "initials": "^3.1.2", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", + "linkify-react": "^4.1.3", + "linkifyjs": "^4.1.3", "lodash": "^4.17.21", "nanoid": "^5.0.3", "node-sass": "^9.0.0", diff --git a/client/src/components/Card/Tasks.jsx b/client/src/components/Card/Tasks.jsx index d76340f..c530474 100644 --- a/client/src/components/Card/Tasks.jsx +++ b/client/src/components/Card/Tasks.jsx @@ -4,6 +4,8 @@ import classNames from 'classnames'; import { Progress } from 'semantic-ui-react'; import { useToggle } from '../../lib/hooks'; +import Linkify from '../Linkify'; + import styles from './Tasks.module.scss'; const Tasks = React.memo(({ items }) => { @@ -48,7 +50,7 @@ const Tasks = React.memo(({ items }) => { key={item.id} className={classNames(styles.task, item.isCompleted && styles.taskCompleted)} > - {item.name} + {item.name} ))} diff --git a/client/src/components/Card/Tasks.module.scss b/client/src/components/Card/Tasks.module.scss index b0af9fe..8f8a7ec 100644 --- a/client/src/components/Card/Tasks.module.scss +++ b/client/src/components/Card/Tasks.module.scss @@ -55,9 +55,10 @@ display: block; font-size: 12px; line-height: 14px; + overflow: hidden; padding-bottom: 6px; padding-left: 14px; - overflow: hidden; + text-overflow: ellipsis; &:before { content: "–"; diff --git a/client/src/components/CardModal/Tasks/Item.jsx b/client/src/components/CardModal/Tasks/Item.jsx index ecaa059..8bb3046 100755 --- a/client/src/components/CardModal/Tasks/Item.jsx +++ b/client/src/components/CardModal/Tasks/Item.jsx @@ -4,11 +4,11 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Draggable } from 'react-beautiful-dnd'; import { Button, Checkbox, Icon } from 'semantic-ui-react'; -import { sanitize } from 'dompurify'; import { usePopup } from '../../../lib/popup'; import NameEdit from './NameEdit'; import ActionsStep from './ActionsStep'; +import Linkify from '../../Linkify'; import styles from './Item.module.scss'; @@ -16,14 +16,11 @@ const Item = React.memo( ({ id, index, name, isCompleted, isPersisted, canEdit, onUpdate, onDelete }) => { const nameEdit = useRef(null); - const handleClick = useCallback( - (event) => { - if (!event.target.closest('a') && isPersisted && canEdit) { - nameEdit.current.open(); - } - }, - [isPersisted, canEdit], - ); + const handleClick = useCallback(() => { + if (isPersisted && canEdit) { + nameEdit.current.open(); + } + }, [isPersisted, canEdit]); const handleNameUpdate = useCallback( (newName) => { @@ -44,16 +41,6 @@ const Item = React.memo( nameEdit.current.open(); }, []); - const parseLinks = (text) => { - const regex = /(http[s]?:\/\/[^\s]+)/g; - return sanitize(text).replace(regex, (match) => { - const url = new URL(match); - return `${ - url.hostname === window.location.hostname ? url.pathname : match - }`; - }); - }; - const ActionsPopup = usePopup(ActionsStep); return ( @@ -78,11 +65,9 @@ const Item = React.memo( className={classNames(styles.text, canEdit && styles.textEditable)} onClick={handleClick} > - + + {name} + {isPersisted && canEdit && ( diff --git a/client/src/components/Linkify.jsx b/client/src/components/Linkify.jsx new file mode 100644 index 0000000..71b01fd --- /dev/null +++ b/client/src/components/Linkify.jsx @@ -0,0 +1,68 @@ +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +import LinkifyReact from 'linkify-react'; + +import history from '../history'; + +const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => { + const handleLinkClick = useCallback( + (event) => { + if (linkStopPropagation) { + event.stopPropagation(); + } + + if (!event.target.getAttribute('target')) { + event.preventDefault(); + history.push(event.target.href); + } + }, + [linkStopPropagation], + ); + + const linkRenderer = useCallback( + ({ attributes: { href, ...linkProps }, content }) => { + let url; + try { + url = new URL(href, window.location); + } catch (error) {} // eslint-disable-line no-empty + + const isSameSite = !!url && url.origin === window.location.origin; + + return ( + + {isSameSite ? url.pathname : content} + + ); + }, + [handleLinkClick], + ); + + return ( + + {children} + + ); +}); + +Linkify.propTypes = { + children: PropTypes.string.isRequired, + linkStopPropagation: PropTypes.bool, +}; + +Linkify.defaultProps = { + linkStopPropagation: false, +}; + +export default Linkify;