diff --git a/webapp/channels/src/components/common/hooks/use_external_link.test.ts b/webapp/channels/src/components/common/hooks/use_external_link.test.ts index d1688512f61..fbd99872e12 100644 --- a/webapp/channels/src/components/common/hooks/use_external_link.test.ts +++ b/webapp/channels/src/components/common/hooks/use_external_link.test.ts @@ -136,9 +136,31 @@ describe('useExternalLink', () => { expect(firstHref).toBe(secondHref); expect(firstParams).toBe(secondParams); }); + it('do not substitute %20 on query params', () => { const url = 'https://www.mattermost.com/some/url?subject=hello%20world'; const {result: {current: [href]}} = renderHookWithContext(() => useExternalLink(url), getBaseState()); expect(href).toContain('subject=hello%20world'); }); + + it('do not error on invalid URLs', () => { + const invalidUrl = 'not a valid url'; + const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(invalidUrl), getBaseState()); + expect(href).toBe(invalidUrl); + expect(queryParams).toEqual({}); + }); + + it('do not modify arbitrary links that happen to include mattermost.com', () => { + const invalidUrl = 'https://example.com/mattermost.com'; + const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(invalidUrl), getBaseState()); + expect(href).toBe(invalidUrl); + expect(queryParams).toEqual({}); + }); + + it('do not modify mailto links on mattermost.com', () => { + const mailtoUrl = 'mailto:support@mattermost.com'; + const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(mailtoUrl), getBaseState()); + expect(href).toBe(mailtoUrl); + expect(queryParams).toEqual({}); + }); }); diff --git a/webapp/channels/src/components/common/hooks/use_external_link.ts b/webapp/channels/src/components/common/hooks/use_external_link.ts index 4e980a60cc6..b05606ea0cc 100644 --- a/webapp/channels/src/components/common/hooks/use_external_link.ts +++ b/webapp/channels/src/components/common/hooks/use_external_link.ts @@ -17,6 +17,17 @@ export type ExternalLinkQueryParams = { userId?: string; } +/** + * useExternalLink is used when linking outside of the MM server to add extra tracking parameters when linking to any + * page on mattermost.com (such as our docs or marketing websites). When passed any URL that isn't on mattermost.com, + * it returns the original URL unmodified. + * + * @param href The external URL being linked to + * @param location The location of the link within the app + * @param overwriteQueryParams + * @return {[string, Record]} A tuple containing the URL (whether or not it was modified) and all query + * parameters on that link (either pre-existing or added by this hook) + */ export function useExternalLink(href: string, location: string = '', overwriteQueryParams: ExternalLinkQueryParams = {}): [string, Record] { const userId = useSelector(getCurrentUserId); const config = useSelector(getConfig); @@ -28,11 +39,20 @@ export function useExternalLink(href: string, location: string = '', overwriteQu const isCloud = useSelector((state: GlobalState) => getLicense(state)?.Cloud === 'true'); return useMemo(() => { - if (!href?.includes('mattermost.com') || href?.startsWith('mailto:')) { + let parsedUrl; + try { + parsedUrl = new URL(href); + } catch { return [href, {}]; } - const parsedUrl = new URL(href); + if (parsedUrl.hostname !== 'mattermost.com' && !parsedUrl.hostname.endsWith('.mattermost.com')) { + return [href, {}]; + } + + if (parsedUrl.protocol === 'mailto:') { + return [href, {}]; + } // Determine edition type (enterprise vs team) const isEnterpriseReady = config?.BuildEnterpriseReady === 'true';