diff --git a/app/actions/remote/command.ts b/app/actions/remote/command.ts index 9360f62b5..93826f502 100644 --- a/app/actions/remote/command.ts +++ b/app/actions/remote/command.ts @@ -143,7 +143,7 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc const match = matchDeepLink(location, serverUrl, config?.SiteURL); if (match) { - handleDeepLink(match, intl, location); + handleDeepLink(match.url, intl, location); } else { const {formatMessage} = intl; const onError = () => Alert.alert( diff --git a/app/components/markdown/markdown_link/markdown_link.tsx b/app/components/markdown/markdown_link/markdown_link.tsx index 4cc662260..2b65bdc3a 100644 --- a/app/components/markdown/markdown_link/markdown_link.tsx +++ b/app/components/markdown/markdown_link/markdown_link.tsx @@ -77,9 +77,9 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU const match = matchDeepLink(url, serverUrl, siteURL); if (match) { - const {error} = await handleDeepLink(match, intl); + const {error} = await handleDeepLink(match.url, intl); if (error) { - tryOpenURL(match, onError); + tryOpenURL(match.url, onError); } } else { tryOpenURL(url, onError); diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 29c6d6368..336075c6b 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -8,6 +8,7 @@ import urlParse from 'url-parse'; import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel'; import {showPermalink} from '@actions/remote/permalink'; import {fetchUsersByUsernames} from '@actions/remote/user'; +import DeepLinkType from '@app/constants/deep_linking'; import {DeepLink, Launch, Screens} from '@constants'; import {getDefaultThemeByAppearance} from '@context/theme'; import DatabaseManager from '@database/manager'; @@ -29,6 +30,8 @@ import { PLUGIN_ID_PATH_PATTERN, } from '@utils/url/path'; +import {removeProtocol} from '../url'; + import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch'; import type {AvailableScreens} from '@typings/screens/navigation'; @@ -124,35 +127,31 @@ const matcherOpts: Parameters[1] = {decode: decodeURIComponent}; type ChannelPathParams = { hostname: string; - subpath?: string; + serverUrl: string; teamName: string; path: 'channels' | 'messages'; identifier: string; postId?: string; }; -const CHANNEL_PATH = `/:subpath*/:teamName(${TEAM_NAME_PATH_PATTERN})/:path(channels|messages)/:identifier(${IDENTIFIER_PATH_PATTERN})/:postId(${ID_PATH_PATTERN})?`; +const CHANNEL_PATH = `:serverUrl(.*)/:teamName(${TEAM_NAME_PATH_PATTERN})/:path(channels|messages)/:identifier(${IDENTIFIER_PATH_PATTERN})/:postId(${ID_PATH_PATTERN})?`; export const matchChannelDeeplink = match(CHANNEL_PATH, matcherOpts); type PermalinkPathParams = { - hostname: string; - subpath?: string; + serverUrl: string; teamName: string; postId: string; }; -const PERMALINK_PATH = `/:subpath*/:teamName(${TEAM_NAME_PATH_PATTERN})/pl/:postId(${ID_PATH_PATTERN})`; +const PERMALINK_PATH = `:serverUrl(.*)/:teamName(${TEAM_NAME_PATH_PATTERN})/pl/:postId(${ID_PATH_PATTERN})`; export const matchPermalinkDeeplink = match(PERMALINK_PATH, matcherOpts); -const getServerUrl = (host: string, subpath?: string) => host + (subpath ? '/' + subpath : ''); - export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { try { - const {host, pathname} = urlParse(deepLinkUrl); + const url = removeProtocol(deepLinkUrl); - const channelMatch = matchChannelDeeplink(pathname); + const channelMatch = matchChannelDeeplink(url); if (channelMatch) { - const {params: {subpath, teamName, path, identifier, postId}} = channelMatch; - const serverUrl = getServerUrl(host, subpath); + const {params: {serverUrl, teamName, path, identifier, postId}} = channelMatch; if (path === 'channels') { return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier, postId}}; @@ -160,43 +159,39 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { if (path === 'messages') { if (identifier.startsWith('@')) { - return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl, teamName, userName: identifier}}; + return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl, teamName, userName: identifier.substring(1)}}; } return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl, teamName, channelId: identifier}}; } } - const permalinkMatch = matchPermalinkDeeplink(pathname); + const permalinkMatch = matchPermalinkDeeplink(url); if (permalinkMatch) { - const {params: {subpath, teamName, postId}} = permalinkMatch; - const serverUrl = getServerUrl(host, subpath); - + const {params: {serverUrl, teamName, postId}} = permalinkMatch; return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl, teamName, postId}}; } - const pluginMatch = match<{subpath?: string; id: string}>(`/:subpath*/plugins/:id(${PLUGIN_ID_PATH_PATTERN})`, matcherOpts)(pathname); + const pluginMatch = match<{serverUrl: string; id: string}>(`:serverUrl(.*)/plugins/:id(${PLUGIN_ID_PATH_PATTERN})`, matcherOpts)(url); if (pluginMatch) { - const {params: {subpath, id}} = pluginMatch; - const serverUrl = getServerUrl(host, subpath); + const {params: {serverUrl, id}} = pluginMatch; return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl, teamName: '', id}}; } - } catch { + } catch (err) { // do nothing just return invalid deeplink } return {type: DeepLink.Invalid, url: deepLinkUrl}; } -export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) { +export function matchDeepLink(url: string, serverURL?: string, siteURL?: string) { if (!url || (!serverURL && !siteURL)) { - return ''; + return null; } let urlToMatch = url; const urlBase = serverURL || siteURL || ''; const parsedUrl = urlParse(url); - const parsedBase = urlParse(urlBase); if (!parsedUrl.protocol) { // If url doesn't contain site or server URL, tack it on. @@ -207,22 +202,13 @@ export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string } } - const finalUrl = urlParse(urlToMatch); - const baseSubpath = parsedBase.pathname.replace('/', ''); - const baseHostname = parsedBase.hostname; - const urlSubpath = finalUrl.pathname.split('/')[1]; - const urlHostname = finalUrl.hostname; + const parsed = parseDeepLink(urlToMatch); - if (baseSubpath) { - // if the server is in a subpath - if (urlHostname === baseHostname && urlSubpath === baseSubpath) { - return urlToMatch; - } - } else if (urlHostname === baseHostname) { - return urlToMatch; + if (parsed.type === DeepLinkType.Invalid) { + return null; } - return ''; + return parsed; } export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => { diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index 6db8531d4..c24280946 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -5,7 +5,7 @@ import {Linking} from 'react-native'; import DeepLinkType from '@constants/deep_linking'; import TestHelper from '@test/test_helper'; -import {matchDeepLink, parseDeepLink} from '@utils/deep_link'; +import {matchDeepLink} from '@utils/deep_link'; import * as UrlUtils from '@utils/url'; /* eslint-disable max-nested-callbacks */ @@ -135,26 +135,29 @@ describe('UrlUtils', () => { const SERVER_WITH_SUBPATH = `http://${URL_PATH_NO_PROTOCOL}`; const DEEPLINK_URL_ROOT = `mattermost://${URL_NO_PROTOCOL}`; + const DM_USER = TestHelper.fakeUserWithId(); + const GM_CHANNEL_NAME = '4862db64e76a321d167fe6677f16e96e9275dabe'; + const tests = [ { name: 'should return null if all inputs are empty', input: {url: '', serverURL: '', siteURL: ''}, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null if any of the input is null', input: {url: '', serverURL: '', siteURL: null}, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null if any of the input is null', input: {url: '', serverURL: null, siteURL: ''}, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null if any of the input is null', input: {url: null, serverURL: '', siteURL: ''}, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null for not supported link', @@ -163,12 +166,12 @@ describe('UrlUtils', () => { serverURL: SERVER_URL, siteURL: SITE_URL, }, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null despite url subset match', input: {url: 'http://myserver.com', serverURL: 'http://myserver.co'}, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should match despite no server URL in input link', @@ -193,7 +196,7 @@ describe('UrlUtils', () => { serverURL: SERVER_URL, siteURL: SITE_URL, }, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null for backslash-invalid deeplink', @@ -202,7 +205,7 @@ describe('UrlUtils', () => { serverURL: SERVER_URL, siteURL: SITE_URL, }, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null for backslash-invalid-alt deeplink', @@ -211,7 +214,7 @@ describe('UrlUtils', () => { serverURL: SERVER_URL, siteURL: SITE_URL, }, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null for double encoded invalid deeplink', @@ -220,7 +223,7 @@ describe('UrlUtils', () => { serverURL: SERVER_URL, siteURL: SITE_URL, }, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should return null for double encoded backslash-invalid deeplink', @@ -229,7 +232,7 @@ describe('UrlUtils', () => { serverURL: SERVER_URL, siteURL: SITE_URL, }, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, { name: 'should match channel link', @@ -296,9 +299,41 @@ describe('UrlUtils', () => { }, }, { - name: 'should match channel link (team name: channels, channel name: messages) with deeplink prefix', + name: 'should match DM channel link with deeplink prefix', input: { - url: DEEPLINK_URL_ROOT + '/channels/channels/messages', + url: DEEPLINK_URL_ROOT + `/pl/messages/@${DM_USER.username}`, + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: { + data: { + userName: DM_USER.username, + serverUrl: URL_NO_PROTOCOL, + teamName: 'pl', + }, + type: DeepLinkType.DirectMessage, + }, + }, + { + name: 'should match GM channel link with deeplink prefix', + input: { + url: DEEPLINK_URL_ROOT + `/pl/messages/${GM_CHANNEL_NAME}`, + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: { + data: { + channelId: GM_CHANNEL_NAME, + serverUrl: URL_NO_PROTOCOL, + teamName: 'pl', + }, + type: DeepLinkType.GroupMessage, + }, + }, + { + name: 'should match channel link (team name: pl, channel name: messages) with deeplink prefix', + input: { + url: DEEPLINK_URL_ROOT + '/pl/channels/messages', serverURL: SERVER_URL, siteURL: SITE_URL, }, @@ -306,7 +341,7 @@ describe('UrlUtils', () => { data: { channelName: 'messages', serverUrl: URL_NO_PROTOCOL, - teamName: 'channels', + teamName: 'pl', }, type: DeepLinkType.Channel, }, @@ -328,9 +363,9 @@ describe('UrlUtils', () => { }, }, { - name: 'should match permalink (team name: channels) with deeplink prefix on a Server hosted in a Subpath', + name: 'should match permalink (team name: pl) with deeplink prefix on a Server hosted in a Subpath', input: { - url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/channels/pl/qe93kkfd7783iqwuwfcwcxbsrr', + url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/pl/pl/qe93kkfd7783iqwuwfcwcxbsrr', serverURL: SERVER_WITH_SUBPATH, siteURL: SERVER_WITH_SUBPATH, }, @@ -338,7 +373,7 @@ describe('UrlUtils', () => { data: { postId: 'qe93kkfd7783iqwuwfcwcxbsrr', serverUrl: URL_PATH_NO_PROTOCOL, - teamName: 'channels', + teamName: 'pl', }, type: DeepLinkType.Permalink, }, @@ -366,7 +401,7 @@ describe('UrlUtils', () => { serverURL: SERVER_WITH_SUBPATH, siteURL: SERVER_WITH_SUBPATH, }, - expected: {type: DeepLinkType.Invalid}, + expected: null, }, ]; @@ -374,10 +409,11 @@ describe('UrlUtils', () => { const {name, input, expected} = test; it(name, () => { - const match = matchDeepLink(input.url!, input.serverURL!, input.siteURL!); - const parsed = parseDeepLink(match); - Reflect.deleteProperty(parsed, 'url'); - expect(parsed).toEqual(expected); + const matched = matchDeepLink(input.url!, input.serverURL!, input.siteURL!); + if (matched) { + Reflect.deleteProperty(matched, 'url'); + } + expect(matched).toEqual(expected); }); } });