diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts index c092aa0db09..4f38a874b52 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/Paste.ts @@ -7,6 +7,7 @@ import convertPastedContentFromWord from './wordConverter/convertPastedContentFr import getPasteSource from './sourceValidations/getPasteSource'; import handleLineMerge from './lineMerge/handleLineMerge'; import sanitizeHtmlColorsFromPastedContent from './sanitizeHtmlColorsFromPastedContent/sanitizeHtmlColorsFromPastedContent'; +import sanitizeLinks from './sanitizeLinks/sanitizeLinks'; import { EditorPlugin, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { GOOGLE_SHEET_NODE_NAME } from './sourceValidations/constants'; import { KnownSourceType } from './sourceValidations/KnownSourceType'; @@ -83,7 +84,7 @@ export default class Paste implements EditorPlugin { handleLineMerge(fragment); break; } - + sanitizeLinks(sanitizingOption); sanitizeHtmlColorsFromPastedContent(sanitizingOption); // Replace unknown tags with SPAN diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeLinks/sanitizeLinks.ts b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeLinks/sanitizeLinks.ts new file mode 100644 index 00000000000..afd89eb228d --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/Paste/sanitizeLinks/sanitizeLinks.ts @@ -0,0 +1,33 @@ +import { chainSanitizerCallback } from 'roosterjs-editor-dom'; +import { HtmlSanitizerOptions } from 'roosterjs-editor-types'; + +const HTTP = 'http:'; +const HTTPS = 'https:'; + +/** + * @internal + * Clear local paths and remove link + * @param sanitizingOption the sanitizingOption of BeforePasteEvent + * */ +export default function sanitizeLinks(sanitizingOption: Required) { + chainSanitizerCallback( + sanitizingOption.attributeCallbacks, + 'href', + (value: string, element: HTMLElement) => validateLink(value, element) + ); +} + +function validateLink(link: string, htmlElement: HTMLElement) { + let url; + try { + url = new URL(link); + } catch { + url = undefined; + } + + if (url && (url.protocol === HTTP || url.protocol === HTTPS)) { + return link; + } + htmlElement.removeAttribute('href'); + return ''; +} diff --git a/packages/roosterjs-editor-plugins/test/paste/sanitizeLinksTest.ts b/packages/roosterjs-editor-plugins/test/paste/sanitizeLinksTest.ts new file mode 100644 index 00000000000..783bb38254b --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/paste/sanitizeLinksTest.ts @@ -0,0 +1,80 @@ +import sanitizeLinks from '../../lib/plugins/Paste/sanitizeLinks/sanitizeLinks'; +import { HtmlSanitizer } from 'roosterjs-editor-dom'; +import { + BeforePasteEvent, + SanitizeHtmlOptions, + PluginEventType, + ClipboardData, +} from 'roosterjs-editor-types'; + +describe('sanitizeLinks', () => { + function callSanitizer(fragment: DocumentFragment, sanitizingOption: SanitizeHtmlOptions) { + const sanitizer = new HtmlSanitizer(sanitizingOption); + sanitizer.convertGlobalCssToInlineCss(fragment); + sanitizer.sanitize(fragment); + } + + function runTest(source: string, expected: string) { + const doc = new DOMParser().parseFromString(source, 'text/html'); + const fragment = doc.createDocumentFragment(); + while (doc.body.firstChild) { + fragment.appendChild(doc.body.firstChild); + } + + const event = createBeforePasteEventMock(fragment); + sanitizeLinks(event.sanitizingOption); + callSanitizer(fragment, event.sanitizingOption); + + while (fragment.firstChild) { + doc.body.appendChild(fragment.firstChild); + } + + expect(doc.body.innerHTML).toBe(expected); + } + + it('sanitize anchor', () => { + runTest('', ''); + }); + + it('not sanitize anchor', () => { + runTest( + '', + '' + ); + }); + + it('sanitize div', () => { + runTest('
', '
'); + }); + + it('not sanitize div', () => { + runTest( + '
', + '
' + ); + }); +}); + +function createBeforePasteEventMock(fragment: DocumentFragment) { + return ({ + eventType: PluginEventType.BeforePaste, + clipboardData: {}, + fragment: fragment, + sanitizingOption: { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }, + htmlBefore: '', + htmlAfter: '', + htmlAttributes: {}, + } as unknown) as BeforePasteEvent; +}