diff --git a/__tests__/lib/strings/url-helpers.test.ts b/__tests__/lib/strings/url-helpers.test.ts index 8bb52ed4..6ac31aeb 100644 --- a/__tests__/lib/strings/url-helpers.test.ts +++ b/__tests__/lib/strings/url-helpers.test.ts @@ -1,3 +1,5 @@ +import {it, describe, expect} from '@jest/globals' + import { linkRequiresWarning, isPossiblyAUrl, @@ -6,6 +8,7 @@ import { describe('linkRequiresWarning', () => { type Case = [string, string, boolean] + const cases: Case[] = [ ['http://example.com', 'http://example.com', false], ['http://example.com', 'example.com', false], @@ -64,6 +67,10 @@ describe('linkRequiresWarning', () => { ['http://bsky.app/', 'https://google.com', true], ['https://bsky.app/', 'https://google.com', true], + // case insensitive + ['https://Example.com', 'example.com', false], + ['https://example.com', 'Example.com', false], + // bad uri inputs, default to true ['', '', true], ['example.com', 'example.com', true], diff --git a/jest/jestSetup.js b/jest/jestSetup.js index 5d6bd4f1..d8cee9bf 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -2,6 +2,9 @@ import {configure} from '@testing-library/react-native' import 'react-native-gesture-handler/jestSetup' +// IMPORTANT: this is what's used in the native runtime +import 'react-native-url-polyfill/auto' + configure({asyncUtilTimeout: 20000}) jest.mock('@react-native-async-storage/async-storage', () => diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index d06bd802..e9bf4111 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -168,8 +168,15 @@ export function getYoutubeVideoId(link: string): string | undefined { return videoId } +/** + * Checks if the label in the post text matches the host of the link facet. + * + * Hosts are case-insensitive, so should be lowercase for comparison. + * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 + */ export function linkRequiresWarning(uri: string, label: string) { const labelDomain = labelToDomain(label) + let urip try { urip = new URL(uri) @@ -177,7 +184,9 @@ export function linkRequiresWarning(uri: string, label: string) { return true } - if (urip.hostname === 'bsky.app') { + const host = urip.hostname.toLowerCase() + + if (host === 'bsky.app') { // if this is a link to internal content, // warn if it represents itself as a URL to another app if ( @@ -194,20 +203,26 @@ export function linkRequiresWarning(uri: string, label: string) { if (!labelDomain) { return true } - return labelDomain !== urip.hostname + return labelDomain !== host } } -function labelToDomain(label: string): string | undefined { +/** + * Returns a lowercase domain hostname if the label is a valid URL. + * + * Hosts are case-insensitive, so should be lowercase for comparison. + * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 + */ +export 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 + return new URL(label).hostname.toLowerCase() } catch {} try { - return new URL('https://' + label).hostname + return new URL('https://' + label).hostname.toLowerCase() } catch {} return undefined }