diff --git a/cypress/component/richtext.cy.ts b/cypress/component/richtext.cy.ts index 0cdc6229b1..adbfa173c4 100644 --- a/cypress/component/richtext.cy.ts +++ b/cypress/component/richtext.cy.ts @@ -7,6 +7,8 @@ // Reference tests: https://github.com/nextcloud-deps/CDMarkdownKit/tree/master/CDMarkdownKitTests import { mount } from 'cypress/vue2' +import Vue from 'vue' +import VueRouter from 'vue-router' import NcRichText from '../../src/components/NcRichText/NcRichText.vue' describe('NcRichText', () => { @@ -402,34 +404,56 @@ describe('NcRichText', () => { }) describe('links', () => { + const TestRouteComponent = { + template: '
', + } + + const mountRichText = (text: string) => { + Vue.use(VueRouter) + const routes = [{ path: '/world', component: TestRouteComponent }] + const router = new VueRouter({ + mode: 'history', + routes, + }) + + mount(NcRichText, { + propsData: { + text, + useMarkdown: true, + }, + extensions: { + plugins: [router], + }, + router, + }) + } + const testLink = (key: string, { text, href = text, name = text }) => { it(key, () => { - mount(NcRichText, { - propsData: { - text, - useMarkdown: true, - }, - }) + mountRichText(text) cy.get('a').should('have.text', name) cy.get('a').invoke('attr', 'href').should('eq', href) }) } testLink('autolink', { text: 'https://autolink.me' }) - testLink('relative link', { text: '[hello](world)', href: 'world', name: 'hello' }) + testLink('relative link', { text: '[hello](/world)', href: '/world', name: 'hello' }) testLink('absolute link', { text: '[hello](https://nextcloud.com)', href: 'https://nextcloud.com', name: 'hello' }) testLink('tel link', { text: '[hello](tel:+49123456789)', href: 'tel:+49123456789', name: 'hello' }) + testLink('mailto link', { text: '[hello](mailto:+49123456789)', href: 'mailto:+49123456789', name: 'hello' }) - it('no link to unknown protocols', () => { - mount(NcRichText, { - propsData: { - text: '[link](other:proto)', - useMarkdown: true, - }, + const testNoLink = (key: string, { text, name = text }) => { + it(key, () => { + mountRichText(text) + + cy.get('body').should('contain', name) + cy.get('a').should('not.exist') }) - cy.get('body').should('contain', name) - cy.get('a').should('not.exist') - }) + } + testNoLink('no link to unknown protocols', { text: '[hello](other:proto)', name: 'hello' }) + testNoLink('no link to unresolved relative link (by router)', { text: '[hello](world)', name: 'hello' }) + testNoLink('no link to relative parameters', { text: '[hello](?parameters=1)', name: 'hello' }) + testNoLink('no link to relative anchor', { text: '[hello](#anchor)', name: 'hello' }) }) describe('multiline code', () => { diff --git a/src/components/NcRichText/NcRichText.vue b/src/components/NcRichText/NcRichText.vue index 8dfb11ebaf..3e5008e09f 100644 --- a/src/components/NcRichText/NcRichText.vue +++ b/src/components/NcRichText/NcRichText.vue @@ -320,6 +320,7 @@ import { RouterLink } from 'vue-router' import NcCheckboxRadioSwitch from '../NcCheckboxRadioSwitch/NcCheckboxRadioSwitch.vue' import NcReferenceList from './NcReferenceList.vue' import NcRichTextCopyButton from './NcRichTextCopyButton.vue' +import NcRichTextExternalLink from './NcRichTextExternalLink.vue' import GenRandomId from '../../utils/GenRandomId.js' import { getRoute, remarkAutolink } from './autolink.js' import { prepareTextNode, remarkPlaceholder } from './placeholder.js' @@ -556,6 +557,7 @@ export default { if (tag === 'a') { const route = getRoute(this.$router, attrs.attrs.href) if (route) { + // Resolved link to this app; render RouterLink delete attrs.attrs.href delete attrs.attrs.target @@ -566,6 +568,18 @@ export default { }, }, children) } + + const isAllowedScheme = /^(https?:\/\/|tel:|mailto:)/.test(attrs.attrs.href) + if (isAllowedScheme) { + // External link; render normally, open in the new tab + attrs.attrs.href = attrs.attrs.href.trim() + return h(NcRichTextExternalLink, attrs, children) + } else { + // Unresolved relative link that does not belong to this app; render only children + delete attrs.attrs.href + delete attrs.attrs.target + return h('span', attrs, children) + } } return h(tag, attrs, children) diff --git a/src/components/NcRichText/NcRichTextExternalLink.vue b/src/components/NcRichText/NcRichTextExternalLink.vue new file mode 100644 index 0000000000..ae48831b4a --- /dev/null +++ b/src/components/NcRichText/NcRichTextExternalLink.vue @@ -0,0 +1,41 @@ + + + + + + +