diff --git a/package.json b/package.json index 4f0b0d6c..ae09867d 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "@types/react-copy-to-clipboard": "5.0.2", "@types/react-dom": "17.0.11", "@types/react-table": "7.7.10", - "@types/sanitize-html": "2.6.2", "@types/sort-object-keys": "1.1.0", "@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/parser": "4.33.0", diff --git a/ui/package-lock.json b/ui/package-lock.json index acc1f1db..9bf727f7 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -16,6 +16,7 @@ "clsx": "^1.1.1", "copy-to-clipboard": "3.3.1", "date-fns": "2.28.0", + "dompurify": "^2.4.1", "file-saver": "^2.0.5", "i18next-http-backend": "^1.4.1", "ky": "0.30.0", @@ -28,8 +29,7 @@ "react-scrollbars-custom": "4.1.1", "react-select": "^5.2.2", "react-table": "7.7.0", - "react-to-string": "^0.1.1", - "sanitize-html": "2.7.0" + "react-to-string": "^0.1.1" }, "devDependencies": { "@commitlint/cli": "16.1.0", @@ -47,6 +47,7 @@ "@tanstack/react-query": "^4.14.1", "@testing-library/jest-dom": "5.16.2", "@testing-library/react": "12.1.2", + "@types/dompurify": "^2.4.0", "@types/file-saver": "^2.0.5", "@types/jest": "^27.4.0", "@types/lodash-es": "4.17.6", @@ -58,7 +59,6 @@ "@types/react-router": "5.1.18", "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.10", - "@types/sanitize-html": "2.6.2", "@typescript-eslint/eslint-plugin": "^5.41.0", "@typescript-eslint/parser": "5.11.0", "@viaa/avo2-components": "3.1.1", @@ -105,7 +105,7 @@ "@meemoo/react-components": "2.21.5", "@studiohyperdrive/pagination": "1.0.0", "@tanstack/react-query": "^4.14.1", - "@viaa/avo2-components": "3.1.1", + "@viaa/avo2-components": "3.1.3", "@viaa/avo2-types": "2.46.4", "graphql": "16.6.0", "immer": "9.0.12", @@ -5528,6 +5528,15 @@ "@types/node": "*" } }, + "node_modules/@types/dompurify": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/eslint": { "version": "8.4.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", @@ -5823,15 +5832,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "node_modules/@types/sanitize-html": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.6.2.tgz", - "integrity": "sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ==", - "dev": true, - "dependencies": { - "htmlparser2": "^6.0.0" - } - }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -9677,6 +9677,11 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz", + "integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA==" + }, "node_modules/domutils": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", @@ -13188,6 +13193,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -16761,11 +16767,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" - }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -20482,19 +20483,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/sanitize-html": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.0.tgz", - "integrity": "sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA==", - "dependencies": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^6.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, "node_modules/sanitize.css": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", @@ -27806,6 +27794,15 @@ "@types/node": "*" } }, + "@types/dompurify": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", + "dev": true, + "requires": { + "@types/trusted-types": "*" + } + }, "@types/eslint": { "version": "8.4.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", @@ -28101,15 +28098,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "@types/sanitize-html": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.6.2.tgz", - "integrity": "sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ==", - "dev": true, - "requires": { - "htmlparser2": "^6.0.0" - } - }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -30967,6 +30955,11 @@ "domelementtype": "^2.3.0" } }, + "dompurify": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz", + "integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA==" + }, "domutils": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", @@ -33550,7 +33543,8 @@ "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true }, "is-potential-custom-element-name": { "version": "1.0.1", @@ -36212,11 +36206,6 @@ "lines-and-columns": "^1.1.6" } }, - "parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" - }, "parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -38778,19 +38767,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sanitize-html": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.0.tgz", - "integrity": "sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA==", - "requires": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^6.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, "sanitize.css": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index 0035caf6..dcfef8d9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -33,7 +33,7 @@ "@meemoo/react-components": "2.21.5", "@studiohyperdrive/pagination": "1.0.0", "@tanstack/react-query": "^4.14.1", - "@viaa/avo2-components": "3.1.1", + "@viaa/avo2-components": "3.1.3", "@viaa/avo2-types": "2.46.4", "graphql": "16.6.0", "immer": "9.0.12", @@ -51,6 +51,7 @@ "clsx": "^1.1.1", "copy-to-clipboard": "3.3.1", "date-fns": "2.28.0", + "dompurify": "^2.4.1", "file-saver": "^2.0.5", "i18next-http-backend": "^1.4.1", "ky": "0.30.0", @@ -63,8 +64,7 @@ "react-scrollbars-custom": "4.1.1", "react-select": "^5.2.2", "react-table": "7.7.0", - "react-to-string": "^0.1.1", - "sanitize-html": "2.7.0" + "react-to-string": "^0.1.1" }, "devDependencies": { "@commitlint/cli": "16.1.0", @@ -82,6 +82,7 @@ "@tanstack/react-query": "^4.14.1", "@testing-library/jest-dom": "5.16.2", "@testing-library/react": "12.1.2", + "@types/dompurify": "^2.4.0", "@types/file-saver": "^2.0.5", "@types/jest": "^27.4.0", "@types/lodash-es": "4.17.6", @@ -93,7 +94,6 @@ "@types/react-router": "5.1.18", "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.10", - "@types/sanitize-html": "2.6.2", "@typescript-eslint/eslint-plugin": "^5.41.0", "@typescript-eslint/parser": "5.11.0", "@viaa/avo2-components": "3.1.1", diff --git a/ui/src/react-admin/modules/content-page/services/content-page.service.ts b/ui/src/react-admin/modules/content-page/services/content-page.service.ts index 6078b4ce..b08fb6b9 100644 --- a/ui/src/react-admin/modules/content-page/services/content-page.service.ts +++ b/ui/src/react-admin/modules/content-page/services/content-page.service.ts @@ -3,24 +3,24 @@ import { Avo } from '@viaa/avo2-types'; import { isArray, isFunction, isPlainObject, kebabCase } from 'lodash-es'; import moment from 'moment'; import { stringifyUrl } from 'query-string'; + +import { AdminConfigManager } from '~core/config'; import { ContentPageOverviewParams } from '~modules/content-page/components/wrappers/PageOverviewWrapper/PageOverviewWrapper'; import { PAGES_PER_PAGE } from '~modules/content-page/const/content-page.consts'; +import { RichEditorStateKey } from '~modules/content-page/const/rich-text-editor.consts'; +import { CONTENT_PAGE_SERVICE_BASE_URL } from '~modules/content-page/services/content-page.const'; import { convertContentPageInfoToDbContentPage, convertDbContentPagesToContentPageInfos, - convertDbContentPageToContentPageInfo + convertDbContentPageToContentPageInfo, } from '~modules/content-page/services/content-page.converters'; +import { CustomError } from '~modules/shared/helpers/custom-error'; import { fetchWithLogoutJson } from '../../shared/helpers/fetch-with-logout'; import { mapDeep } from '../../shared/helpers/map-deep/map-deep'; import { sanitizeHtml } from '../../shared/helpers/sanitize'; import { SanitizePreset } from '../../shared/helpers/sanitize/presets'; import { ContentBlockConfig } from '../types/content-block.types'; - -import { AdminConfigManager } from '~core/config'; -import { CustomError } from '~modules/shared/helpers/custom-error'; -import { CONTENT_PAGE_SERVICE_BASE_URL } from '~modules/content-page/services/content-page.const'; -import { RichEditorStateKey } from '~modules/content-page/const/rich-text-editor.consts'; import { ContentOverviewTableCols, ContentPageInfo, @@ -267,10 +267,11 @@ export class ContentPageService { if (value && value.toHTML && isFunction(value.toHTML)) { htmlFromRichTextEditor = value.toHTML(); } - obj[htmlKey] = sanitizeHtml( + const sanitizedHtml = sanitizeHtml( htmlFromRichTextEditor || obj[htmlKey] || '', - 'full' + SanitizePreset.full ); + obj[htmlKey] = sanitizedHtml; } else if (!isPlainObject(value) && !isArray(value)) { obj[key] = value; } else if (isPlainObject(value)) { @@ -391,7 +392,7 @@ export class ContentPageService { public static getDescription( contentPageInfo: ContentPageInfo, - sanitizePreset: SanitizePreset = 'link' + sanitizePreset: SanitizePreset = SanitizePreset.link ): string | null { const description = (contentPageInfo as any).description_state ? (contentPageInfo as any).description_state.toHTML() @@ -407,7 +408,9 @@ export class ContentPageService { try { const dbContentPage = await fetchWithLogoutJson( stringifyUrl({ - url: AdminConfigManager.getConfig().services.getContentPageByPathEndpoint || this.getBaseUrl(), + url: + AdminConfigManager.getConfig().services.getContentPageByPathEndpoint || + this.getBaseUrl(), query: { path, }, @@ -416,9 +419,7 @@ export class ContentPageService { if (!dbContentPage) { return null; } - return convertDbContentPageToContentPageInfo( - dbContentPage - ); + return convertDbContentPageToContentPageInfo(dbContentPage); } catch (err) { throw new CustomError('Failed to get content page by path', err); } diff --git a/ui/src/react-admin/modules/content-page/views/ContentPageDetailMetaData.tsx b/ui/src/react-admin/modules/content-page/views/ContentPageDetailMetaData.tsx index 60f76ce1..a5bed444 100644 --- a/ui/src/react-admin/modules/content-page/views/ContentPageDetailMetaData.tsx +++ b/ui/src/react-admin/modules/content-page/views/ContentPageDetailMetaData.tsx @@ -1,7 +1,3 @@ -import { compact, get } from 'lodash-es'; -import moment from 'moment'; -import React, { FunctionComponent } from 'react'; - import { BlockHeading, Container, @@ -12,6 +8,10 @@ import { TagOption, Thumbnail, } from '@viaa/avo2-components'; +import { compact, get } from 'lodash-es'; +import moment from 'moment'; +import React, { FunctionComponent } from 'react'; + import { GET_CONTENT_PAGE_WIDTH_OPTIONS } from '~modules/content-page/const/content-page.consts'; import { useContentTypes } from '~modules/content-page/hooks/useContentTypes'; import { ContentPageService } from '~modules/content-page/services/content-page.service'; @@ -23,6 +23,7 @@ import { renderDetailRow, renderSimpleDetailRows, } from '~modules/shared/helpers/render-detail-fields'; +import { SanitizePreset } from '~modules/shared/helpers/sanitize/presets'; import { useTranslation } from '~modules/shared/hooks/useTranslation'; import { useUserGroupOptions } from '~modules/user-group/hooks/useUserGroupOptions'; @@ -102,7 +103,7 @@ export const ContentPageDetailMetaData: FunctionComponent @@ -111,7 +112,7 @@ export const ContentPageDetailMetaData: FunctionComponent {tHtml('admin/content/views/content-detail___omschrijving')} - + )} diff --git a/ui/src/react-admin/modules/content-page/views/ContentPageEdit.tsx b/ui/src/react-admin/modules/content-page/views/ContentPageEdit.tsx index e0280c4f..9d15047e 100644 --- a/ui/src/react-admin/modules/content-page/views/ContentPageEdit.tsx +++ b/ui/src/react-admin/modules/content-page/views/ContentPageEdit.tsx @@ -118,7 +118,7 @@ const ContentPageEdit: FC = ({ id, className, renderBack } } if ( !hasPerm(PermissionName.EDIT_ANY_CONTENT_PAGES) && - contentPageObj.owner.id !== getProfileId(user) + contentPageObj.userProfileId !== getProfileId(user) ) { setLoadingInfo({ state: 'error', @@ -579,8 +579,9 @@ const ContentPageEdit: FC = ({ id, className, renderBack } }; const renderEditContentPage = () => { - const contentPageOwnerId = contentPageState.initialContentPageInfo.owner.id; - const isOwner = contentPageOwnerId ? user?.profileId === contentPageOwnerId : true; + const contentPageOwnerId = contentPageState.initialContentPageInfo.userProfileId; + const isOwner = + user?.profileId && contentPageOwnerId && user?.profileId === contentPageOwnerId; const isAllowedToSave = hasPerm(EDIT_ANY_CONTENT_PAGES) || (hasPerm(EDIT_OWN_CONTENT_PAGES) && isOwner); diff --git a/ui/src/react-admin/modules/shared/components/Html/Html.tsx b/ui/src/react-admin/modules/shared/components/Html/Html.tsx index d5e7607c..30f940be 100644 --- a/ui/src/react-admin/modules/shared/components/Html/Html.tsx +++ b/ui/src/react-admin/modules/shared/components/Html/Html.tsx @@ -12,7 +12,7 @@ export interface HtmlProps { const Html: FunctionComponent = ({ content, - sanitizePreset = 'link', + sanitizePreset = SanitizePreset.link, type = 'p', className, }) => { diff --git a/ui/src/react-admin/modules/shared/helpers/map-deep/map-deep.spec.ts b/ui/src/react-admin/modules/shared/helpers/map-deep/map-deep.spec.ts index 6b6579b5..7e349112 100644 --- a/ui/src/react-admin/modules/shared/helpers/map-deep/map-deep.spec.ts +++ b/ui/src/react-admin/modules/shared/helpers/map-deep/map-deep.spec.ts @@ -1,8 +1,9 @@ -import { mapDeep } from './map-deep'; import { isArray, isFunction, isPlainObject } from 'lodash-es'; -import { sanitizeHtml } from '../../../shared/helpers/sanitize'; -import { mockObject1, mockObject2 } from '../../../shared/helpers/map-deep/map-deep.mocks'; +import { SanitizePreset } from '~modules/shared/helpers/sanitize/presets'; import { RichEditorStateKey } from '../../../content-page/const/rich-text-editor.consts'; +import { mockObject1, mockObject2 } from '../../../shared/helpers/map-deep/map-deep.mocks'; +import { sanitizeHtml } from '../../../shared/helpers/sanitize'; +import { mapDeep } from './map-deep'; describe('map-deep', () => { it('Should correctly map all levels of an object', () => { @@ -48,7 +49,7 @@ describe('map-deep', () => { } obj[htmlKey] = sanitizeHtml( htmlFromRichTextEditor || obj[htmlKey] || '', - 'full' + SanitizePreset.full ); } else if (!isPlainObject(value) && !isArray(value)) { obj[key] = value; diff --git a/ui/src/react-admin/modules/shared/helpers/render-detail-fields.tsx b/ui/src/react-admin/modules/shared/helpers/render-detail-fields.tsx index 2fcf9eb8..dfd0659c 100644 --- a/ui/src/react-admin/modules/shared/helpers/render-detail-fields.tsx +++ b/ui/src/react-admin/modules/shared/helpers/render-detail-fields.tsx @@ -1,8 +1,9 @@ import { get, isBoolean, isNil, isString } from 'lodash-es'; import React, { ReactElement, ReactNode } from 'react'; +import { SanitizePreset } from '~modules/shared/helpers/sanitize/presets'; import { formatDate } from './formatters/date'; -import { sanitizeHtml, sanitizePresets } from './sanitize'; +import { sanitizeHtml } from './sanitize'; export function renderDetailRow(value: ReactNode, label: string): ReactElement { return ( @@ -10,7 +11,7 @@ export function renderDetailRow(value: ReactNode, label: string): ReactElement { {label} {isString(value) && ( )} {!isString(value) && {value}} diff --git a/ui/src/react-admin/modules/shared/helpers/sanitize/index.ts b/ui/src/react-admin/modules/shared/helpers/sanitize/index.ts index c03d3ac2..65ce2789 100644 --- a/ui/src/react-admin/modules/shared/helpers/sanitize/index.ts +++ b/ui/src/react-admin/modules/shared/helpers/sanitize/index.ts @@ -1,8 +1,10 @@ -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; import sanitizePresets, { SanitizePreset } from './presets'; -const sanitizeHtml = (input: string, preset: SanitizePreset): string => - sanitize(input, sanitizePresets[preset] || sanitizePresets['basic']); +const sanitizeHtml = (input: string, preset: SanitizePreset): string => { + const presetConfig = sanitizePresets[preset]; + return sanitize(input, presetConfig) as string; +}; export { sanitizeHtml, sanitizePresets }; diff --git a/ui/src/react-admin/modules/shared/helpers/sanitize/presets/index.ts b/ui/src/react-admin/modules/shared/helpers/sanitize/presets/index.ts index 2eba972f..732e3f6e 100644 --- a/ui/src/react-admin/modules/shared/helpers/sanitize/presets/index.ts +++ b/ui/src/react-admin/modules/shared/helpers/sanitize/presets/index.ts @@ -1,5 +1,7 @@ -const basic = { - allowedTags: [ +import DOMPurify from 'dompurify'; + +export const basic: DOMPurify.Config = { + ALLOWED_TAGS: [ 'h1', 'h2', 'h3', @@ -23,46 +25,32 @@ const basic = { 'super', 'span', ], - allowedAttributes: { - span: ['style'], - p: ['style'], - }, + RETURN_DOM: false, + ADD_ATTR: ['target'], // Allow target _blank for links }; -const link = { - allowedTags: [...basic.allowedTags, 'a'], - allowedAttributes: { - ...basic.allowedAttributes, - a: ['href', 'target'], - }, +export const link: DOMPurify.Config = { + ALLOWED_TAGS: [...(basic.ALLOWED_TAGS || []), 'a'], + RETURN_DOM: false, + ADD_ATTR: ['target'], // Allow target _blank for links }; -const full = { - allowedTags: [...link.allowedTags, 'img', 'table', 'tr', 'td', 'div'], - allowedAttributes: { - ...link.allowedAttributes, - table: ['class'], - td: ['colSpan', 'rowSpan'], - img: ['src'], - div: ['class', 'style'], - span: ['class', 'style'], - h1: ['class', 'style'], - h2: ['class', 'style'], - h3: ['class', 'style'], - h4: ['class', 'style'], - h5: ['class', 'style'], - h6: ['class', 'style'], - }, +export const full: DOMPurify.Config = { + ALLOWED_TAGS: [...(link.ALLOWED_TAGS || []), 'img', 'table', 'tr', 'td', 'div'], + RETURN_DOM: false, + ADD_ATTR: ['target'], // Allow target _blank for links }; -export type SanitizePreset = 'basic' | 'link' | 'full'; +export enum SanitizePreset { + basic = 'basic', + link = 'link', + full = 'full', +} -/* eslint-disable @typescript-eslint/no-unused-vars */ -const presetLookup: { [preset in SanitizePreset]: any } = { - basic, - link, - full, +const presetLookup: Record = { + [SanitizePreset.basic]: basic, + [SanitizePreset.link]: link, + [SanitizePreset.full]: full, }; -/* eslint-enable @typescript-eslint/no-unused-vars */ export default presetLookup; diff --git a/ui/src/react-admin/modules/shared/helpers/sanitize/sanitize.spec.ts b/ui/src/react-admin/modules/shared/helpers/sanitize/sanitize.spec.ts new file mode 100644 index 00000000..ad4ce96c --- /dev/null +++ b/ui/src/react-admin/modules/shared/helpers/sanitize/sanitize.spec.ts @@ -0,0 +1,23 @@ +import sanitize from 'sanitize-html'; +import { SanitizePreset } from './presets'; +import { sanitizeHtml } from './index'; + +describe('sanitize', () => { + it('Should not remove p style attribute and also leave link alone', () => { + const originalHtml = + '

Simpel! Drie stappen naar een geslaagde opdracht:

'; + const sanitizedHtml = sanitizeHtml(originalHtml, SanitizePreset.full); + expect(sanitizedHtml).toEqual( + '

Simpel! Drie stappen naar een geslaagde opdracht:

' + ); + }); + + it('Should remove link', () => { + const originalHtml = + '

Simpel! Drie stappen naar een geslaagde opdracht:

'; + const sanitizedHtml = sanitizeHtml(originalHtml, SanitizePreset.basic); + expect(sanitizedHtml).toEqual( + '

Simpel! Drie stappen naar een geslaagde opdracht:

' + ); + }); +}); diff --git a/ui/src/react-admin/modules/shared/styles/components/_content.scss b/ui/src/react-admin/modules/shared/styles/components/_content.scss index a6ba438c..95472e04 100644 --- a/ui/src/react-admin/modules/shared/styles/components/_content.scss +++ b/ui/src/react-admin/modules/shared/styles/components/_content.scss @@ -8,7 +8,7 @@ Usage: use these classes to style individual components like their rich text equivalent ========================================================================== */ -@import "./braft-editor-output"; +@import './braft-editor-output'; .c-h1-display, .c-content .c-h1-display { @@ -108,11 +108,12 @@ } .c-content { - white-space: initial; + white-space: normal; + overflow: auto; // Otherwise margins on c-content div collapse on https://onderwijs-qas.hetarchief.be/leerlingen p { margin: 1.5rem 0; - min-height: 1em // Braft editor output css + min-height: 1em; // Braft editor output css } a { @@ -121,7 +122,7 @@ dl { border: 1px solid $color-gray-100; - border-radius: .3rem; + border-radius: 0.3rem; overflow: hidden; padding: 1.2rem; margin: 1.5rem 0; @@ -151,11 +152,13 @@ list-style: decimal; } - ul, ol { + ul, + ol { padding: 0; margin: 1.5rem 0 1.5rem 1.75rem; - ul, ol { + ul, + ol { margin: 0; padding-left: 2.25rem; } @@ -222,16 +225,17 @@ font-weight: normal; line-height: 16px; word-wrap: break-word; - white-space: pre-wrap + white-space: pre-wrap; } - pre, code { + pre, + code { font-family: $g-code-font-family; } pre pre { margin: 0; - padding: 0 + padding: 0; } code { @@ -262,7 +266,6 @@ color: $color-gray-100; } - // Overline plus paragraph .c-overline-plus-p { margin: 1.5rem 0; diff --git a/ui/src/react-admin/modules/translations/views/TranslationsOverview.tsx b/ui/src/react-admin/modules/translations/views/TranslationsOverview.tsx index 478fe97a..8d08fe9e 100644 --- a/ui/src/react-admin/modules/translations/views/TranslationsOverview.tsx +++ b/ui/src/react-admin/modules/translations/views/TranslationsOverview.tsx @@ -1,4 +1,4 @@ -import { Container, Flex, Pagination as PaginationAvo, Spinner } from '@viaa/avo2-components'; +import { Pagination as PaginationAvo } from '@viaa/avo2-components'; import { orderBy } from 'lodash-es'; import React, { FunctionComponent, @@ -387,7 +387,7 @@ const TranslationsOverview: FunctionComponent = ({ }; if (!translations) { - return ; + return ; } return (
diff --git a/ui/src/react-admin/modules/user-group/views/UserGroupOverview.tsx b/ui/src/react-admin/modules/user-group/views/UserGroupOverview.tsx index f0768fcd..a4a3e8a6 100644 --- a/ui/src/react-admin/modules/user-group/views/UserGroupOverview.tsx +++ b/ui/src/react-admin/modules/user-group/views/UserGroupOverview.tsx @@ -1,7 +1,15 @@ import { keysEnter, onKey, Table, TextInput } from '@meemoo/react-components'; -import React, { ChangeEvent, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import React, { + ChangeEvent, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react'; import { cloneDeep, remove, sortBy } from 'lodash-es'; -import { Column, TableOptions } from 'react-table'; +import { Column, TableOptions, UseSortByColumnOptions } from 'react-table'; import { PermissionData } from '~modules/permissions/permissions.types'; import { CenteredSpinner } from '~modules/shared/components/Spinner/CenteredSpinner'; @@ -18,7 +26,7 @@ import { UserGroupOverviewProps, UserGroupOverviewRef, UserGroupUpdate, - UserGroupWithPermissions + UserGroupWithPermissions, } from '../types/user-group.types'; const UserGroupOverview = forwardRef( @@ -33,13 +41,13 @@ const UserGroupOverview = forwardRef { - if (!userGroupId || !permissionId || !currentUserGroups || !permissions) { - return; - } + // Add updated permission to changelog + const updateUserGroupUpdates = useCallback( + (userGroupId: string, permissionId: string | number, hasPermission: boolean) => { + if (!userGroupId || !permissionId) { + return; + } - const userGroups = cloneDeep(currentUserGroups); - const userGroup = userGroups.find((group) => String(group.id) === String(userGroupId)); + const newUpdates = cloneDeep(userGroupUpdates); + const currentUpdate = newUpdates?.find( + (update) => + update.permissionId === permissionId && update.userGroupId === userGroupId + ); + if (currentUpdate) { + newUpdates.splice(newUpdates.indexOf(currentUpdate), 1); + } + newUpdates.push({ userGroupId, permissionId, hasPermission }); + setUserGroupUpdates(newUpdates); - if (!userGroup) { - return; - } + // Fire onChange for parent component + onChangePermissions?.(!!newUpdates.length); + }, + [onChangePermissions, userGroupUpdates] + ); - // Filter out permission (if present) - const removed = remove( - userGroup.permissions, - (permission) => permission.id === permissionId - ); + const updateUserGroup = useCallback( + (userGroupId: string, permissionId: string | number, hasPermission: boolean) => { + if (!userGroupId || !permissionId || !currentUserGroups || !permissions) { + return; + } - if (removed.length) { - // Permission was removed - setCurrentUserGroups(userGroups); - } else { - // Permission was not present - const newPermission = permissions.find( - (permission) => permission.id === permissionId + const userGroups = cloneDeep(currentUserGroups); + const userGroup = userGroups.find( + (group) => String(group.id) === String(userGroupId) ); - newPermission && userGroup.permissions.push(newPermission); - setCurrentUserGroups(userGroups); - } - // Update changelog - updateUserGroupUpdates(userGroupId, permissionId, hasPermission); - }; + if (!userGroup) { + return; + } - // Add updated permission to changelog - const updateUserGroupUpdates = ( - userGroupId: string, - permissionId: string | number, - hasPermission: boolean - ) => { - if (!userGroupId || !permissionId) { - return; - } + // Filter out permission (if present) + const removed = remove( + userGroup.permissions, + (permission) => permission.id === permissionId + ); - const newUpdates = cloneDeep(userGroupUpdates); - const currentUpdate = newUpdates?.find( - (update) => - update.permissionId === permissionId && update.userGroupId === userGroupId - ); - if (currentUpdate) { - newUpdates.splice(newUpdates.indexOf(currentUpdate), 1); - } - newUpdates.push({ userGroupId, permissionId, hasPermission }); - setUserGroupUpdates(newUpdates); + if (removed.length) { + // Permission was removed + setCurrentUserGroups(userGroups); + } else { + // Permission was not present + const newPermission = permissions.find( + (permission) => permission.id === permissionId + ); + if (newPermission) { + userGroup.permissions = userGroup.permissions || []; + userGroup.permissions.push(newPermission); + } + setCurrentUserGroups(userGroups); + } - // Fire onChange for parent component - onChangePermissions?.(!!newUpdates.length); - }; + // Update changelog + updateUserGroupUpdates(userGroupId, permissionId, hasPermission); + }, + [currentUserGroups, permissions, updateUserGroupUpdates] + ); const onClickCancel = () => { setCurrentUserGroups(cloneDeep(userGroups)); @@ -142,12 +153,12 @@ const UserGroupOverview = forwardRef ({ onCancel: onClickCancel, onSave: onClickSave, - onSearch: onSearchSubmit + onSearch: onSearchSubmit, })); /** @@ -209,7 +220,7 @@ const UserGroupOverview = forwardRef & + UseSortByColumnOptions)[] => { + if (!currentUserGroups) { + return []; + } + return UserGroupTableColumns(currentUserGroups, updateUserGroup); + }, [currentUserGroups, updateUserGroup]); + const renderUserGroupOverview = () => { if (!currentUserGroups) { return null; } - console.log({ - userGroups, - currentUserGroups, - permissions - }); return (
[], + columns, data: searchResults || permissions || [], initialState: { - pageSize: permissions?.length - } + pageSize: permissions?.length, + }, } as TableOptions /* eslint-enable @typescript-eslint/ban-types */ } diff --git a/ui/src/react-admin/modules/user/user.consts.ts b/ui/src/react-admin/modules/user/user.consts.ts index e27c0318..3221ab0a 100644 --- a/ui/src/react-admin/modules/user/user.consts.ts +++ b/ui/src/react-admin/modules/user/user.consts.ts @@ -13,7 +13,6 @@ import { NULL_FILTER } from '~modules/shared/helpers/filters'; import { isAvo } from '~modules/shared/helpers/is-avo'; import { normalizeTimestamp } from '~modules/shared/helpers/formatters/date'; import { PermissionService } from '~modules/shared/services/permission-service'; -import { DatabaseType } from '@viaa/avo2-types'; import { CommonUser, UserBulkAction, UserOverviewTableCol } from './user.types'; type UserBulkActionOption = SelectOption & { @@ -483,12 +482,14 @@ function getError(rule: ValidationRule, object: T) { return rule.error(object); } -const GET_TEMP_ACCESS_VALIDATION_RULES_FOR_SAVE: (i18n: I18n) => ValidationRule< - Partial ->[] = (i18n: I18n) => [ +const GET_TEMP_ACCESS_VALIDATION_RULES_FOR_SAVE: ( + i18n: I18n +) => ValidationRule>[] = (i18n: I18n) => [ { // until cannot be null and must be in the future - error: i18n.tText('admin/users/user___de-einddatum-is-verplicht-en-moet-in-de-toekomst-liggen'), + error: i18n.tText( + 'admin/users/user___de-einddatum-is-verplicht-en-moet-in-de-toekomst-liggen' + ), isValid: (tempAccess: Partial) => !!tempAccess.until && normalizeTimestamp(tempAccess.until).isAfter(), }, @@ -506,9 +507,12 @@ const GET_TEMP_ACCESS_VALIDATION_RULES_FOR_SAVE: (i18n: I18n) => ValidationRule< }, ]; -export const getTempAccessValidationErrors = (tempAccess: Avo.User.TempAccess, i18n: I18n): string[] => { +export const getTempAccessValidationErrors = ( + tempAccess: Avo.User.TempAccess, + i18n: I18n +): string[] => { const validationErrors = [...GET_TEMP_ACCESS_VALIDATION_RULES_FOR_SAVE(i18n)].map((rule) => { return rule.isValid(tempAccess) ? null : getError(rule, tempAccess); }); return compact(validationErrors); -}; \ No newline at end of file +};