From df52740752079d9afc3b279441ebb7ac1cd92055 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 12 Sep 2023 07:26:05 -0300 Subject: [PATCH] Add alert when deeplink is invalid (#7538) --- app/init/launch.ts | 2 +- app/managers/global_event_handler.ts | 10 +++-- app/screens/home/index.tsx | 13 +++++- app/utils/deep_link/index.ts | 61 ++++++++++++++++++---------- app/utils/general/index.ts | 10 +++++ app/utils/server/server.test.ts | 2 +- assets/base/i18n/en.json | 1 + test/intl-test-helper.tsx | 9 +--- 8 files changed, 71 insertions(+), 37 deletions(-) diff --git a/app/init/launch.ts b/app/init/launch.ts index ca46ede8b..f03c6426c 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -74,7 +74,7 @@ const launchApp = async (props: LaunchProps) => { let serverUrl: string | undefined; switch (props?.launchType) { case Launch.DeepLink: - if (props.extra?.type !== DeepLink.Invalid) { + if (props.extra && props.extra.type !== DeepLink.Invalid) { const extra = props.extra as DeepLinkWithData; const existingServer = DatabaseManager.searchUrl(extra.data!.serverUrl); serverUrl = existingServer; diff --git a/app/managers/global_event_handler.ts b/app/managers/global_event_handler.ts index 377e03c8e..cc9b5c9dd 100644 --- a/app/managers/global_event_handler.ts +++ b/app/managers/global_event_handler.ts @@ -17,7 +17,8 @@ import {queryTeamDefaultChannel} from '@queries/servers/channel'; import {getCommonSystemValues} from '@queries/servers/system'; import {getTeamChannelHistory} from '@queries/servers/team'; import {setScreensOrientation} from '@screens/navigation'; -import {handleDeepLink} from '@utils/deep_link'; +import {alertInvalidDeepLink, handleDeepLink} from '@utils/deep_link'; +import {getIntlShape} from '@utils/general'; type LinkingCallbackArg = {url: string}; @@ -50,13 +51,16 @@ class GlobalEventHandler { } }; - onDeepLink = (event: LinkingCallbackArg) => { + onDeepLink = async (event: LinkingCallbackArg) => { if (event.url?.startsWith(Sso.REDIRECT_URL_SCHEME) || event.url?.startsWith(Sso.REDIRECT_URL_SCHEME_DEV)) { return; } if (event.url) { - handleDeepLink(event.url); + const {error} = await handleDeepLink(event.url); + if (error) { + alertInvalidDeepLink(getIntlShape(DEFAULT_LOCALE)); + } } }; diff --git a/app/screens/home/index.tsx b/app/screens/home/index.tsx index 1304f89d3..828cca8a1 100644 --- a/app/screens/home/index.tsx +++ b/app/screens/home/index.tsx @@ -17,7 +17,7 @@ import {useAppState} from '@hooks/device'; import {getAllServers} from '@queries/app/servers'; import {findChannels, popToRoot} from '@screens/navigation'; import NavigationStore from '@store/navigation_store'; -import {handleDeepLink} from '@utils/deep_link'; +import {alertInvalidDeepLink, handleDeepLink} from '@utils/deep_link'; import {logError} from '@utils/log'; import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation'; import {notificationError} from '@utils/notification'; @@ -121,9 +121,18 @@ export default function HomeScreen(props: HomeProps) { useEffect(() => { if (props.launchType === 'deeplink') { + if (props.launchError) { + alertInvalidDeepLink(intl); + return; + } + const deepLink = props.extra as DeepLinkWithData; if (deepLink?.url) { - handleDeepLink(deepLink.url); + handleDeepLink(deepLink.url).then((result) => { + if (result.error) { + alertInvalidDeepLink(intl); + } + }); } } }, []); diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 24031fc23..7b2bb982d 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -10,14 +10,14 @@ import {fetchUsersByUsernames} from '@actions/remote/user'; import {DeepLink, Launch, Screens} from '@constants'; import {getDefaultThemeByAppearance} from '@context/theme'; import DatabaseManager from '@database/manager'; -import {DEFAULT_LOCALE, getTranslations} from '@i18n'; +import {DEFAULT_LOCALE, getTranslations, t} from '@i18n'; import WebsocketManager from '@managers/websocket_manager'; import {getActiveServerUrl} from '@queries/app/servers'; import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user'; import {dismissAllModalsAndPopToRoot} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; -import {errorBadChannel, errorUnkownUser} from '@utils/draft'; +import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '@utils/draft'; import {logError} from '@utils/log'; import {escapeRegex} from '@utils/markdown'; import {addNewServer} from '@utils/server'; @@ -115,31 +115,39 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, } export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { - const url = removeProtocol(deepLinkUrl); + try { + const url = removeProtocol(decodeURIComponent(deepLinkUrl)); - 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]}}; - } + if (url.includes('../') || url.includes('/..')) { + return {type: DeepLink.Invalid, url: deepLinkUrl}; + } - 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]}}; - } + 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]}}; + } - 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('(.*)\\/([^\\/]+)\\/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.GroupMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelId: 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('(.*)\\/plugins\\/([^\\/]+)\\/(\\S+)').exec(url); - if (match) { - return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl: match[1], id: match[2], teamName: ''}}; + 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: ''}}; + } + } catch { + // do nothing just return invalid deeplink } return {type: DeepLink.Invalid, url: deepLinkUrl}; @@ -201,3 +209,12 @@ export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = fals return launchProps; }; + +export function alertInvalidDeepLink(intl: IntlShape) { + const message = { + id: t('mobile.deep_link.invalid'), + defaultMessage: 'This link you are trying to open is invalid.', + }; + + return alertErrorWithFallback(intl, {}, message); +} diff --git a/app/utils/general/index.ts b/app/utils/general/index.ts index 5db186d06..fb5a40d59 100644 --- a/app/utils/general/index.ts +++ b/app/utils/general/index.ts @@ -1,13 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {createIntl} from 'react-intl'; import DeviceInfo from 'react-native-device-info'; import ReactNativeHapticFeedback, {HapticFeedbackTypes} from 'react-native-haptic-feedback'; +import {getTranslations} from '@i18n'; + type SortByCreatAt = (Session | Channel | Team | Post) & { create_at: number; } +export function getIntlShape(locale = 'en') { + return createIntl({ + locale, + messages: getTranslations(locale), + }); +} + // eslint-disable-next-line @typescript-eslint/no-unused-vars export function emptyFunction(..._args: any[]) { // do nothing diff --git a/app/utils/server/server.test.ts b/app/utils/server/server.test.ts index b7e0ab691..62eac99c9 100644 --- a/app/utils/server/server.test.ts +++ b/app/utils/server/server.test.ts @@ -3,7 +3,7 @@ import {Alert} from 'react-native'; -import {getIntlShape} from '@test/intl-test-helper'; +import {getIntlShape} from '@utils/general'; import {unsupportedServer} from '.'; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 4ff97515b..15707f980 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -521,6 +521,7 @@ "mobile.custom_status.clear_after": "Clear After", "mobile.custom_status.clear_after.title": "Clear Custom Status After", "mobile.custom_status.modal_confirm": "Done", + "mobile.deep_link.invalid": "This link you are trying to open is invalid.", "mobile.diagnostic_id.empty": "A DiagnosticId value is missing for this server. Contact your system admin to review this value and restart the server.", "mobile.direct_message.error": "We couldn't open a DM with {displayName}.", "mobile.display_settings.clockDisplay": "Clock Display", diff --git a/test/intl-test-helper.tsx b/test/intl-test-helper.tsx index 576ca514e..fc1cb9aef 100644 --- a/test/intl-test-helper.tsx +++ b/test/intl-test-helper.tsx @@ -4,7 +4,7 @@ import DatabaseProvider from '@nozbe/watermelondb/DatabaseProvider'; import {render} from '@testing-library/react-native'; import React, {type ReactElement} from 'react'; -import {createIntl, IntlProvider} from 'react-intl'; +import {IntlProvider} from 'react-intl'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import {ThemeContext, getDefaultThemeByAppearance} from '@context/theme'; @@ -12,13 +12,6 @@ import {getTranslations} from '@i18n'; import type Database from '@nozbe/watermelondb/Database'; -export function getIntlShape(locale = 'en') { - return createIntl({ - locale, - messages: getTranslations(locale), - }); -} - export function renderWithIntl(ui: ReactElement, {locale = 'en', ...renderOptions} = {}) { function Wrapper({children}: {children: ReactElement}) { return (