From fc7d8a3d081d3a2acb0b975aa25fa72812b9d28a Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Wed, 18 Oct 2023 16:56:14 -0500 Subject: [PATCH 01/12] include encoded --- app/utils/deep_link/index.ts | 7 ++++++- app/utils/url/test.ts | 38 +++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 7b2bb982d..bfa9e18ac 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -118,7 +118,12 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { try { const url = removeProtocol(decodeURIComponent(deepLinkUrl)); - if (url.includes('../') || url.includes('/..')) { + if ( + url.includes('../') || + url.includes('/..') || + url.includes('..%2f') || + url.includes('%2f..') + ) { return {type: DeepLink.Invalid, url: deepLinkUrl}; } diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index f7057e1f4..c47d5ee64 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -139,22 +139,22 @@ describe('UrlUtils', () => { { name: 'should return null if all inputs are empty', input: {url: '', serverURL: '', siteURL: ''}, - expected: {type: 'invalid'}, + expected: {type: DeepLinkType.Invalid}, }, { name: 'should return null if any of the input is null', input: {url: '', serverURL: '', siteURL: null}, - expected: {type: 'invalid'}, + expected: {type: DeepLinkType.Invalid}, }, { name: 'should return null if any of the input is null', input: {url: '', serverURL: null, siteURL: ''}, - expected: {type: 'invalid'}, + expected: {type: DeepLinkType.Invalid}, }, { name: 'should return null if any of the input is null', input: {url: null, serverURL: '', siteURL: ''}, - expected: {type: 'invalid'}, + expected: {type: DeepLinkType.Invalid}, }, { name: 'should return null for not supported link', @@ -163,12 +163,12 @@ describe('UrlUtils', () => { serverURL: SERVER_URL, siteURL: SITE_URL, }, - expected: {type: 'invalid'}, + expected: {type: DeepLinkType.Invalid}, }, { name: 'should return null despite url subset match', input: {url: 'http://myserver.com', serverURL: 'http://myserver.co'}, - expected: {type: 'invalid'}, + expected: {type: DeepLinkType.Invalid}, }, { name: 'should match despite no server URL in input link', @@ -186,6 +186,24 @@ describe('UrlUtils', () => { type: DeepLinkType.Permalink, }, }, + { + name: 'should return null for invalid deeplink', + input: { + url: DEEPLINK_URL_ROOT + '/ad-1/channels/../town-square', + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: {type: DeepLinkType.Invalid}, + }, + { + name: 'should return null for encoded invalid deeplink', + input: { + url: DEEPLINK_URL_ROOT + '/ad-1/channels/%252f..%252ftown-square', + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: {type: DeepLinkType.Invalid}, + }, { name: 'should match channel link', input: { @@ -247,7 +265,7 @@ describe('UrlUtils', () => { serverUrl: URL_PATH_NO_PROTOCOL, teamName: 'ad-1', }, - type: 'permalink', + type: DeepLinkType.Permalink, }, }, { @@ -263,7 +281,7 @@ describe('UrlUtils', () => { serverUrl: URL_PATH_NO_PROTOCOL, teamName: 'ad-1', }, - type: 'permalink', + type: DeepLinkType.Permalink, }, }, { @@ -273,9 +291,7 @@ describe('UrlUtils', () => { serverURL: SERVER_WITH_SUBPATH, siteURL: SERVER_WITH_SUBPATH, }, - expected: { - type: 'invalid', - }, + expected: {type: DeepLinkType.Invalid}, }, ]; From 03ccfda5148e36c201c38cd8a994da096dac25dd Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Mon, 23 Oct 2023 17:25:28 -0500 Subject: [PATCH 02/12] better support variations --- app/utils/deep_link/index.ts | 12 ++++++------ app/utils/url/test.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index bfa9e18ac..1776e75ce 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -118,12 +118,7 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { try { const url = removeProtocol(decodeURIComponent(deepLinkUrl)); - if ( - url.includes('../') || - url.includes('/..') || - url.includes('..%2f') || - url.includes('%2f..') - ) { + if (isDeepLinkInvalid(url)) { return {type: DeepLink.Invalid, url: deepLinkUrl}; } @@ -158,6 +153,11 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { return {type: DeepLink.Invalid, url: deepLinkUrl}; } +function isDeepLinkInvalid(decodedUrl: string) { + const url = decodeURIComponent(decodedUrl); + return url.includes('../') || url.includes('/..'); +} + export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) { if (!url || (!serverURL && !siteURL)) { return ''; diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index c47d5ee64..c5e526961 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -196,9 +196,9 @@ describe('UrlUtils', () => { expected: {type: DeepLinkType.Invalid}, }, { - name: 'should return null for encoded invalid deeplink', + name: 'should return null for double encoded invalid deeplink', input: { - url: DEEPLINK_URL_ROOT + '/ad-1/channels/%252f..%252ftown-square', + url: DEEPLINK_URL_ROOT + '/ad-1/channels/%252f%252e.town-square', serverURL: SERVER_URL, siteURL: SITE_URL, }, From 744d5c11203c5602c76850752ccf8efcbc8e6e41 Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Fri, 3 Nov 2023 14:46:00 -0500 Subject: [PATCH 03/12] include backslashes --- app/utils/deep_link/index.ts | 2 +- app/utils/url/test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 1776e75ce..41abb8e20 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -155,7 +155,7 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { function isDeepLinkInvalid(decodedUrl: string) { const url = decodeURIComponent(decodedUrl); - return url.includes('../') || url.includes('/..'); + return Boolean((/([/\\]\.\.|\.\.[/\\])/).exec(url)?.length); } export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) { diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index c5e526961..ea0210586 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -195,6 +195,24 @@ describe('UrlUtils', () => { }, expected: {type: DeepLinkType.Invalid}, }, + { + name: 'should return null for backslash-invalid deeplink', + input: { + url: DEEPLINK_URL_ROOT + '/ad-1/channels/\\..town-square', + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: {type: DeepLinkType.Invalid}, + }, + { + name: 'should return null for backslash-invalid-alt deeplink', + input: { + url: DEEPLINK_URL_ROOT + '/ad-1/channels/t..\\town-square', + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: {type: DeepLinkType.Invalid}, + }, { name: 'should return null for double encoded invalid deeplink', input: { @@ -204,6 +222,15 @@ describe('UrlUtils', () => { }, expected: {type: DeepLinkType.Invalid}, }, + { + name: 'should return null for double encoded backslash-invalid deeplink', + input: { + url: DEEPLINK_URL_ROOT + '/ad-1/channels/%255C%252e.town-square', + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: {type: DeepLinkType.Invalid}, + }, { name: 'should match channel link', input: { From bf25f10c382fd65519d0bb8c270d98e81606db5a Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Wed, 15 Nov 2023 21:24:48 -0600 Subject: [PATCH 04/12] refactor parseDeepLink with path-to-regexp --- app/utils/deep_link/index.ts | 100 ++++++++++++++++++++++------------- app/utils/url/path.ts | 20 +++++++ package-lock.json | 11 ++++ package.json | 1 + 4 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 app/utils/url/path.ts diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 41abb8e20..5b77d6ed0 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {match} from 'path-to-regexp'; import {createIntl, type IntlShape} from 'react-intl'; import urlParse from 'url-parse'; @@ -21,7 +22,12 @@ import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '@utils/d import {logError} from '@utils/log'; import {escapeRegex} from '@utils/markdown'; import {addNewServer} from '@utils/server'; -import {removeProtocol} from '@utils/url'; +import { + TEAM_NAME_PATH_PATTERN, + IDENTIFIER_PATH_PATTERN, + ID_PATH_PATTERN, + PLUGIN_ID_PATH_PATTERN, +} from '@utils/url/path'; import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch'; import type {AvailableScreens} from '@typings/screens/navigation'; @@ -100,12 +106,6 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, showPermalink(existingServerUrl, deepLinkData.teamName, deepLinkData.postId); break; } - case DeepLink.Plugin: { - // https://mattermost.atlassian.net/browse/MM-49846 - // const deepLinkData = parsed.data as DeepLinkPlugin; - // showModal('PluginInternal', deepLinkData.id, {link: location}); - break; - } } return {error: false}; } catch (error) { @@ -114,37 +114,66 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, } } +const matcherOpts: Parameters[1] = {decode: decodeURIComponent}; + +type ChannelPathParams = { + hostname: string; + subpath?: 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}|${ID_PATH_PATTERN})/:postId(${ID_PATH_PATTERN})?`; +export const matchChannelDeeplink = match(CHANNEL_PATH, matcherOpts); + +type PermalinkPathParams = { + hostname: string; + subpath?: string; + teamName: string; + postId: string; +}; +const PERMALINK_PATH = `/:subpath?/: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 url = removeProtocol(decodeURIComponent(deepLinkUrl)); + const {host, pathname} = urlParse(decodeURIComponent(deepLinkUrl)); + const channelMatch = matchChannelDeeplink(pathname); - if (isDeepLinkInvalid(url)) { - return {type: DeepLink.Invalid, url: deepLinkUrl}; + if (channelMatch) { + const {params: {subpath, teamName, path, identifier, postId}} = channelMatch; + const serverUrl = getServerUrl(host, subpath); + + if (path === 'channels') { + return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier, postId}}; + } + + if (path === 'messages') { + if (identifier.startsWith('@')) { + return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl, teamName, userName: identifier}}; + } + + return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl, teamName, channelId: identifier}}; + } } - let match = new RegExp('(.*)\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url); - if (match) { - return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelName: match[3]}}; + const permalinkMatch = matchPermalinkDeeplink(pathname); + if (permalinkMatch) { + const {params: {subpath, teamName, postId}} = permalinkMatch; + const serverUrl = getServerUrl(host, subpath); + + return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl, teamName, postId}}; } - match = new RegExp('(.*)\\/([^\\/]+)\\/pl\\/(\\w+)').exec(url); - if (match) { - return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], postId: match[3]}}; - } - - match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(url); - if (match) { - return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], userName: match[3]}}; - } - - match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/(\\S+)').exec(url); - if (match) { - return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelId: match[3]}}; - } - - match = new RegExp('(.*)\\/plugins\\/([^\\/]+)\\/(\\S+)').exec(url); - if (match) { - return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl: match[1], id: match[2], teamName: ''}}; + const pluginMatch = match<{subpath?: string; id: string}>(`/:subpath?/plugins/:id(${PLUGIN_ID_PATH_PATTERN})`, matcherOpts)(pathname); + if (pluginMatch) { + const {params: {subpath, id}} = pluginMatch; + const serverUrl = getServerUrl(host, subpath); + return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl, teamName: '', id}}; } } catch { // do nothing just return invalid deeplink @@ -153,11 +182,6 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { return {type: DeepLink.Invalid, url: deepLinkUrl}; } -function isDeepLinkInvalid(decodedUrl: string) { - const url = decodeURIComponent(decodedUrl); - return Boolean((/([/\\]\.\.|\.\.[/\\])/).exec(url)?.length); -} - export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) { if (!url || (!serverURL && !siteURL)) { return ''; @@ -171,8 +195,8 @@ export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string if (!parsedUrl.protocol) { // If url doesn't contain site or server URL, tack it on. // e.g. URLs from autolink plugin. - const match = new RegExp(escapeRegex(urlBase)).exec(url); - if (!match) { + const deepLinkMatch = new RegExp(escapeRegex(urlBase)).exec(url); + if (!deepLinkMatch) { urlToMatch = urlBase + url; } } diff --git a/app/utils/url/path.ts b/app/utils/url/path.ts new file mode 100644 index 000000000..bf3afe95c --- /dev/null +++ b/app/utils/url/path.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export const ID_PATH_PATTERN = '[a-z0-9]{26}'; +export const PLUGIN_ID_PATH_PATTERN = '^[a-zA-Z0-9-_.]+$'; + +// This should cover: +// - Team name (lowercase english characters, numbers or -) +// - Two ids separated by __ (userID__userID) +export const TEAM_NAME_PATH_PATTERN = '[a-z0-9\\-_]+'; + +// This should cover: +// - Channel name +// - Channel ID +// - Group Channel Name (40 length UID) +// - DM Name (userID__userID) +// - Username prefixed by a @ +// - User ID +// - Email +export const IDENTIFIER_PATH_PATTERN = '[@a-zA-Z\\-_0-9][@a-zA-Z\\-_0-9.]*'; diff --git a/package-lock.json b/package-lock.json index dc6e5a2af..961bb2714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "mime-db": "1.52.0", "moment-timezone": "0.5.43", "pako": "2.1.0", + "path-to-regexp": "6.2.1", "react": "18.2.0", "react-freeze": "1.0.3", "react-intl": "6.4.4", @@ -18577,6 +18578,11 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -36657,6 +36663,11 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", diff --git a/package.json b/package.json index 2590a3e11..87ee599d4 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "mime-db": "1.52.0", "moment-timezone": "0.5.43", "pako": "2.1.0", + "path-to-regexp": "6.2.1", "react": "18.2.0", "react-freeze": "1.0.3", "react-intl": "6.4.4", From bf91340fd48c9a89136a793565f802473005bf9f Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Wed, 15 Nov 2023 21:29:54 -0600 Subject: [PATCH 05/12] put back plugin case --- app/utils/deep_link/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 5b77d6ed0..44221f7a4 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -106,6 +106,12 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, showPermalink(existingServerUrl, deepLinkData.teamName, deepLinkData.postId); break; } + case DeepLink.Plugin: { + // https://mattermost.atlassian.net/browse/MM-49846 + // const deepLinkData = parsed.data as DeepLinkPlugin; + // showModal('PluginInternal', deepLinkData.id, {link: location}); + break; + } } return {error: false}; } catch (error) { From f8f4e9e4742b9a9bfcb9ff4d80270c8efb8baaf6 Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Fri, 17 Nov 2023 11:18:05 -0800 Subject: [PATCH 06/12] review changes --- app/utils/deep_link/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 44221f7a4..29c6d6368 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -131,7 +131,7 @@ type ChannelPathParams = { postId?: string; }; -const CHANNEL_PATH = `/:subpath?/:teamName(${TEAM_NAME_PATH_PATTERN})/:path(channels|messages)/:identifier(${IDENTIFIER_PATH_PATTERN}|${ID_PATH_PATTERN})/:postId(${ID_PATH_PATTERN})?`; +const CHANNEL_PATH = `/:subpath*/: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 = { @@ -140,16 +140,16 @@ type PermalinkPathParams = { teamName: string; postId: string; }; -const PERMALINK_PATH = `/:subpath?/:teamName(${TEAM_NAME_PATH_PATTERN})/pl/:postId(${ID_PATH_PATTERN})`; +const PERMALINK_PATH = `/:subpath*/: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(decodeURIComponent(deepLinkUrl)); - const channelMatch = matchChannelDeeplink(pathname); + const {host, pathname} = urlParse(deepLinkUrl); + const channelMatch = matchChannelDeeplink(pathname); if (channelMatch) { const {params: {subpath, teamName, path, identifier, postId}} = channelMatch; const serverUrl = getServerUrl(host, subpath); @@ -175,7 +175,7 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { 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<{subpath?: string; id: string}>(`/:subpath*/plugins/:id(${PLUGIN_ID_PATH_PATTERN})`, matcherOpts)(pathname); if (pluginMatch) { const {params: {subpath, id}} = pluginMatch; const serverUrl = getServerUrl(host, subpath); From 12a80c4f296ff4914ef49d006954da95bc063c4d Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Tue, 21 Nov 2023 22:05:35 -0600 Subject: [PATCH 07/12] subpath tests, variations test --- app/utils/url/test.ts | 52 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index ea0210586..6db8531d4 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -129,7 +129,7 @@ describe('UrlUtils', () => { describe('matchDeepLink', () => { const URL_NO_PROTOCOL = 'localhost:8065'; - const URL_PATH_NO_PROTOCOL = 'localhost:8065/subpath'; + const URL_PATH_NO_PROTOCOL = 'localhost:8065/subpath/deepsubpath'; const SITE_URL = `http://${URL_NO_PROTOCOL}`; const SERVER_URL = `http://${URL_NO_PROTOCOL}`; const SERVER_WITH_SUBPATH = `http://${URL_PATH_NO_PROTOCOL}`; @@ -279,10 +279,42 @@ describe('UrlUtils', () => { type: DeepLinkType.Channel, }, }, + { + name: 'should match channel link (channel name: messages) with deeplink prefix', + input: { + url: DEEPLINK_URL_ROOT + '/ad-1/channels/messages', + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: { + data: { + channelName: 'messages', + serverUrl: URL_NO_PROTOCOL, + teamName: 'ad-1', + }, + type: DeepLinkType.Channel, + }, + }, + { + name: 'should match channel link (team name: channels, channel name: messages) with deeplink prefix', + input: { + url: DEEPLINK_URL_ROOT + '/channels/channels/messages', + serverURL: SERVER_URL, + siteURL: SITE_URL, + }, + expected: { + data: { + channelName: 'messages', + serverUrl: URL_NO_PROTOCOL, + teamName: 'channels', + }, + type: DeepLinkType.Channel, + }, + }, { name: 'should match permalink with deeplink prefix on a Server hosted in a Subpath', input: { - url: DEEPLINK_URL_ROOT + '/subpath/ad-1/pl/qe93kkfd7783iqwuwfcwcxbsrr', + url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/ad-1/pl/qe93kkfd7783iqwuwfcwcxbsrr', serverURL: SERVER_WITH_SUBPATH, siteURL: SERVER_WITH_SUBPATH, }, @@ -295,6 +327,22 @@ describe('UrlUtils', () => { type: DeepLinkType.Permalink, }, }, + { + name: 'should match permalink (team name: channels) with deeplink prefix on a Server hosted in a Subpath', + input: { + url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/channels/pl/qe93kkfd7783iqwuwfcwcxbsrr', + serverURL: SERVER_WITH_SUBPATH, + siteURL: SERVER_WITH_SUBPATH, + }, + expected: { + data: { + postId: 'qe93kkfd7783iqwuwfcwcxbsrr', + serverUrl: URL_PATH_NO_PROTOCOL, + teamName: 'channels', + }, + type: DeepLinkType.Permalink, + }, + }, { name: 'should match permalink on a Server hosted in a Subpath', input: { From a3b811dcfb7b5ed1491f45c60548cdb1377ef1ea Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Wed, 22 Nov 2023 17:49:35 -0600 Subject: [PATCH 08/12] refactor --- app/actions/remote/command.ts | 2 +- .../markdown/markdown_link/markdown_link.tsx | 4 +- app/utils/deep_link/index.ts | 58 +++++-------- app/utils/url/test.ts | 82 +++++++++++++------ 4 files changed, 84 insertions(+), 62 deletions(-) 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); }); } }); From e39736b4dac1aafb191204b06898a31ba3b6d1a9 Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Wed, 22 Nov 2023 22:54:42 -0600 Subject: [PATCH 09/12] parameter cleanup --- app/utils/deep_link/index.ts | 11 +++++------ app/utils/url/test.ts | 2 +- types/launch/index.ts | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 336075c6b..86f5261de 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -95,7 +95,7 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, } case DeepLink.GroupMessage: { const deepLinkData = parsed.data as DeepLinkGM; - switchToChannelByName(existingServerUrl, deepLinkData.channelId, deepLinkData.teamName, errorBadChannel, intl); + switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl); break; } case DeepLink.Permalink: { @@ -131,10 +131,9 @@ type ChannelPathParams = { teamName: string; path: 'channels' | 'messages'; identifier: string; - postId?: string; }; -const CHANNEL_PATH = `:serverUrl(.*)/: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})`; export const matchChannelDeeplink = match(CHANNEL_PATH, matcherOpts); type PermalinkPathParams = { @@ -151,10 +150,10 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { const channelMatch = matchChannelDeeplink(url); if (channelMatch) { - const {params: {serverUrl, teamName, path, identifier, postId}} = channelMatch; + const {params: {serverUrl, teamName, path, identifier}} = channelMatch; if (path === 'channels') { - return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier, postId}}; + return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier}}; } if (path === 'messages') { @@ -162,7 +161,7 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl, teamName, userName: identifier.substring(1)}}; } - return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl, teamName, channelId: identifier}}; + return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier}}; } } diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index c24280946..eed9fab90 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -323,7 +323,7 @@ describe('UrlUtils', () => { }, expected: { data: { - channelId: GM_CHANNEL_NAME, + channelName: GM_CHANNEL_NAME, serverUrl: URL_NO_PROTOCOL, teamName: 'pl', }, diff --git a/types/launch/index.ts b/types/launch/index.ts index 334e9c392..e25baf93a 100644 --- a/types/launch/index.ts +++ b/types/launch/index.ts @@ -21,7 +21,7 @@ export interface DeepLinkPermalink extends DeepLink { } export interface DeepLinkGM extends DeepLink { - channelId: string; + channelName: string; } export interface DeepLinkPlugin extends DeepLink { From 5d35b4a995987c179407783c37e699dd9ea412dc Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Wed, 22 Nov 2023 22:59:59 -0600 Subject: [PATCH 10/12] remove unneeded decode --- app/utils/deep_link/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 86f5261de..b2aa88a3c 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -123,8 +123,6 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, } } -const matcherOpts: Parameters[1] = {decode: decodeURIComponent}; - type ChannelPathParams = { hostname: string; serverUrl: string; @@ -134,7 +132,7 @@ type ChannelPathParams = { }; const CHANNEL_PATH = `:serverUrl(.*)/:teamName(${TEAM_NAME_PATH_PATTERN})/:path(channels|messages)/:identifier(${IDENTIFIER_PATH_PATTERN})`; -export const matchChannelDeeplink = match(CHANNEL_PATH, matcherOpts); +export const matchChannelDeeplink = match(CHANNEL_PATH); type PermalinkPathParams = { serverUrl: string; @@ -142,7 +140,7 @@ type PermalinkPathParams = { postId: string; }; const PERMALINK_PATH = `:serverUrl(.*)/:teamName(${TEAM_NAME_PATH_PATTERN})/pl/:postId(${ID_PATH_PATTERN})`; -export const matchPermalinkDeeplink = match(PERMALINK_PATH, matcherOpts); +export const matchPermalinkDeeplink = match(PERMALINK_PATH); export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { try { @@ -171,7 +169,7 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl, teamName, postId}}; } - const pluginMatch = match<{serverUrl: string; id: string}>(`:serverUrl(.*)/plugins/:id(${PLUGIN_ID_PATH_PATTERN})`, matcherOpts)(url); + const pluginMatch = match<{serverUrl: string; id: string}>(`:serverUrl(.*)/plugins/:id(${PLUGIN_ID_PATH_PATTERN})`)(url); if (pluginMatch) { const {params: {serverUrl, id}} = pluginMatch; return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl, teamName: '', id}}; From 89fc14bb4352a26e450ccf78dee61b4f46baee12 Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Fri, 24 Nov 2023 13:15:26 -0600 Subject: [PATCH 11/12] fix plugin path matching, add coverage --- app/utils/deep_link/index.ts | 12 +++++-- app/utils/url/path.ts | 2 +- app/utils/url/test.ts | 61 ++++++++++++++++++++++++++++++++++++ types/launch/index.ts | 1 + 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index b2aa88a3c..04dc46095 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -169,10 +169,16 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl, teamName, postId}}; } - const pluginMatch = match<{serverUrl: string; id: string}>(`:serverUrl(.*)/plugins/:id(${PLUGIN_ID_PATH_PATTERN})`)(url); + const pluginMatch = match<{serverUrl: string; id: string; route?: string[]}>(`:serverUrl(.*)/plugins/:id(${PLUGIN_ID_PATH_PATTERN})/:route+`)(url); if (pluginMatch) { - const {params: {serverUrl, id}} = pluginMatch; - return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl, teamName: '', id}}; + const {params: {serverUrl, id, route}} = pluginMatch; + const data: DeepLinkWithData['data'] = {serverUrl, teamName: '', id}; + + if (route) { + data.route = route.join('/'); + } + + return {type: DeepLink.Plugin, url: deepLinkUrl, data}; } } catch (err) { // do nothing just return invalid deeplink diff --git a/app/utils/url/path.ts b/app/utils/url/path.ts index bf3afe95c..19a579512 100644 --- a/app/utils/url/path.ts +++ b/app/utils/url/path.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. export const ID_PATH_PATTERN = '[a-z0-9]{26}'; -export const PLUGIN_ID_PATH_PATTERN = '^[a-zA-Z0-9-_.]+$'; +export const PLUGIN_ID_PATH_PATTERN = '[a-zA-Z0-9-_.]{3,190}'; // This should cover: // - Team name (lowercase english characters, numbers or -) diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index eed9fab90..774c399a0 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -403,6 +403,67 @@ describe('UrlUtils', () => { }, expected: null, }, + { + name: 'should match plugin path on a Server hosted in a Subpath', + input: { + url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/com.acme.abc-test/api/testroute', + serverURL: SERVER_WITH_SUBPATH, + siteURL: SERVER_WITH_SUBPATH, + }, + expected: { + data: { + id: 'com.acme.abc-test', + route: 'api/testroute', + serverUrl: URL_PATH_NO_PROTOCOL, + teamName: '', + }, + type: DeepLinkType.Plugin, + }, + }, + { + name: 'should match plugin path with single-level route', + input: { + url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_ab/testroute', + serverURL: SERVER_WITH_SUBPATH, + siteURL: SERVER_WITH_SUBPATH, + }, + expected: { + data: { + id: 'abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_ab', + route: 'testroute', + serverUrl: URL_PATH_NO_PROTOCOL, + teamName: '', + }, + type: DeepLinkType.Plugin, + }, + }, + { + name: 'should not match plugin path with invalid plugin id len=2', + input: { + url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/ab/api/testroute', + serverURL: SERVER_WITH_SUBPATH, + siteURL: SERVER_WITH_SUBPATH, + }, + expected: null, + }, + { + name: 'should not match plugin path with invalid plugin id len=191', + input: { + url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc/api/testroute', + serverURL: SERVER_WITH_SUBPATH, + siteURL: SERVER_WITH_SUBPATH, + }, + expected: null, + }, + { + name: 'should not match plugin path without a route', + input: { + url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/abc', + serverURL: SERVER_WITH_SUBPATH, + siteURL: SERVER_WITH_SUBPATH, + }, + expected: null, + }, ]; for (const test of tests) { diff --git a/types/launch/index.ts b/types/launch/index.ts index e25baf93a..4dffb4d71 100644 --- a/types/launch/index.ts +++ b/types/launch/index.ts @@ -26,6 +26,7 @@ export interface DeepLinkGM extends DeepLink { export interface DeepLinkPlugin extends DeepLink { id: string; + route?: string; } export type DeepLinkType = typeof DeepLink[keyof typeof DeepLink]; From 4a422af3d6167a8a9f3bf704d09a26dc81c45550 Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Fri, 24 Nov 2023 16:43:19 -0600 Subject: [PATCH 12/12] refactor /:route param --- app/utils/deep_link/index.ts | 10 ++-------- app/utils/url/test.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 04dc46095..c4ec593de 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -169,16 +169,10 @@ export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl, teamName, postId}}; } - const pluginMatch = match<{serverUrl: string; id: string; route?: string[]}>(`:serverUrl(.*)/plugins/:id(${PLUGIN_ID_PATH_PATTERN})/:route+`)(url); + const pluginMatch = match<{serverUrl: string; id: string; route?: string}>(`:serverUrl(.*)/plugins/:id(${PLUGIN_ID_PATH_PATTERN})/:route(.*)?`)(url); if (pluginMatch) { const {params: {serverUrl, id, route}} = pluginMatch; - const data: DeepLinkWithData['data'] = {serverUrl, teamName: '', id}; - - if (route) { - data.route = route.join('/'); - } - - return {type: DeepLink.Plugin, url: deepLinkUrl, data}; + return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl, teamName: '', id, route}}; } } catch (err) { // do nothing just return invalid deeplink diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index 774c399a0..9e8f55af8 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -456,13 +456,21 @@ describe('UrlUtils', () => { expected: null, }, { - name: 'should not match plugin path without a route', + name: 'should match plugin path without a route', input: { url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/abc', serverURL: SERVER_WITH_SUBPATH, siteURL: SERVER_WITH_SUBPATH, }, - expected: null, + expected: { + data: { + id: 'abc', + route: undefined, + serverUrl: URL_PATH_NO_PROTOCOL, + teamName: '', + }, + type: DeepLinkType.Plugin, + }, }, ];