diff --git a/package-lock.json b/package-lock.json index 9f7cc5101..4d1f15573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10142,6 +10142,11 @@ "version": "1.4.1", "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz", + "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==" + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -18296,13 +18301,6 @@ "url": "https://opencollective.com/preact" } }, - "node_modules/preact-markup": { - "version": "2.1.1", - "license": "MIT", - "peerDependencies": { - "preact": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -22149,6 +22147,7 @@ "big.js": "^6.2.1", "classnames": "^2.3.1", "didi": "^10.0.1", + "dompurify": "^3.0.8", "feelers": "^1.2.0", "feelin": "^3.0.0", "flatpickr": "^4.6.13", @@ -22156,7 +22155,6 @@ "lodash": "^4.5.0", "min-dash": "^4.0.0", "preact": "^10.5.14", - "preact-markup": "^2.1.1", "showdown": "^2.1.0" } }, @@ -23722,6 +23720,7 @@ "big.js": "^6.2.1", "classnames": "^2.3.1", "didi": "^10.0.1", + "dompurify": "^3.0.8", "feelers": "^1.2.0", "feelin": "^3.0.0", "flatpickr": "^4.6.13", @@ -23729,7 +23728,6 @@ "lodash": "^4.5.0", "min-dash": "^4.0.0", "preact": "^10.5.14", - "preact-markup": "^2.1.1", "showdown": "^2.1.0" }, "dependencies": { @@ -29347,6 +29345,11 @@ "domify": { "version": "1.4.1" }, + "dompurify": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz", + "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==" + }, "dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -34965,10 +34968,6 @@ "preact": { "version": "10.5.14" }, - "preact-markup": { - "version": "2.1.1", - "requires": {} - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/packages/form-js-carbon-styles/src/carbon-styles.scss b/packages/form-js-carbon-styles/src/carbon-styles.scss index bbfcf8adf..af8b39a08 100644 --- a/packages/form-js-carbon-styles/src/carbon-styles.scss +++ b/packages/form-js-carbon-styles/src/carbon-styles.scss @@ -165,7 +165,7 @@ // Markdown styles ///////////// -.fjs-container .fjs-form-field.fjs-form-field-text .markup { +.fjs-container .fjs-form-field.fjs-form-field-text { font-size: var(--cds-body-long-01-font-size); font-weight: var(--cds-body-long-01-font-weight); line-height: var(--cds-body-long-01-line-height); diff --git a/packages/form-js-viewer/package.json b/packages/form-js-viewer/package.json index c3bb846f8..f786ec2fe 100644 --- a/packages/form-js-viewer/package.json +++ b/packages/form-js-viewer/package.json @@ -48,6 +48,7 @@ "big.js": "^6.2.1", "classnames": "^2.3.1", "didi": "^10.0.1", + "dompurify": "^3.0.8", "feelers": "^1.2.0", "feelin": "^3.0.0", "flatpickr": "^4.6.13", @@ -55,7 +56,6 @@ "lodash": "^4.5.0", "min-dash": "^4.0.0", "preact": "^10.5.14", - "preact-markup": "^2.1.1", "showdown": "^2.1.0" }, "sideEffects": [ diff --git a/packages/form-js-viewer/rollup.config.js b/packages/form-js-viewer/rollup.config.js index 7dbb50fc0..3a048a25a 100644 --- a/packages/form-js-viewer/rollup.config.js +++ b/packages/form-js-viewer/rollup.config.js @@ -55,7 +55,6 @@ export default [ 'preact/jsx-runtime', 'preact/hooks', 'preact/compat', - 'preact-markup', 'flatpickr', 'showdown', '@carbon/grid', diff --git a/packages/form-js-viewer/src/render/components/form-fields/Text.js b/packages/form-js-viewer/src/render/components/form-fields/Text.js index ecb18d921..284ed506d 100644 --- a/packages/form-js-viewer/src/render/components/form-fields/Text.js +++ b/packages/form-js-viewer/src/render/components/form-fields/Text.js @@ -1,7 +1,6 @@ -import Markup from 'preact-markup'; -import { useMemo } from 'preact/hooks'; +import { useCallback, useMemo } from 'preact/hooks'; import { useService, useTemplateEvaluation } from '../../hooks'; -import { sanitizeHTML } from '../Sanitizer'; +import { SanitizedRawHTMLRenderer } from './parts/SanitizedRawHTMLRenderer'; import { formFieldClasses @@ -19,33 +18,35 @@ export function Text(props) { const { text = '', strict = false } = field; const markdownRenderer = useService('markdownRenderer'); - - // feelers => pure markdown const markdown = useTemplateEvaluation(text, { debug: true, strict }); + const html = useMemo(() => markdownRenderer.render(markdown), [ markdownRenderer, markdown ]); + + const transformLinks = useCallback((html) => { + + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + + const links = tempDiv.querySelectorAll('a'); - // markdown => safe HTML - const safeHtml = useMemo(() => { - const html = markdownRenderer.render(markdown); - return sanitizeHTML(html); - }, [ markdownRenderer, markdown ]); + links.forEach(link => { - const OverriddenTargetLink = useMemo(() => BuildOverriddenTargetLink(textLinkTarget), [ textLinkTarget ]); + if (disableLinks) { + link.setAttribute('class', 'fjs-disabled-link'); + link.setAttribute('tabIndex', '-1'); + } - const componentOverrides = useMemo(() => { + if (textLinkTarget) { + link.setAttribute('target', textLinkTarget); + } - if (disableLinks) { - return { 'a': DisabledLink }; - } + }); - if (textLinkTarget) { - return { 'a': OverriddenTargetLink }; - } + return tempDiv.innerHTML; - return {}; - }, [ disableLinks, OverriddenTargetLink, textLinkTarget ]); + }, [ disableLinks, textLinkTarget ]); return
- +
; } @@ -59,11 +60,3 @@ Text.config = { ...options }) }; - -function BuildOverriddenTargetLink(target) { - return function({ children, ...rest }) { - return { children }; - }; -} - -function DisabledLink({ children, ...rest }) { return { children }; } diff --git a/packages/form-js-viewer/src/render/components/form-fields/parts/SanitizedRawHTMLRenderer.js b/packages/form-js-viewer/src/render/components/form-fields/parts/SanitizedRawHTMLRenderer.js new file mode 100644 index 000000000..0a22811e6 --- /dev/null +++ b/packages/form-js-viewer/src/render/components/form-fields/parts/SanitizedRawHTMLRenderer.js @@ -0,0 +1,9 @@ +import DOMPurify from 'dompurify'; +import { useMemo } from 'preact/hooks'; + +export const SanitizedRawHTMLRenderer = ({ html, transform = (html) => html }) => { + const sanitizedHtml = DOMPurify.sanitize(html); + const tranformedHtml = useMemo(() => transform(sanitizedHtml), [ sanitizedHtml, transform ]); + + return
; +}; diff --git a/packages/form-js-viewer/test/spec/render/components/form-fields/Text.spec.js b/packages/form-js-viewer/test/spec/render/components/form-fields/Text.spec.js index 1c9e8aa2d..5fea82a16 100644 --- a/packages/form-js-viewer/test/spec/render/components/form-fields/Text.spec.js +++ b/packages/form-js-viewer/test/spec/render/components/form-fields/Text.spec.js @@ -82,7 +82,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql(`

h1

+ expect(formField.innerHTML).to.eql(`

h1

h2

h3

h4

@@ -102,7 +102,7 @@ Some _em_ **strong** [text](#text) \`code\`.
Some Code

Some em strong text code.


-

Image

`); +

Image

`); }); @@ -178,7 +178,7 @@ Some _em_ **strong** [text](#text) \`code\`. expect(formField).to.exist; expect(formField.classList.contains('fjs-form-field-text')).to.be.true; - expect(formField.innerHTML).to.equal('
'); + expect(formField.innerHTML).to.equal('
'); }); @@ -199,7 +199,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo

'); + expect(formField.innerHTML).to.eql('

foo

'); }); @@ -216,7 +216,7 @@ Some _em_ **strong** [text](#text) \`code\`. // then const formField = container.querySelector('.fjs-form-field'); - const expected = `
+ const expected = `
@@ -229,7 +229,7 @@ Some _em_ **strong** [text](#text) \`code\`. -
fooqux
`; +
`; expect(formField).to.exist; expect(formField.innerHTML).to.eql(expected); @@ -258,7 +258,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo

'); + expect(formField.innerHTML).to.eql('

foo

'); }); @@ -278,7 +278,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

["#foo", "###bar"]

'); + expect(formField.innerHTML).to.eql('

["#foo", "###bar"]

'); }); @@ -330,7 +330,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo

'); + expect(formField.innerHTML).to.eql('

foo

'); }); @@ -354,7 +354,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

bar

'); + expect(formField.innerHTML).to.eql('

bar

'); }); @@ -378,7 +378,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

bar

'); + expect(formField.innerHTML).to.eql('

bar

'); }); @@ -403,7 +403,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo bar

'); + expect(formField.innerHTML).to.eql('

foo bar

'); }); @@ -428,7 +428,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

bar foo

'); + expect(formField.innerHTML).to.eql('

bar foo

'); }); @@ -453,7 +453,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo bar

'); + expect(formField.innerHTML).to.eql('

foo bar

'); }); @@ -478,7 +478,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo

'); + expect(formField.innerHTML).to.eql('

foo

'); }); @@ -503,7 +503,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo bar

'); + expect(formField.innerHTML).to.eql('

foo bar

'); }); @@ -527,7 +527,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo barbarbar

'); + expect(formField.innerHTML).to.eql('

foo barbarbar

'); }); @@ -551,7 +551,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo barbarbar

'); + expect(formField.innerHTML).to.eql('

foo barbarbar

'); }); @@ -575,7 +575,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo abc

'); + expect(formField.innerHTML).to.eql('

foo abc

'); }); @@ -601,7 +601,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

foo {{⚠}}

'); + expect(formField.innerHTML).to.eql('

foo {{⚠}}

'); }); @@ -629,7 +629,7 @@ Some _em_ **strong** [text](#text) \`code\`. const formField = container.querySelector('.fjs-form-field'); expect(formField).to.exist; - expect(formField.innerHTML).to.eql('

EVALUATED:myTemplate

'); + expect(formField.innerHTML).to.eql('

EVALUATED:myTemplate

'); }); });