Skip to content

Commit a5ae090

Browse files
committed
fix(NcRichText)!: do not render non-resolved relative links
- still works: external links, internal absolute links, and internal relative links, that do have a router match for current app - internal relative links without match (also ?query and #anchor) only render an inner text - allow tel: and mail: to reduce breaking changes Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
1 parent 97636a4 commit a5ae090

2 files changed

Lines changed: 41 additions & 10 deletions

File tree

src/components/NcRichText/NcRichText.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ import { RouterLink } from 'vue-router'
320320
import NcCheckboxRadioSwitch from '../NcCheckboxRadioSwitch/NcCheckboxRadioSwitch.vue'
321321
import NcReferenceList from './NcReferenceList.vue'
322322
import NcRichTextCopyButton from './NcRichTextCopyButton.vue'
323+
import NcRichTextExternalLink from './NcRichTextExternalLink.vue'
323324
import { createElementId } from '../../utils/createElementId.ts'
324325
import { getRoute, parseUrl, remarkAutolink } from './autolink.ts'
325326
import { remarkPlaceholder } from './remarkPlaceholder.ts'
@@ -630,6 +631,7 @@ export default {
630631
if (String(type) === 'a') {
631632
const route = getRoute(this.$router, props.href)
632633
if (route) {
634+
// Resolved link to this app; render RouterLink
633635
delete props.href
634636
delete props.target
635637
@@ -638,6 +640,18 @@ export default {
638640
to: route,
639641
}, { default: () => children })
640642
}
643+
644+
const isAllowedScheme = /^(https?:\/\/|tel:|mailto:)/.test(props.href)
645+
if (isAllowedScheme) {
646+
// External link; render normally, open in the new tab
647+
props.href = props.href.trim()
648+
return h(NcRichTextExternalLink, props, children)
649+
} else {
650+
// Unresolved relative link that does not belong to this app; render only children
651+
delete props.href
652+
delete props.target
653+
return h('span', props, children)
654+
}
641655
}
642656
return h(type, props, children)
643657
}

tests/component/components/NcRichText/markown-rendering.spec.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,33 +420,50 @@ test.describe('inline code', () => {
420420
})
421421

422422
test.describe('links', () => {
423+
const TestRouteComponent = {
424+
template: '<div />',
425+
}
426+
423427
const testLink = (key: string, { text, href = text, name = text }) => {
424428
test(key, async ({ mount }) => {
425429
const component = await mount(NcRichText, {
426430
props: {
427431
text,
428432
useExtendedMarkdown: true,
429433
},
434+
hooksConfig: {
435+
routes: [{ path: '/world', component: TestRouteComponent }],
436+
},
430437
})
431438
await expect(component.getByRole('link', { name }))
432439
.toHaveAttribute('href', href)
433440
})
434441
}
435442
testLink('autolink', { text: 'https://autolink.me' })
436-
testLink('relative link', { text: '[hello](world)', href: 'world', name: 'hello' })
443+
testLink('relative link', { text: '[hello](/world)', href: '/world', name: 'hello' })
437444
testLink('absolute link', { text: '[hello](https://nextcloud.com)', href: 'https://nextcloud.com', name: 'hello' })
438445
testLink('tel link', { text: '[hello](tel:+49123456789)', href: 'tel:+49123456789', name: 'hello' })
446+
testLink('mailto link', { text: '[hello](mailto:+49123456789)', href: 'mailto:+49123456789', name: 'hello' })
439447

440-
test('no link to unknown protocols', async ({ mount }) => {
441-
const component = await mount(NcRichText, {
442-
props: {
443-
text: '[link](other:proto)',
444-
useExtendedMarkdown: true,
445-
},
448+
const testNoLink = (key: string, { text, name = text }) => {
449+
test(key, async ({ mount }) => {
450+
const component = await mount(NcRichText, {
451+
props: {
452+
text,
453+
useExtendedMarkdown: true,
454+
},
455+
hooksConfig: {
456+
routes: [{ path: '/world', component: TestRouteComponent }],
457+
},
458+
})
459+
await expect(component).toContainText(name)
460+
await expect(component.getByText(name)).not.toHaveRole('link')
446461
})
447-
await expect(component).toContainText('link')
448-
await expect(component.getByText('link')).not.toHaveRole('link')
449-
})
462+
}
463+
testNoLink('no link to unknown protocols', { text: '[hello](other:proto)', name: 'hello' })
464+
testNoLink('no link to unresolved relative link (by router)', { text: '[hello](world)', name: 'hello' })
465+
testNoLink('no link to relative parameters', { text: '[hello](?parameters=1)', name: 'hello' })
466+
testNoLink('no link to relative anchor', { text: '[hello](#anchor)', name: 'hello' })
450467
})
451468

452469
test.describe('multiline code', () => {

0 commit comments

Comments
 (0)