Skip to content

Commit b65d0c3

Browse files
authored
Merge pull request #1155 from cultuurnet/feature/III-6955-more-dompurify-sanitization
III-6955: more dompurify sanitization
2 parents 61c57cc + 855189b commit b65d0c3

File tree

5 files changed

+109
-51
lines changed

5 files changed

+109
-51
lines changed

src/pages/TranslateForm.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,22 @@ import { FormElement } from '@/ui/FormElement';
1818
import { Inline } from '@/ui/Inline';
1919
import { Input } from '@/ui/Input';
2020
import { Page } from '@/ui/Page';
21+
import { Panel } from '@/ui/Panel';
2122
import { Stack } from '@/ui/Stack';
2223
import { Text } from '@/ui/Text';
2324
import { getGlobalBorderRadius, getValueFromTheme } from '@/ui/theme';
2425
import { Title } from '@/ui/Title';
2526
import { Toast } from '@/ui/Toast';
27+
import { sanitizationPresets, sanitizeDom } from '@/utils/sanitizeDom';
28+
29+
import { DescriptionPreview } from './events/[eventId]/preview/DescriptionPreview';
2630

2731
const htmlToDraft =
2832
typeof window === 'object' && require('html-to-draftjs').default;
2933

3034
const languageOptions = [...Object.values(SupportedLanguages), 'en'];
3135
const getGlobalValue = getValueFromTheme('global');
36+
const getTextValue = getValueFromTheme('text');
3237

3338
const TranslateForm = () => {
3439
const { t } = useTranslation();
@@ -88,7 +93,10 @@ const TranslateForm = () => {
8893
const newEditorStates: Record<string, EditorState> = {};
8994

9095
languageOptions.forEach((langValue) => {
91-
const description = offer.description?.[langValue];
96+
const description = sanitizeDom(
97+
offer.description?.[langValue],
98+
sanitizationPresets.EVENT_DESCRIPTION,
99+
);
92100

93101
if (description) {
94102
const draftState = htmlToDraft(description);
@@ -316,21 +324,30 @@ const TranslateForm = () => {
316324
</Text>
317325
{originalLanguage === language &&
318326
!isEditingOriginalDescription ? (
319-
<Inline>
320-
<Text variant="muted">
321-
{descriptionEditorStates[language]
322-
? descriptionEditorStates[language]
323-
.getCurrentContent()
324-
.getPlainText()
325-
: ''}
326-
</Text>
327+
<Stack spacing={3}>
328+
<Panel padding={3} color={getTextValue('muted.color')}>
329+
<DescriptionPreview
330+
description={
331+
descriptionEditorStates[language]
332+
? draftToHtml(
333+
convertToRaw(
334+
descriptionEditorStates[
335+
language
336+
].getCurrentContent(),
337+
),
338+
)
339+
: ''
340+
}
341+
/>
342+
</Panel>
343+
327344
<Button
328345
variant={ButtonVariants.LINK}
329346
onClick={toggleEditOriginalDescription}
330347
>
331348
{t('translate.change')}
332349
</Button>
333-
</Inline>
350+
</Stack>
334351
) : (
335352
<div id={`description-editor-container-${language}`}>
336353
<FormElement
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Text } from '@/ui/Text';
2+
import { getValueFromTheme } from '@/ui/theme';
3+
import { sanitizationPresets, sanitizeDom } from '@/utils/sanitizeDom';
4+
5+
type Props = {
6+
description: string;
7+
};
8+
9+
const DescriptionPreview = ({ description }: Props) => {
10+
const getLinkThemeValue = getValueFromTheme('link');
11+
12+
return (
13+
<Text
14+
css={`
15+
p {
16+
margin: 7.5px 0;
17+
}
18+
a {
19+
color: ${getLinkThemeValue('color')};
20+
text-decoration: underline;
21+
&:hover {
22+
color: ${getLinkThemeValue('hoverColor')};
23+
}
24+
}
25+
em {
26+
font-style: italic;
27+
}
28+
strong {
29+
font-weight: bold;
30+
}
31+
ul,
32+
ol {
33+
margin: 7.5px 0 7.5px 20px;
34+
}
35+
ul {
36+
list-style-type: disc;
37+
}
38+
ol {
39+
list-style-type: decimal;
40+
}
41+
`}
42+
dangerouslySetInnerHTML={{
43+
__html: sanitizeDom(description, sanitizationPresets.EVENT_DESCRIPTION),
44+
}}
45+
/>
46+
);
47+
};
48+
49+
export { DescriptionPreview };

src/pages/events/[eventId]/preview/Preview.tsx

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import DOMPurify from 'isomorphic-dompurify';
21
import { useRouter } from 'next/router';
32
import { useState } from 'react';
43
import { useTranslation } from 'react-i18next';
@@ -27,6 +26,7 @@ import { parseOfferId } from '@/utils/parseOfferId';
2726

2827
import { BookingInfoPreview } from './BookingInfoPreview';
2928
import { ContactInfoPreview } from './ContactInfoPreview';
29+
import { DescriptionPreview } from './DescriptionPreview';
3030
import { LocationPreview } from './LocationPreview';
3131

3232
const getGlobalValue = getValueFromTheme('global');
@@ -37,7 +37,6 @@ const Preview = () => {
3737
const router = useRouter();
3838
const { t } = useTranslation();
3939
const { eventId } = router.query;
40-
const getLinkThemeValue = getValueFromTheme('link');
4140
const [activeTab, setActiveTab] = useState(() => {
4241
if (typeof window !== 'undefined') {
4342
const hash = window.location.hash.replace('#', '');
@@ -285,43 +284,7 @@ const Preview = () => {
285284
{
286285
field: t('preview.labels.description'),
287286
value: description ? (
288-
<Text
289-
css={`
290-
p {
291-
margin: 7.5px 0;
292-
}
293-
a {
294-
color: ${getLinkThemeValue('color')};
295-
text-decoration: underline;
296-
&:hover {
297-
color: ${getLinkThemeValue('hoverColor')};
298-
}
299-
}
300-
ul {
301-
list-style-type: disc;
302-
margin: 7.5px 0 7.5px 20px;
303-
}
304-
ol {
305-
list-style-type: decimal;
306-
margin: 7.5px 0 7.5px 20px;
307-
}
308-
`}
309-
dangerouslySetInnerHTML={{
310-
__html: DOMPurify.sanitize(description, {
311-
ALLOWED_TAGS: [
312-
'ul',
313-
'ol',
314-
'li',
315-
'span',
316-
'p',
317-
'em',
318-
'strong',
319-
'a',
320-
],
321-
ALLOWED_ATTR: ['style', 'href'],
322-
}),
323-
}}
324-
/>
287+
<DescriptionPreview description={description} />
325288
) : (
326289
<EmptyValue>{t('preview.empty_value.description')}</EmptyValue>
327290
),

src/pages/steps/AdditionalInformationStep/DescriptionStep.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ProgressBar, ProgressBarVariants } from '@/ui/ProgressBar';
2020
import { getStackProps, Stack, StackProps } from '@/ui/Stack';
2121
import { Text, TextVariants } from '@/ui/Text';
2222
import { Breakpoints } from '@/ui/theme';
23+
import { sanitizationPresets, sanitizeDom } from '@/utils/sanitizeDom';
2324

2425
import { TabContentProps, ValidationStatus } from './AdditionalInformationStep';
2526

@@ -122,9 +123,13 @@ const DescriptionStep = ({
122123

123124
useEffect(() => {
124125
const newDescription = entity?.description?.[i18n.language];
125-
if (!newDescription) return;
126+
const sanitizedDescription = sanitizeDom(
127+
newDescription,
128+
sanitizationPresets.EVENT_DESCRIPTION,
129+
);
130+
if (!sanitizedDescription) return;
126131

127-
const draftState = htmlToDraft(newDescription);
132+
const draftState = htmlToDraft(sanitizedDescription);
128133
const contentState = ContentState.createFromBlockArray(
129134
draftState.contentBlocks,
130135
draftState.entityMap,

src/utils/sanitizeDom.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import DOMPurify from 'isomorphic-dompurify';
2+
3+
const sanitizationPresets = {
4+
EVENT_DESCRIPTION: 'eventDescription',
5+
} as const;
6+
7+
const sanitizationOptions = {
8+
[sanitizationPresets.EVENT_DESCRIPTION]: {
9+
ALLOWED_TAGS: ['ul', 'ol', 'li', 'span', 'p', 'em', 'strong', 'a'],
10+
ALLOWED_ATTR: ['style', 'href'],
11+
},
12+
};
13+
14+
type SanitizationPreset =
15+
(typeof sanitizationPresets)[keyof typeof sanitizationPresets];
16+
17+
const sanitizeDom = (
18+
html: string,
19+
preset: SanitizationPreset = sanitizationPresets.EVENT_DESCRIPTION,
20+
): string => {
21+
return DOMPurify.sanitize(html, sanitizationOptions[preset]);
22+
};
23+
24+
export { sanitizationPresets, sanitizeDom };

0 commit comments

Comments
 (0)