diff --git a/packages/app-mobile/components/NoteEditor/RichTextEditor.test.tsx b/packages/app-mobile/components/NoteEditor/RichTextEditor.test.tsx index 4319ccf780..147b0c89db 100644 --- a/packages/app-mobile/components/NoteEditor/RichTextEditor.test.tsx +++ b/packages/app-mobile/components/NoteEditor/RichTextEditor.test.tsx @@ -288,6 +288,26 @@ describe('RichTextEditor', () => { }); }); + it('should avoid rendering URLs with unknown protocols', async () => { + let body = '[link](unknown://test)'; + + render( { body = newBody; }} + />); + + const renderedLink = await findElement('a[href][data-original-href]'); + expect(renderedLink.getAttribute('href')).toBe('#'); + expect(renderedLink.getAttribute('data-original-href')).toBe('unknown://test'); + + const window = await getEditorWindow(); + mockTyping(window, ' testing'); + + await waitFor(async () => { + expect(body.trim()).toBe('[link](unknown://test) testing'); + }); + }); + it.each([ MarkupLanguage.Markdown, MarkupLanguage.Html, ])('should preserve image attachments on edit (case %#)', async (markupLanguage) => { diff --git a/packages/app-mobile/contentScripts/richTextEditorBundle/contentScript/index.ts b/packages/app-mobile/contentScripts/richTextEditorBundle/contentScript/index.ts index 65bbee572f..4644079fca 100644 --- a/packages/app-mobile/contentScripts/richTextEditorBundle/contentScript/index.ts +++ b/packages/app-mobile/contentScripts/richTextEditorBundle/contentScript/index.ts @@ -10,6 +10,24 @@ import convertHtmlToMarkdown from './convertHtmlToMarkdown'; import { ExportedWebViewGlobals as MarkdownEditorWebViewGlobals } from '../../markdownEditorBundle/types'; import { EditorEventType } from '@joplin/editor/events'; +const postprocessHtml = (html: HTMLElement) => { + // Fix resource URLs + const resources = html.querySelectorAll('img[data-resource-id]'); + for (const resource of resources) { + const resourceId = resource.getAttribute('data-resource-id'); + resource.src = `:/${resourceId}`; + } + + // Restore HREFs + const links = html.querySelectorAll('a[href="#"][data-original-href]'); + for (const link of links) { + link.href = link.getAttribute('data-original-href'); + link.removeAttribute('data-original-href'); + } + + return html; +}; + const wrapHtmlForMarkdownConversion = (html: HTMLElement) => { // Add a container element -- when converting to HTML, Turndown // sometimes doesn't process the toplevel element in the same way diff --git a/packages/editor/ProseMirror/schema.ts b/packages/editor/ProseMirror/schema.ts index 4d927a4d50..a6ea4dd6bd 100644 --- a/packages/editor/ProseMirror/schema.ts +++ b/packages/editor/ProseMirror/schema.ts @@ -3,6 +3,8 @@ import { nodeSpecs as joplinEditableNodes } from './plugins/joplinEditablePlugin import { tableNodes } from 'prosemirror-tables'; import { nodeSpecs as listNodes } from './plugins/listPlugin'; import { nodeSpecs as resourcePlaceholderNodes } from './plugins/resourcePlaceholderPlugin'; +import { hasProtocol } from '@joplin/utils/url'; +import { isResourceUrl } from '@joplin/lib/models/utils/resourceUtils'; import { nodeSpecs as detailsNodes } from './plugins/detailsPlugin'; // For reference, see: @@ -259,8 +261,15 @@ const marks = { tag: 'a[href]', getAttrs: node => { const resourceId = node.getAttribute('data-resource-id'); - const href = node.getAttribute('href'); + let href = node.getAttribute('href'); const isResourceLink = resourceId && href === '#'; + if (isResourceLink) { + href = `:/${resourceId}`; + } + + if (href === '#' && node.hasAttribute('data-original-href')) { + href = node.getAttribute('data-original-href'); + } return { href: isResourceLink ? `:/${resourceId}` : href, @@ -269,10 +278,29 @@ const marks = { }; }, }], - toDOM: node => [ - 'a', - { href: node.attrs.href, title: node.attrs.title, 'data-resource-id': node.attrs.dataResourceId }, - ], + toDOM: node => { + const isSafeForRendering = (href: string) => { + return hasProtocol(href, ['http', 'https', 'joplin']) || isResourceUrl(href); + }; + + // Avoid rendering URLs with unknown protocols (avoid rendering or pasting unsafe HREFs). + // Note that URL click handling is handled elsewhere and does not use the HTML "href" attribute. + // However "href" may be used by the right-click menu on web: + const safeHref = isSafeForRendering(node.attrs.href) ? node.attrs.href : '#'; + + return [ + 'a', + { + href: safeHref, + ...(safeHref !== node.attrs.href ? { + 'data-original-href': node.attrs.href, + } : {}), + + title: node.attrs.title, + 'data-resource-id': node.attrs.dataResourceId, + }, + ]; + }, }, } satisfies Record;