diff --git a/webapp/channels/src/components/markdown_image/index.ts b/webapp/channels/src/components/markdown_image/index.ts index ad10648efc..14205a776a 100644 --- a/webapp/channels/src/components/markdown_image/index.ts +++ b/webapp/channels/src/components/markdown_image/index.ts @@ -5,9 +5,23 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import type {Dispatch} from 'redux'; +import {getPost} from 'mattermost-redux/selectors/entities/posts'; + import {openModal} from 'actions/views/modals'; +import type {GlobalState} from 'types/store'; + import MarkdownImage from './markdown_image'; +import type {Props} from './markdown_image'; + +function mapStateToProps(state: GlobalState, ownProps: Props) { + const post = getPost(state, ownProps.postId); + const isUnsafeLinksPost = post?.props?.unsafe_links === 'true'; + + return { + isUnsafeLinksPost, + }; +} function mapDispatchToProps(dispatch: Dispatch) { return { @@ -17,6 +31,6 @@ function mapDispatchToProps(dispatch: Dispatch) { }; } -const connector = connect(null, mapDispatchToProps); +const connector = connect(mapStateToProps, mapDispatchToProps); export default connector(MarkdownImage); diff --git a/webapp/channels/src/components/markdown_image/markdown_image.test.tsx b/webapp/channels/src/components/markdown_image/markdown_image.test.tsx index 8781aa9b71..7568d48697 100644 --- a/webapp/channels/src/components/markdown_image/markdown_image.test.tsx +++ b/webapp/channels/src/components/markdown_image/markdown_image.test.tsx @@ -332,4 +332,12 @@ describe('components/MarkdownImage', () => { expect(childrenWrapper).toMatchSnapshot(); }); + + test('should render a alt text if the link is unsafe', () => { + const props = {...baseProps, isUnsafeLinksPost: true}; + const wrapper = shallow( + , + ); + expect(wrapper.text()).toBe(props.alt); + }); }); diff --git a/webapp/channels/src/components/markdown_image/markdown_image.tsx b/webapp/channels/src/components/markdown_image/markdown_image.tsx index 69795ec90e..82dfec9ac6 100644 --- a/webapp/channels/src/components/markdown_image/markdown_image.tsx +++ b/webapp/channels/src/components/markdown_image/markdown_image.tsx @@ -36,6 +36,7 @@ export type Props = { openModal:

(modalData: ModalData

) => void; }; hideUtilities?: boolean; + isUnsafeLinksPost: boolean; }; type State = { @@ -156,6 +157,9 @@ export default class MarkdownImage extends PureComponent { ); } + if (this.props.isUnsafeLinksPost) { + return <>{alt}; + } return ( { + const testCases = [ + {input: '[link text](http://markdownlink.com)', expected: '

link text : http://markdownlink.com

'}, + {input: '[link text](//markdownlink.com/test)', expected: '

link text : //markdownlink.com/test

'}, + {input: '[link text](http://my.site.com/whatever)', expected: '

link text : http://my.site.com/whatever

'}, + {input: '[link text](http://my.site.com/api/v4/image?url=ohno)', expected: '

link text : http://my.site.com/api/v4/image?url=ohno

'}, + {input: '[link text](http://my.site.com/_redirect/pl/c18xpcpusjd88en1g4j7us31ur)', expected: '

link text

'}, + {input: '[link text](http://my.site.com/someteam/pl/c18xpcpusjd88en1g4j7us31ur)', expected: '

link text

'}, + {input: '[link text](http://my.site.com/_redirect/pl/c18xpcpusjd88en1g4j7us31ur/ohno)', expected: '

link text : http://my.site.com/_redirect/pl/c18xpcpusjd88en1g4j7us31ur/ohno

'}, + {input: '[link text](http://my.site.com/more/stuff/here/pl/c18xpcpusjd88en1g4j7us31ur)', expected: '

link text : http://my.site.com/more/stuff/here/pl/c18xpcpusjd88en1g4j7us31ur

'}, + {input: '[link text](http://myqsite.com/someteam/pl/c18xpcpusjd88en1g4j7us31ur)', expected: '

link text : http://myqsite.com/someteam/pl/c18xpcpusjd88en1g4j7us31ur

'}, + + ]; + for (const testCase of testCases) { + const output = format(testCase.input, {unsafeLinks: true, siteURL: 'http://my.site.com'}); + expect(output).toEqual(testCase.expected); + } + }); }); diff --git a/webapp/channels/src/utils/markdown/renderer.tsx b/webapp/channels/src/utils/markdown/renderer.tsx index 2633dc1bc4..b3671d9066 100644 --- a/webapp/channels/src/utils/markdown/renderer.tsx +++ b/webapp/channels/src/utils/markdown/renderer.tsx @@ -7,7 +7,7 @@ import type {MarkedOptions} from 'marked'; import EmojiMap from 'utils/emoji_map'; import * as PostUtils from 'utils/post_utils'; import * as TextFormatting from 'utils/text_formatting'; -import {getScheme, isUrlSafe, shouldOpenInNewTab} from 'utils/url'; +import {mightTriggerExternalRequest, getScheme, isUrlSafe, shouldOpenInNewTab} from 'utils/url'; import {parseImageDimensions} from './helpers'; @@ -136,6 +136,13 @@ export default class Renderer extends marked.Renderer { public link(href: string, title: string, text: string, isUrl = false) { let outHref = href; + if (this.formattingOptions.unsafeLinks && mightTriggerExternalRequest(href, this.formattingOptions.siteURL)) { + if (text === href) { + return text; + } + return text + ' : ' + href; + } + if (!href.startsWith('/')) { const scheme = getScheme(href); if (!scheme) { diff --git a/webapp/channels/src/utils/text_formatting.tsx b/webapp/channels/src/utils/text_formatting.tsx index b57c3ae1ed..4d0b55ad5e 100644 --- a/webapp/channels/src/utils/text_formatting.tsx +++ b/webapp/channels/src/utils/text_formatting.tsx @@ -200,6 +200,13 @@ export interface TextFormattingOptionsBase { * Defaults to `false`. */ atPlanMentions: boolean; + + /** + * If true, the renderer will assume links are not safe. + * + * Defaults to `false`. + */ + unsafeLinks: boolean; } export type TextFormattingOptions = Partial; @@ -227,6 +234,7 @@ const DEFAULT_OPTIONS: TextFormattingOptions = { proxyImages: false, editedAt: 0, postId: '', + unsafeLinks: false, }; /** diff --git a/webapp/channels/src/utils/url.tsx b/webapp/channels/src/utils/url.tsx index 52429cf0c3..f1989fe39d 100644 --- a/webapp/channels/src/utils/url.tsx +++ b/webapp/channels/src/utils/url.tsx @@ -168,6 +168,24 @@ export function validateChannelUrl(url: string, intl?: IntlShape): Array