From fd5bbb27699942f7d741d074eafdf16bfc9ecdd6 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 2 Oct 2023 14:47:39 -0700 Subject: Warn the user on links that dont match their text (#1573) * Add link warning modal when URLs do not match their text * Simplify the misleading link case for clarity * Fix typecheck * fix dark mode * Give a stronger visual indication of the root domain in the link warning * More rigorous URL mismatch logic * Remove debug --------- Co-authored-by: Ansh Nanda --- src/lib/strings/url-helpers.ts | 51 ++++++++++++ src/state/models/ui/shell.ts | 7 ++ src/view/com/modals/LinkWarning.tsx | 162 ++++++++++++++++++++++++++++++++++++ src/view/com/modals/Modal.tsx | 4 + src/view/com/modals/Modal.web.tsx | 3 + src/view/com/util/Link.tsx | 25 +++++- src/view/com/util/text/RichText.tsx | 1 + 7 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 src/view/com/modals/LinkWarning.tsx (limited to 'src') diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 671dc9781..3c27d8639 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -1,6 +1,7 @@ import {AtUri} from '@atproto/api' import {PROD_SERVICE} from 'state/index' import TLDs from 'tlds' +import psl from 'psl' export function isValidDomain(str: string): boolean { return !!TLDs.find(tld => { @@ -166,3 +167,53 @@ export function getYoutubeVideoId(link: string): string | undefined { } return videoId } + +export function linkRequiresWarning(uri: string, label: string) { + const labelDomain = labelToDomain(label) + if (!labelDomain) { + return true + } + try { + const urip = new URL(uri) + return labelDomain !== urip.hostname + } catch { + return true + } +} + +function labelToDomain(label: string): string | undefined { + // any spaces just immediately consider the label a non-url + if (/\s/.test(label)) { + return undefined + } + try { + return new URL(label).hostname + } catch {} + try { + return new URL('https://' + label).hostname + } catch {} + return undefined +} + +export function isPossiblyAUrl(str: string): boolean { + str = str.trim() + if (str.startsWith('http://')) { + return true + } + if (str.startsWith('https://')) { + return true + } + const [firstWord] = str.split(/[\s\/]/) + return isValidDomain(firstWord) +} + +export function splitApexDomain(hostname: string): [string, string] { + const hostnamep = psl.parse(hostname) + if (hostnamep.error || !hostnamep.listed || !hostnamep.domain) { + return ['', hostname] + } + return [ + hostnamep.subdomain ? `${hostnamep.subdomain}.` : '', + hostnamep.domain, + ] +} diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index bd285c8cd..a8937b84c 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -154,6 +154,12 @@ export interface SwitchAccountModal { name: 'switch-account' } +export interface LinkWarningModal { + name: 'link-warning' + text: string + href: string +} + export type Modal = // Account | AddAppPasswordModal @@ -191,6 +197,7 @@ export type Modal = // Generic | ConfirmModal + | LinkWarningModal interface LightboxModel {} diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx new file mode 100644 index 000000000..67a156af4 --- /dev/null +++ b/src/view/com/modals/LinkWarning.tsx @@ -0,0 +1,162 @@ +import React from 'react' +import {Linking, SafeAreaView, StyleSheet, View} from 'react-native' +import {ScrollView} from './util' +import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {useStores} from 'state/index' +import {s, colors} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {isWeb} from 'platform/detection' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers' + +export const snapPoints = ['50%'] + +export const Component = observer(function Component({ + text, + href, +}: { + text: string + href: string +}) { + const pal = usePalette('default') + const store = useStores() + const {isMobile} = useWebMediaQueries() + const potentiallyMisleading = isPossiblyAUrl(text) + + const onPressVisit = () => { + store.shell.closeModal() + Linking.openURL(href) + } + + return ( + + + + {potentiallyMisleading ? ( + <> + + + Potentially Misleading Link + + + ) : ( + + Leaving Bluesky + + )} + + + + + This link is taking you to the following website: + + + + + {potentiallyMisleading && ( + + Make sure this is where you intend to go! + + )} + + + +