From 095ac215a4b4724d8651462bc68645a6087ec33d Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Fri, 6 Mar 2026 16:36:44 +0200 Subject: [PATCH 1/3] fix: change default link protocol --- packages/super-editor/src/components/toolbar/LinkInput.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/super-editor/src/components/toolbar/LinkInput.vue b/packages/super-editor/src/components/toolbar/LinkInput.vue index b375027067..bf4e666f96 100644 --- a/packages/super-editor/src/components/toolbar/LinkInput.vue +++ b/packages/super-editor/src/components/toolbar/LinkInput.vue @@ -121,7 +121,7 @@ const isAnchor = ref(false); // Prepend http if missing const url = computed(() => { if (!rawUrl.value) return ''; - if (!rawUrl.value.startsWith('http') && !rawUrl.value.startsWith('#')) return 'http://' + rawUrl.value; + if (!rawUrl.value.startsWith('https') && !rawUrl.value.startsWith('#')) return 'https://' + rawUrl.value; return rawUrl.value; }); From f46998a81381f3e1eae780e8934d15d81656ddff Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 17 Mar 2026 17:09:04 -0700 Subject: [PATCH 2/3] fix: default link protocol to https without mangling existing http URLs --- .../src/components/toolbar/LinkInput.test.js | 28 +++++++++++++++++++ .../src/components/toolbar/LinkInput.vue | 8 ++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/super-editor/src/components/toolbar/LinkInput.test.js b/packages/super-editor/src/components/toolbar/LinkInput.test.js index 8196b1b569..dbd656969f 100644 --- a/packages/super-editor/src/components/toolbar/LinkInput.test.js +++ b/packages/super-editor/src/components/toolbar/LinkInput.test.js @@ -843,4 +843,32 @@ describe('LinkInput - getLinkHrefAtSelection type safety and boundary checking', expect(mockClosePopover).not.toHaveBeenCalled(); }); }); + + describe('URL normalization', () => { + it('preserves explicit http links when submitting an existing link', async () => { + const mockEditor = createMockEditor(); + mockEditor.options = { documentMode: 'editing' }; + const linkMark = mockEditor.state.schema.marks.link; + mockEditor.state.selection.$from.nodeAfter = { + marks: [{ type: linkMark, attrs: { href: 'http://example.com' } }], + }; + + const wrapper = mount(LinkInput, { + props: { + editor: mockEditor, + closePopover: mockClosePopover, + showInput: true, + }, + }); + + await nextTick(); + await nextTick(); + + wrapper.vm.handleSubmit(); + + expect(mockEditor.commands.toggleLink).toHaveBeenCalledWith( + expect.objectContaining({ href: 'http://example.com' }), + ); + }); + }); }); diff --git a/packages/super-editor/src/components/toolbar/LinkInput.vue b/packages/super-editor/src/components/toolbar/LinkInput.vue index bf4e666f96..ee52f60ceb 100644 --- a/packages/super-editor/src/components/toolbar/LinkInput.vue +++ b/packages/super-editor/src/components/toolbar/LinkInput.vue @@ -118,11 +118,13 @@ const text = ref(''); const rawUrl = ref(''); const isAnchor = ref(false); -// Prepend http if missing +const HAS_PROTOCOL = /^[a-z][a-z0-9+.-]*:/i; + +// Default to https:// when no protocol is specified const url = computed(() => { if (!rawUrl.value) return ''; - if (!rawUrl.value.startsWith('https') && !rawUrl.value.startsWith('#')) return 'https://' + rawUrl.value; - return rawUrl.value; + if (rawUrl.value.startsWith('#') || HAS_PROTOCOL.test(rawUrl.value)) return rawUrl.value; + return 'https://' + rawUrl.value; }); const validUrl = computed(() => { From cfed35f9497f57c0750c9c4ae24dcc375271205d Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 17 Mar 2026 17:14:54 -0700 Subject: [PATCH 3/3] fix: default link protocol to https and sanitize href before use --- .../src/components/toolbar/LinkInput.test.js | 58 +++++++++++++++++++ .../src/components/toolbar/LinkInput.vue | 28 +++++---- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/packages/super-editor/src/components/toolbar/LinkInput.test.js b/packages/super-editor/src/components/toolbar/LinkInput.test.js index dbd656969f..ae377b4c28 100644 --- a/packages/super-editor/src/components/toolbar/LinkInput.test.js +++ b/packages/super-editor/src/components/toolbar/LinkInput.test.js @@ -845,6 +845,31 @@ describe('LinkInput - getLinkHrefAtSelection type safety and boundary checking', }); describe('URL normalization', () => { + it('defaults bare domains to https when submitting a new link', async () => { + const mockEditor = createMockEditor(); + mockEditor.options = { documentMode: 'editing' }; + + const wrapper = mount(LinkInput, { + props: { + editor: mockEditor, + closePopover: mockClosePopover, + showInput: true, + }, + }); + + await nextTick(); + await nextTick(); + + await wrapper.find('input[name="link"]').setValue('example.com'); + await nextTick(); + + wrapper.vm.handleSubmit(); + + expect(mockEditor.commands.toggleLink).toHaveBeenCalledWith( + expect.objectContaining({ href: 'https://example.com' }), + ); + }); + it('preserves explicit http links when submitting an existing link', async () => { const mockEditor = createMockEditor(); mockEditor.options = { documentMode: 'editing' }; @@ -870,5 +895,38 @@ describe('LinkInput - getLinkHrefAtSelection type safety and boundary checking', expect.objectContaining({ href: 'http://example.com' }), ); }); + + it('blocks unsafe schemes in both submit and open-link flows', async () => { + const mockEditor = createMockEditor(); + mockEditor.options = { documentMode: 'editing' }; + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); + + const wrapper = mount(LinkInput, { + props: { + editor: mockEditor, + closePopover: mockClosePopover, + showInput: true, + }, + }); + + await nextTick(); + await nextTick(); + + await wrapper.find('input[name="link"]').setValue('javascript:foo.bar()'); + await nextTick(); + + const openLinkBtn = wrapper.find('.open-link-icon'); + expect(openLinkBtn.classes()).toContain('disabled'); + + wrapper.vm.handleSubmit(); + await openLinkBtn.trigger('click'); + + expect(wrapper.vm.urlError).toBe(true); + expect(mockEditor.commands.toggleLink).not.toHaveBeenCalled(); + expect(mockClosePopover).not.toHaveBeenCalled(); + expect(openSpy).not.toHaveBeenCalled(); + + openSpy.mockRestore(); + }); }); }); diff --git a/packages/super-editor/src/components/toolbar/LinkInput.vue b/packages/super-editor/src/components/toolbar/LinkInput.vue index ee52f60ceb..933bb080fe 100644 --- a/packages/super-editor/src/components/toolbar/LinkInput.vue +++ b/packages/super-editor/src/components/toolbar/LinkInput.vue @@ -1,5 +1,6 @@