From 5b6320b7dcf03f63009786551a0ba32d5dd914ee Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 3 Jul 2025 11:05:12 -0400 Subject: [PATCH] MM-62744 Change how custom URLs are autolinked to fix remote user at-mentions (#32080) * Stop explicitly passing autocompleteUrlSchemes into text formatting code This is the first part of changing how autocompleteUrlSchemes works so that it can be moved to be part of the parser like in mobile instead of happening in the renderer. I'm not a fan of using the global store directly in utils/markdown, but this seems like the only way to have this apply to all the Markdown that's rendered in various helpers throughout the app. Ideally, we'd have some getMarkdownParser selector and a hook which provides the config, but that's a future improvement to make" * MM-62744 Move URL filtering to the Markdown parser instead of the renderer MM-62744 is caused by two things: 1. URL autolinking takes place in the Markdown parser which occurs before at-mention parsing which (despite the "parsing" part) happens in the Markdown renderer in the web app. 2. The autolinking in marked is very aggressive and identifies anything that looks like some:text as a link. Those lead to remote mentions like `@user:server` being incorrectly parsed by Markdown as a link to `user:server`. It isn't renderered as a link because the URL filtering logic in the Markdown parser blocks that, but at that point, the Markdown renderer won't check if it's an at-mention. By moving the URL filtering to occur earlier, like it does in the mobile app, the Markdown code won't autolink `@user:server` (unless the server has `user` configured as a custom URL scheme for some reason), so it's free to be turned into an at-mention by the renderer code. * MM-62744 Ensure various regexes and features support remote mentions * Update marked back to master --- webapp/channels/package.json | 2 +- .../__snapshots__/panel_body.test.tsx.snap | 40 ------ .../channels/src/components/markdown/index.ts | 3 +- .../src/components/markdown/markdown.tsx | 2 - .../button_binding/button_binding.tsx | 1 - .../embedded_binding.test.tsx.snap | 4 - .../embedded_binding/embedded_binding.tsx | 1 - .../action_button/action_button.tsx | 1 - .../message_attachment.test.tsx.snap | 4 - .../message_attachment/message_attachment.tsx | 1 - .../src/actions/posts.test.ts | 7 ++ .../mattermost-redux/src/actions/posts.ts | 2 +- .../message_html_to_component.test.tsx.snap | 36 ++++++ webapp/channels/src/utils/constants.tsx | 2 +- webapp/channels/src/utils/markdown/index.ts | 21 +++- .../channels/src/utils/markdown/renderer.tsx | 11 +- .../utils/message_html_to_component.test.tsx | 17 +++ webapp/channels/src/utils/post_utils.test.tsx | 44 +++++++ webapp/channels/src/utils/text_formatting.tsx | 16 +-- .../utils/text_formatting_at_mentions.test.ts | 13 ++ .../src/utils/text_formatting_links.test.ts | 119 ++++++++++-------- webapp/package-lock.json | 6 +- 22 files changed, 213 insertions(+), 140 deletions(-) diff --git a/webapp/channels/package.json b/webapp/channels/package.json index 487644d613..d60b98f44d 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -51,7 +51,7 @@ "lodash": "4.17.21", "luxon": "3.6.1", "mark.js": "8.11.1", - "marked": "github:mattermost/marked#3b13ba8ddf725327ddf0298361d6d304a021f2d1", + "marked": "github:mattermost/marked#08f3638e37e17738fafcaf749683ce6fee1d8edc", "memoize-one": "6.0.0", "moment-timezone": "0.5.38", "monaco-editor": "0.52.2", diff --git a/webapp/channels/src/components/drafts/panel/__snapshots__/panel_body.test.tsx.snap b/webapp/channels/src/components/drafts/panel/__snapshots__/panel_body.test.tsx.snap index b3acbd9ab1..6361d83cc7 100644 --- a/webapp/channels/src/components/drafts/panel/__snapshots__/panel_body.test.tsx.snap +++ b/webapp/channels/src/components/drafts/panel/__snapshots__/panel_body.test.tsx.snap @@ -173,16 +173,6 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe } > { options={{ mentionHighlight: false, markdown: false, - autolinkedUrlSchemes: [], }} /> diff --git a/webapp/channels/src/components/post_view/embedded_bindings/embedded_binding/__snapshots__/embedded_binding.test.tsx.snap b/webapp/channels/src/components/post_view/embedded_bindings/embedded_binding/__snapshots__/embedded_binding.test.tsx.snap index c8219ffe04..2d0a43087e 100644 --- a/webapp/channels/src/components/post_view/embedded_bindings/embedded_binding/__snapshots__/embedded_binding.test.tsx.snap +++ b/webapp/channels/src/components/post_view/embedded_bindings/embedded_binding/__snapshots__/embedded_binding.test.tsx.snap @@ -18,7 +18,6 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn message="some text" options={ Object { - "autolinkedUrlSchemes": Array [], "mentionHighlight": false, "renderer": LinkOnlyRenderer { "options": Object {}, @@ -80,7 +79,6 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn message="some text" options={ Object { - "autolinkedUrlSchemes": Array [], "mentionHighlight": false, "renderer": LinkOnlyRenderer { "options": Object {}, @@ -142,7 +140,6 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn message="some text" options={ Object { - "autolinkedUrlSchemes": Array [], "mentionHighlight": false, "renderer": LinkOnlyRenderer { "options": Object {}, @@ -204,7 +201,6 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn message="some text" options={ Object { - "autolinkedUrlSchemes": Array [], "mentionHighlight": false, "renderer": LinkOnlyRenderer { "options": Object {}, diff --git a/webapp/channels/src/components/post_view/embedded_bindings/embedded_binding/embedded_binding.tsx b/webapp/channels/src/components/post_view/embedded_bindings/embedded_binding/embedded_binding.tsx index 6bfcd21052..4777f1fd7b 100644 --- a/webapp/channels/src/components/post_view/embedded_bindings/embedded_binding/embedded_binding.tsx +++ b/webapp/channels/src/components/post_view/embedded_bindings/embedded_binding/embedded_binding.tsx @@ -161,7 +161,6 @@ export default class EmbeddedBinding extends React.PureComponent { options={{ mentionHighlight: false, renderer: new LinkOnlyRenderer(), - autolinkedUrlSchemes: [], }} postId={this.props.post.id} /> diff --git a/webapp/channels/src/components/post_view/message_attachments/action_button/action_button.tsx b/webapp/channels/src/components/post_view/message_attachments/action_button/action_button.tsx index 1d811c0d8f..48db65c43a 100644 --- a/webapp/channels/src/components/post_view/message_attachments/action_button/action_button.tsx +++ b/webapp/channels/src/components/post_view/message_attachments/action_button/action_button.tsx @@ -26,7 +26,6 @@ const getStatusColors = (theme: Theme) => { const markdownOptions = { mentionHighlight: false, markdown: false, - autolinkedUrlSchemes: [], }; type Props = { diff --git a/webapp/channels/src/components/post_view/message_attachments/message_attachment/__snapshots__/message_attachment.test.tsx.snap b/webapp/channels/src/components/post_view/message_attachments/message_attachment/__snapshots__/message_attachment.test.tsx.snap index 2c5e8e5566..e3ff2fddd3 100644 --- a/webapp/channels/src/components/post_view/message_attachments/message_attachment/__snapshots__/message_attachment.test.tsx.snap +++ b/webapp/channels/src/components/post_view/message_attachments/message_attachment/__snapshots__/message_attachment.test.tsx.snap @@ -425,7 +425,6 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a options={ Object { "atMentions": false, - "autolinkedUrlSchemes": Array [], "mentionHighlight": false, "renderer": LinkOnlyRenderer { "options": Object {}, @@ -471,7 +470,6 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a options={ Object { "atMentions": false, - "autolinkedUrlSchemes": Array [], "mentionHighlight": false, "renderer": LinkOnlyRenderer { "options": Object {}, @@ -517,7 +515,6 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a options={ Object { "atMentions": false, - "autolinkedUrlSchemes": Array [], "mentionHighlight": false, "renderer": LinkOnlyRenderer { "options": Object {}, @@ -563,7 +560,6 @@ exports[`components/post_view/MessageAttachment should match snapshot when the f options={ Object { "atMentions": false, - "autolinkedUrlSchemes": Array [], "mentionHighlight": false, "renderer": LinkOnlyRenderer { "options": Object {}, diff --git a/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx b/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx index 8d7e51927a..cec6e10456 100644 --- a/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx +++ b/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx @@ -430,7 +430,6 @@ export default class MessageAttachment extends React.PureComponent atMentions: false, mentionHighlight: false, renderer: new LinkOnlyRenderer(), - autolinkedUrlSchemes: [], }} postId={this.props.postId} /> diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.test.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.test.ts index aa66c004c8..2423f49cd1 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.test.ts @@ -673,6 +673,13 @@ describe('Actions.Posts', () => { }), expected: new Set(['ccc', 'ddd', 'fff', 'ggg']), }, + { + name: 'should return potential remote mentions', + input: TestHelper.getPostMock({ + message: '@user1:org1 @user2:org2/@user3:org3/@user4:org4 (@user5:org5) @user6:org6', + }), + expected: new Set(['user1:org1', 'user2:org2', 'user3:org3', 'user4:org4', 'user5:org5', 'user6:org6']), + }, ]; for (const specialMention of [ diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts index c5df41d790..a3a8d7ce33 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts @@ -1098,7 +1098,7 @@ export function getNeededAtMentionedUsernamesAndGroups(state: GlobalState, posts groupsByName = getAllGroupsByName(state); } - const pattern = /\B@(([a-z0-9_.-]*[a-z0-9_])[.-]*)/gi; + const pattern = /\B@(([a-z0-9.\-_:]*[a-z0-9_])[.\-:]*)/gi; let match; while ((match = pattern.exec(text)) !== null) { diff --git a/webapp/channels/src/utils/__snapshots__/message_html_to_component.test.tsx.snap b/webapp/channels/src/utils/__snapshots__/message_html_to_component.test.tsx.snap index b83bf4a2f1..1a46e3c825 100644 --- a/webapp/channels/src/utils/__snapshots__/message_html_to_component.test.tsx.snap +++ b/webapp/channels/src/utils/__snapshots__/message_html_to_component.test.tsx.snap @@ -114,6 +114,42 @@ exports[`messageHtmlToComponent Inline markdown image where image is link 1`] = `; +exports[`messageHtmlToComponent Remote at mention 1`] = ` +

+ + + + @joram + + + +

+`; + +exports[`messageHtmlToComponent Remote at mention 2`] = ` +

+ + + @joram + + +

+`; + exports[`messageHtmlToComponent html 1`] = ` { + return (url: string) => { + const scheme = getScheme(url); + + return !scheme || autolinkedUrlSchemes.includes(scheme); + }; + }, +); + export function stripMarkdown(text: string) { if (typeof text === 'string' && text.length > 0) { return convertEntityToCharacter( diff --git a/webapp/channels/src/utils/markdown/renderer.tsx b/webapp/channels/src/utils/markdown/renderer.tsx index fe7de72037..ae642fb6d2 100644 --- a/webapp/channels/src/utils/markdown/renderer.tsx +++ b/webapp/channels/src/utils/markdown/renderer.tsx @@ -133,7 +133,7 @@ export default class Renderer extends marked.Renderer { return `${text}`; } - public link(href: string, title: string, text: string, isUrl = false) { + public link(href: string, title: string, text: string) { let outHref = href; if (this.formattingOptions.unsafeLinks && mightTriggerExternalRequest(href, this.formattingOptions.siteURL)) { @@ -147,15 +147,6 @@ export default class Renderer extends marked.Renderer { const scheme = getScheme(href); if (!scheme) { outHref = `http://${outHref}`; - } else if (isUrl && this.formattingOptions.autolinkedUrlSchemes) { - const isValidUrl = - this.formattingOptions.autolinkedUrlSchemes.indexOf( - scheme.toLowerCase(), - ) !== -1; - - if (!isValidUrl) { - return text; - } } } diff --git a/webapp/channels/src/utils/message_html_to_component.test.tsx b/webapp/channels/src/utils/message_html_to_component.test.tsx index c461fb5f2f..ce9b20ac5c 100644 --- a/webapp/channels/src/utils/message_html_to_component.test.tsx +++ b/webapp/channels/src/utils/message_html_to_component.test.tsx @@ -135,6 +135,23 @@ const myFunction = () => { expect(shallow(component).find(AtMention).prop('disableGroupHighlight')).toBe(true); }); + test('Remote at mention', () => { + const options = {mentionHighlight: true, atMentions: true, mentionKeys: [{key: '@joram'}]}; + let html = TextFormatting.formatText('@joram', options, emptyEmojiMap); + + let component = messageHtmlToComponent(html, {mentionHighlight: true}); + expect(component).toMatchSnapshot(); + expect(shallow(component).find(AtMention).prop('disableHighlight')).toBe(false); + + options.mentionHighlight = false; + + html = TextFormatting.formatText('@joram', options, emptyEmojiMap); + + component = messageHtmlToComponent(html, {mentionHighlight: false}); + expect(component).toMatchSnapshot(); + expect(shallow(component).find(AtMention).prop('disableHighlight')).toBe(true); + }); + test('typescript', () => { const input = `Text before typescript codeblock \`\`\`typescript diff --git a/webapp/channels/src/utils/post_utils.test.tsx b/webapp/channels/src/utils/post_utils.test.tsx index 769b8e8685..a6da7c2f0d 100644 --- a/webapp/channels/src/utils/post_utils.test.tsx +++ b/webapp/channels/src/utils/post_utils.test.tsx @@ -6,6 +6,7 @@ import {createIntl} from 'react-intl'; import {Preferences} from 'mattermost-redux/constants'; import enMessages from 'i18n/en.json'; +import {makeInitialState} from 'packages/mattermost-redux/test/test_store'; import {PostListRowListIds, Constants} from 'utils/constants'; import EmojiMap from 'utils/emoji_map'; import * as PostUtils from 'utils/post_utils'; @@ -1354,6 +1355,49 @@ describe('makeGetIsReactionAlreadyAddedToPost', () => { }); }); +describe('makeGetUserOrGroupMentionCountFromMessage', () => { + const baseState = makeInitialState({ + entities: { + groups: { + groups: { + group1: TestHelper.getGroupMock({id: 'group1', name: 'group.one', member_count: 4}), + }, + }, + users: { + profiles: { + remoteUser: TestHelper.getUserMock({id: 'remoteUser', username: 'remote.user:org1'}), + user1: TestHelper.getUserMock({id: 'user1', username: 'user.one'}), + user2: TestHelper.getUserMock({id: 'user2', username: 'user.two'}), + }, + }, + }, + }); + + test('should count mentioned users', () => { + const getUserOrGroupMentionCountFromMessage = PostUtils.makeGetUserOrGroupMentionCountFromMessage(); + + expect(getUserOrGroupMentionCountFromMessage(baseState, '@user.one @user.two Hello!')).toEqual(2); + }); + + test('should count mentioned groups', () => { + const getUserOrGroupMentionCountFromMessage = PostUtils.makeGetUserOrGroupMentionCountFromMessage(); + + expect(getUserOrGroupMentionCountFromMessage(baseState, '@group.one @user.one Hello!')).toEqual(5); + }); + + test('should count remote user mentions', () => { + const getUserOrGroupMentionCountFromMessage = PostUtils.makeGetUserOrGroupMentionCountFromMessage(); + + expect(getUserOrGroupMentionCountFromMessage(baseState, '@user.one @user.two @remote.user:org1 Hello!')).toEqual(3); + }); + + test('should not count non-existant users/groups', () => { + const getUserOrGroupMentionCountFromMessage = PostUtils.makeGetUserOrGroupMentionCountFromMessage(); + + expect(getUserOrGroupMentionCountFromMessage(baseState, '@not.user.three @fake.group @not.user:fake Hello!')).toEqual(0); + }); +}); + describe('makeGetUniqueEmojiNameReactionsForPost', () => { const baseState = { entities: { diff --git a/webapp/channels/src/utils/text_formatting.tsx b/webapp/channels/src/utils/text_formatting.tsx index 02e7c827fd..e78e0f909e 100644 --- a/webapp/channels/src/utils/text_formatting.tsx +++ b/webapp/channels/src/utils/text_formatting.tsx @@ -17,7 +17,6 @@ import * as Emoticons from './emoticons'; import * as Markdown from './markdown'; const punctuationRegex = /[^\p{L}\d]/u; -const AT_MENTION_PATTERN = /(?:\B|\b_+)@([a-z0-9.\-_]+)/gi; const UNICODE_EMOJI_REGEX = emojiRegex(); const htmlEmojiPattern = /^

\s*(?:]*>|]*>[^<]*<\/span>\s*|[^<]*<\/span>\s*)+<\/p>$/; @@ -175,13 +174,6 @@ export interface TextFormattingOptionsBase { */ proxyImages: boolean; - /** - * An array of url schemes that will be allowed for autolinking. - * - * Defaults to autolinking with any url scheme. - */ - autolinkedUrlSchemes: string[]; - /** * An array of paths on the server that are managed by another server. Any path provided will be treated as an * external link that will not by handled by react-router. @@ -551,17 +543,17 @@ export function autolinkAtMentions(text: string, tokens: Tokens): string { ); // handle all other mentions (supports trailing punctuation) - let match = output.match(AT_MENTION_PATTERN); + let match = output.match(Constants.MENTIONS_REGEX); while (match && match.length > 0) { - output = output.replace(AT_MENTION_PATTERN, replaceAtMentionWithToken); - match = output.match(AT_MENTION_PATTERN); + output = output.replace(Constants.MENTIONS_REGEX, replaceAtMentionWithToken); + match = output.match(Constants.MENTIONS_REGEX); } return output; } export function allAtMentions(text: string): string[] { - return text.match(Constants.SPECIAL_MENTIONS_REGEX && AT_MENTION_PATTERN) || []; + return text.match(Constants.SPECIAL_MENTIONS_REGEX && Constants.MENTIONS_REGEX) || []; } export function autolinkChannelMentions( diff --git a/webapp/channels/src/utils/text_formatting_at_mentions.test.ts b/webapp/channels/src/utils/text_formatting_at_mentions.test.ts index d22cae8db4..81e262e5c2 100644 --- a/webapp/channels/src/utils/text_formatting_at_mentions.test.ts +++ b/webapp/channels/src/utils/text_formatting_at_mentions.test.ts @@ -147,4 +147,17 @@ describe('TextFormatting.AtMentions', () => { it(test.label, () => expect(test.actual).toBe(test.expected)); }); }); + + test('MM-62744 should recognize remote mentions', () => { + expect(TextFormatting.formatText( + '@user1:org1 @user2:org2/@user3:org3/@user4:org4 (@user5:org5) @user6:org6', + {atMentions: true}, + emptyEmojiMap, + )).toEqual( + '

@user1:org1 ' + + '@user2:org2/@user3:org3/@user4:org4 ' + + '(@user5:org5) ' + + '@user6:org6

', + ); + }); }); diff --git a/webapp/channels/src/utils/text_formatting_links.test.ts b/webapp/channels/src/utils/text_formatting_links.test.ts index 0051b64ba6..e51fa81b00 100644 --- a/webapp/channels/src/utils/text_formatting_links.test.ts +++ b/webapp/channels/src/utils/text_formatting_links.test.ts @@ -1,6 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import store from 'stores/redux_store'; + +import {makeInitialState} from 'packages/mattermost-redux/test/test_store'; import EmojiMap from 'utils/emoji_map'; import * as Markdown from 'utils/markdown'; import * as TextFormatting from 'utils/text_formatting'; @@ -41,10 +44,6 @@ describe('Markdown.Links', () => { }); it('External links', () => { - expect(Markdown.format('test.:test').trim()).toBe( - '

test.:test

', - ); - expect(Markdown.format('http://example.com').trim()).toBe( '

http://example.com

', ); @@ -402,7 +401,18 @@ describe('Markdown.Links', () => { }); describe('autolinkedUrlSchemes', () => { - test('all links are rendered when not provided', () => { + test('only some types of links are rendered when there are custom URL schemes defined', () => { + jest.spyOn(store, 'getState').mockReturnValue(makeInitialState({ + entities: { + general: { + config: { + CustomUrlSchemes: '', + }, + }, + }, + })); + + // These are always linked expect(Markdown.format('http://example.com').trim()).toBe(`

${link('http://example.com')}

`); expect(Markdown.format('https://example.com').trim()).toBe(`

${link('https://example.com')}

`); @@ -413,67 +423,68 @@ describe('Markdown.Links', () => { expect(Markdown.format('mailto:test@example.com').trim()).toBe(`

${link('mailto:test@example.com')}

`); + // These aren't linked since they're not configured on the server + expect(Markdown.format('git://git.example.com').trim()).toBe('

git://git.example.com

'); + + expect(Markdown.format('test:test').trim()).toBe('

test:test

'); + + expect(Markdown.format('test.:test').trim()).toBe('

test.:test

'); + + expect(Markdown.format('taco+what://example.com').trim()).toBe('

taco+what://example.com

'); + + expect(Markdown.format('taco.what://example.com').trim()).toBe('

taco.what://example.com

'); + }); + + test('matching links are rendered when schemes are provided', () => { + jest.spyOn(store, 'getState').mockReturnValue(makeInitialState({ + entities: { + general: { + config: { + CustomUrlSchemes: 'git,test,test.,taco+what,taco.what', + }, + }, + }, + })); + + // These are always linked + expect(Markdown.format('http://example.com').trim()).toBe(`

${link('http://example.com')}

`); + + expect(Markdown.format('https://example.com').trim()).toBe(`

${link('https://example.com')}

`); + + expect(Markdown.format('ftp://ftp.example.com').trim()).toBe(`

${link('ftp://ftp.example.com')}

`); + + expect(Markdown.format('tel:1-555-123-4567').trim()).toBe(`

${link('tel:1-555-123-4567')}

`); + + expect(Markdown.format('mailto:test@example.com').trim()).toBe(`

${link('mailto:test@example.com')}

`); + + // These are linked since they're configured on the server expect(Markdown.format('git://git.example.com').trim()).toBe(`

${link('git://git.example.com')}

`); expect(Markdown.format('test:test').trim()).toBe(`

${link('test:test')}

`); - }); - test('no links are rendered when no schemes are provided', () => { - const options = { - autolinkedUrlSchemes: [], - }; + expect(Markdown.format('test.:test').trim()).toBe(`

${link('test.:test')}

`); - expect(Markdown.format('http://example.com', options).trim()).toBe('

http://example.com

'); + expect(Markdown.format('taco+what://example.com').trim()).toBe(`

${link('taco+what://example.com')}

`); - expect(Markdown.format('https://example.com', options).trim()).toBe('

https://example.com

'); - - expect(Markdown.format('ftp://ftp.example.com', options).trim()).toBe('

ftp://ftp.example.com

'); - - expect(Markdown.format('tel:1-555-123-4567', options).trim()).toBe('

tel:1-555-123-4567

'); - - expect(Markdown.format('mailto:test@example.com', options).trim()).toBe('

mailto:test@example.com

'); - - expect(Markdown.format('git://git.example.com', options).trim()).toBe('

git://git.example.com

'); - - expect(Markdown.format('test:test', options).trim()).toBe('

test:test

'); - }); - - test('only matching links are rendered when schemes are provided', () => { - const options = { - autolinkedUrlSchemes: ['https', 'git', 'test', 'test.', 'taco+what', 'taco.what'], - }; - - expect(Markdown.format('http://example.com', options).trim()).toBe('

http://example.com

'); - - expect(Markdown.format('https://example.com', options).trim()).toBe(`

${link('https://example.com')}

`); - - expect(Markdown.format('ftp://ftp.example.com', options).trim()).toBe('

ftp://ftp.example.com

'); - - expect(Markdown.format('tel:1-555-123-4567', options).trim()).toBe('

tel:1-555-123-4567

'); - - expect(Markdown.format('mailto:test@example.com', options).trim()).toBe('

mailto:test@example.com

'); - - expect(Markdown.format('git://git.example.com', options).trim()).toBe(`

${link('git://git.example.com')}

`); - - expect(Markdown.format('test:test', options).trim()).toBe(`

${link('test:test')}

`); - - expect(Markdown.format('test.:test', options).trim()).toBe(`

${link('test.:test')}

`); - - expect(Markdown.format('taco+what://example.com', options).trim()).toBe(`

${link('taco+what://example.com')}

`); - - expect(Markdown.format('taco.what://example.com', options).trim()).toBe(`

${link('taco.what://example.com')}

`); + expect(Markdown.format('taco.what://example.com').trim()).toBe(`

${link('taco.what://example.com')}

`); }); test('explicit links are not affected by this setting', () => { - const options = { - autolinkedUrlSchemes: [], - }; + jest.spyOn(store, 'getState').mockReturnValue(makeInitialState({ + entities: { + general: { + config: { + CustomUrlSchemes: '', + }, + }, + }, + })); - expect(Markdown.format('www.example.com', options).trim()).toBe(`

${link('http://www.example.com', 'www.example.com')}

`); + expect(Markdown.format('www.example.com').trim()).toBe(`

${link('http://www.example.com', 'www.example.com')}

`); - expect(Markdown.format('[link](git://git.example.com)', options).trim()).toBe(`

${link('git://git.example.com', 'link')}

`); + expect(Markdown.format('[link](git://git.example.com)').trim()).toBe(`

${link('git://git.example.com', 'link')}

`); - expect(Markdown.format('', options).trim()).toBe(`

${link('http://example.com')}

`); + expect(Markdown.format('').trim()).toBe(`

${link('git://git.example.com')}

`); }); }); }); diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 30335c62dc..4e6e30c49d 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -105,7 +105,7 @@ "lodash": "4.17.21", "luxon": "3.6.1", "mark.js": "8.11.1", - "marked": "github:mattermost/marked#3b13ba8ddf725327ddf0298361d6d304a021f2d1", + "marked": "github:mattermost/marked#08f3638e37e17738fafcaf749683ce6fee1d8edc", "memoize-one": "6.0.0", "moment-timezone": "0.5.38", "monaco-editor": "0.52.2", @@ -20662,8 +20662,8 @@ }, "node_modules/marked": { "version": "0.3.6", - "resolved": "git+ssh://git@github.com/mattermost/marked.git#3b13ba8ddf725327ddf0298361d6d304a021f2d1", - "integrity": "sha512-/G1/szBhwdamaCQQjdk+eKQKw4Xkmk8aQYxEQvIjfbcFHEaIgUbpH6RBSC+b0pDzlhm7mxHAaGnzfH3WqUks1Q==", + "resolved": "git+ssh://git@github.com/mattermost/marked.git#08f3638e37e17738fafcaf749683ce6fee1d8edc", + "integrity": "sha512-zaU3iMz9BuvdYUjAF4zL0ioAstBgFI+xUgO5BNaEDGI+cXoXcRa5X2Bq0uA7Xn6NSQi0X04Xn5EuH+pxhvIllQ==", "license": "MIT", "bin": { "marked": "bin/marked"