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