From d766bd5fc2fe6727a327470e2f8e85155577bc1d Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 12 Aug 2024 09:30:33 +0200 Subject: [PATCH] fix(link): respect custom protocols #5468 --- .changeset/fluffy-bears-remember.md | 5 ++++ packages/extension-link/src/link.ts | 25 ++++++++++++++----- .../integration/extensions/link.spec.ts | 25 +++++++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 .changeset/fluffy-bears-remember.md diff --git a/.changeset/fluffy-bears-remember.md b/.changeset/fluffy-bears-remember.md new file mode 100644 index 00000000000..4a7cb569805 --- /dev/null +++ b/.changeset/fluffy-bears-remember.md @@ -0,0 +1,5 @@ +--- +"@tiptap/extension-link": patch +--- + +Respect custom protocols for links again, custom protocols are supported in additional to the default set #5468 diff --git a/packages/extension-link/src/link.ts b/packages/extension-link/src/link.ts index 335cf7c2314..f78dc1db0b3 100644 --- a/packages/extension-link/src/link.ts +++ b/packages/extension-link/src/link.ts @@ -106,11 +106,24 @@ declare module '@tiptap/core' { // From DOMPurify // https://github.com/cure53/DOMPurify/blob/main/src/regexp.js -const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex -const IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape +// eslint-disable-next-line no-control-regex +const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g -function isAllowedUri(uri: string | undefined) { - return !uri || uri.replace(ATTR_WHITESPACE, '').match(IS_ALLOWED_URI) +function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) { + const allowedProtocols: string[] = ['http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp'] + + if (protocols) { + protocols.forEach(protocol => { + const nextProtocol = (typeof protocol === 'string' ? protocol : protocol.scheme) + + if (nextProtocol) { + allowedProtocols.push(nextProtocol) + } + }) + } + + // eslint-disable-next-line no-useless-escape + return !uri || uri.replace(ATTR_WHITESPACE, '').match(new RegExp(`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`, 'i')) } /** @@ -187,7 +200,7 @@ export const Link = Mark.create({ const href = (dom as HTMLElement).getAttribute('href') // prevent XSS attacks - if (!href || !isAllowedUri(href)) { + if (!href || !isAllowedUri(href, this.options.protocols)) { return false } return null @@ -197,7 +210,7 @@ export const Link = Mark.create({ renderHTML({ HTMLAttributes }) { // prevent XSS attacks - if (!isAllowedUri(HTMLAttributes.href)) { + if (!isAllowedUri(HTMLAttributes.href, this.options.protocols)) { // strip out the href return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0] } diff --git a/tests/cypress/integration/extensions/link.spec.ts b/tests/cypress/integration/extensions/link.spec.ts index b6f03442cd9..28dfc8487a8 100644 --- a/tests/cypress/integration/extensions/link.spec.ts +++ b/tests/cypress/integration/extensions/link.spec.ts @@ -250,4 +250,29 @@ describe('extension-link', () => { getEditorEl()?.remove() }) }) + + describe('custom protocols', () => { + it('allows using additional custom protocols', () => { + ['custom://test.css', 'another-custom://protocol.html', ...validUrls].forEach(url => { + editor = new Editor({ + element: createEditorEl(), + extensions: [ + Document, + Text, + Paragraph, + Link.configure({ + protocols: ['custom', { scheme: 'another-custom' }], + }), + ], + content: `

hello world!

`, + }) + + expect(editor.getHTML()).to.include(url) + expect(JSON.stringify(editor.getJSON())).to.include(url) + + editor?.destroy() + getEditorEl()?.remove() + }) + }) + }) })