Skip to content

Commit ee3ba36

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 36cab76 commit ee3ba36

2 files changed

Lines changed: 54 additions & 16 deletions

File tree

cypress/component/richtext.cy.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
// Reference tests: https://github.com/nextcloud-deps/CDMarkdownKit/tree/master/CDMarkdownKitTests
88

99
import { mount } from 'cypress/vue2'
10+
import Vue from 'vue'
11+
import VueRouter from 'vue-router'
1012
import NcRichText from '../../src/components/NcRichText/NcRichText.vue'
1113

1214
describe('NcRichText', () => {
@@ -402,34 +404,56 @@ describe('NcRichText', () => {
402404
})
403405

404406
describe('links', () => {
407+
const TestRouteComponent = {
408+
template: '<div />',
409+
}
410+
411+
const mountRichText = (text: string) => {
412+
Vue.use(VueRouter)
413+
const routes = [{ path: '/world', component: TestRouteComponent }]
414+
const router = new VueRouter({
415+
mode: 'history',
416+
routes,
417+
})
418+
419+
mount(NcRichText, {
420+
propsData: {
421+
text,
422+
useMarkdown: true,
423+
},
424+
extensions: {
425+
plugins: [router],
426+
},
427+
router,
428+
})
429+
}
430+
405431
const testLink = (key: string, { text, href = text, name = text }) => {
406432
it(key, () => {
407-
mount(NcRichText, {
408-
propsData: {
409-
text,
410-
useMarkdown: true,
411-
},
412-
})
433+
mountRichText(text)
413434
cy.get('a').should('have.text', name)
414435
cy.get('a').invoke('attr', 'href').should('eq', href)
415436
})
416437
}
417438

418439
testLink('autolink', { text: 'https://autolink.me' })
419-
testLink('relative link', { text: '[hello](world)', href: 'world', name: 'hello' })
440+
testLink('relative link', { text: '[hello](/world)', href: '/world', name: 'hello' })
420441
testLink('absolute link', { text: '[hello](https://nextcloud.com)', href: 'https://nextcloud.com', name: 'hello' })
421442
testLink('tel link', { text: '[hello](tel:+49123456789)', href: 'tel:+49123456789', name: 'hello' })
443+
testLink('mailto link', { text: '[hello](mailto:+49123456789)', href: 'mailto:+49123456789', name: 'hello' })
422444

423-
it('no link to unknown protocols', () => {
424-
mount(NcRichText, {
425-
propsData: {
426-
text: '[link](other:proto)',
427-
useMarkdown: true,
428-
},
445+
const testNoLink = (key: string, { text, name = text }) => {
446+
it(key, () => {
447+
mountRichText(text)
448+
449+
cy.get('body').should('contain', name)
450+
cy.get('a').should('not.exist')
429451
})
430-
cy.get('body').should('contain', name)
431-
cy.get('a').should('not.exist')
432-
})
452+
}
453+
testNoLink('no link to unknown protocols', { text: '[hello](other:proto)', name: 'hello' })
454+
testNoLink('no link to unresolved relative link (by router)', { text: '[hello](world)', name: 'hello' })
455+
testNoLink('no link to relative parameters', { text: '[hello](?parameters=1)', name: 'hello' })
456+
testNoLink('no link to relative anchor', { text: '[hello](#anchor)', name: 'hello' })
433457
})
434458

435459
describe('multiline code', () => {

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 GenRandomId from '../../utils/GenRandomId.js'
324325
import { getRoute, remarkAutolink } from './autolink.js'
325326
import { prepareTextNode, remarkPlaceholder } from './placeholder.js'
@@ -556,6 +557,7 @@ export default {
556557
if (tag === 'a') {
557558
const route = getRoute(this.$router, attrs.attrs.href)
558559
if (route) {
560+
// Resolved link to this app; render RouterLink
559561
delete attrs.attrs.href
560562
delete attrs.attrs.target
561563
@@ -566,6 +568,18 @@ export default {
566568
},
567569
}, children)
568570
}
571+
572+
const isAllowedScheme = /^(https?:\/\/|tel:|mailto:)/.test(attrs.attrs.href)
573+
if (isAllowedScheme) {
574+
// External link; render normally, open in the new tab
575+
attrs.attrs.href = attrs.attrs.href.trim()
576+
return h(NcRichTextExternalLink, attrs, children)
577+
} else {
578+
// Unresolved relative link that does not belong to this app; render only children
579+
delete attrs.attrs.href
580+
delete attrs.attrs.target
581+
return h('span', attrs, children)
582+
}
569583
}
570584
571585
return h(tag, attrs, children)

0 commit comments

Comments
 (0)