refactor parseDeepLink with path-to-regexp
This commit is contained in:
parent
744d5c1120
commit
bf25f10c38
4 changed files with 94 additions and 38 deletions
|
|
@ -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<typeof match>[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<ChannelPathParams>(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<PermalinkPathParams>(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. <jump to convo> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
app/utils/url/path.ts
Normal file
20
app/utils/url/path.ts
Normal file
|
|
@ -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.]*';
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue