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 487644d6138..d60b98f44d8 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 b3acbd9ab14..6361d83cc7a 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 c8219ffe048..2d0a43087e6 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 6bfcd21052c..4777f1fd7b1 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 1d811c0d8f1..48db65c43a2 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 2c5e8e55661..e3ff2fddd31 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 8d7e51927a3..cec6e10456c 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 aa66c004c8d..2423f49cd11 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 c5df41d790e..a3a8d7ce338 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 b83bf4a2f10..1a46e3c8257 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 fe7de72037c..ae642fb6d24 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 c461fb5f2fa..ce9b20ac5c6 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 769b8e8685a..a6da7c2f0da 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 02e7c827fde..e78e0f909e0 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 d22cae8db4b..81e262e5c27 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 0051b64ba67..e51fa81b004 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 30335c62dcf..4e6e30c49d0 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"