From 12c6c6dcf9a6875db03dbb9d66eed897c1d964e3 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 11 Mar 2026 14:19:39 +0100 Subject: [PATCH] Profile editing: Add warning for links (#38148) --- .../account_edit/modals/fields_modals.tsx | 20 +++++++++++++- app/javascript/mastodon/locales/en.json | 3 ++- app/javascript/mastodon/utils/checks.test.ts | 21 +++++++++++++++ app/javascript/mastodon/utils/checks.ts | 26 +++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 app/javascript/mastodon/utils/checks.test.ts diff --git a/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx b/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx index c7b3b6ebc5..b5a095cf68 100644 --- a/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx @@ -18,6 +18,7 @@ import { useAppDispatch, useAppSelector, } from '@/mastodon/store'; +import { isUrlWithoutProtocol } from '@/mastodon/utils/checks'; import { ConfirmationModal } from '../../ui/components/confirmation_modals'; import type { DialogModalProps } from '../../ui/components/dialog_modal'; @@ -48,7 +49,7 @@ const messages = defineMessages({ }, editValueHint: { id: 'account_edit.field_edit_modal.value_hint', - defaultMessage: 'E.g. “example.me”', + defaultMessage: 'E.g. “https://example.me”', }, limitHeader: { id: 'account_edit.field_edit_modal.limit_header', @@ -109,6 +110,10 @@ export const EditFieldModal: FC = ({ ); return hasLink && hasEmoji; }, [customEmojiCodes, newLabel, newValue]); + const hasLinkWithoutProtocol = useMemo( + () => isUrlWithoutProtocol(newValue), + [newValue], + ); const dispatch = useAppDispatch(); const handleSave = useCallback(() => { @@ -175,6 +180,19 @@ export const EditFieldModal: FC = ({ /> )} + + {hasLinkWithoutProtocol && ( + + https://, + }} + /> + + )} ); }; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 57b868c848..2ede5449d1 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -173,7 +173,8 @@ "account_edit.field_edit_modal.link_emoji_warning": "We recommend against the use of custom emoji in combination with urls. Custom fields containing both will display as text only instead of as a link, in order to prevent user confusion.", "account_edit.field_edit_modal.name_hint": "E.g. “Personal website”", "account_edit.field_edit_modal.name_label": "Label", - "account_edit.field_edit_modal.value_hint": "E.g. “example.me”", + "account_edit.field_edit_modal.url_warning": "To add a link, please include {protocol} at the beginning.", + "account_edit.field_edit_modal.value_hint": "E.g. “https://example.me”", "account_edit.field_edit_modal.value_label": "Value", "account_edit.field_reorder_modal.drag_cancel": "Dragging was cancelled. Field \"{item}\" was dropped.", "account_edit.field_reorder_modal.drag_end": "Field \"{item}\" was dropped.", diff --git a/app/javascript/mastodon/utils/checks.test.ts b/app/javascript/mastodon/utils/checks.test.ts new file mode 100644 index 0000000000..862a2a0abf --- /dev/null +++ b/app/javascript/mastodon/utils/checks.test.ts @@ -0,0 +1,21 @@ +import { isUrlWithoutProtocol } from './checks'; + +describe('isUrlWithoutProtocol', () => { + test.concurrent.each([ + ['example.com', true], + ['sub.domain.co.uk', true], + ['example', false], // No dot + ['example..com', false], // Consecutive dots + ['example.com.', false], // Trailing dot + ['example.c', false], // TLD too short + ['example.123', false], // Numeric TLDs are not valid + ['example.com/path', true], // Paths are allowed + ['example.com?query=string', true], // Query strings are allowed + ['example.com#fragment', true], // Fragments are allowed + ['example .com', false], // Spaces are not allowed + ['example://com', false], // Protocol inside the string is not allowed + ['example.com^', false], // Invalid characters not allowed + ])('should return %s for input "%s"', (input, expected) => { + expect(isUrlWithoutProtocol(input)).toBe(expected); + }); +}); diff --git a/app/javascript/mastodon/utils/checks.ts b/app/javascript/mastodon/utils/checks.ts index 8b05ac24a7..d5d528bdc6 100644 --- a/app/javascript/mastodon/utils/checks.ts +++ b/app/javascript/mastodon/utils/checks.ts @@ -9,3 +9,29 @@ export function isValidUrl( return false; } } + +/** + * Checks if the input string is probably a URL without a protocol. Note this is not full URL validation, + * and is mostly used to detect link-like inputs. + * @see https://www.xjavascript.com/blog/check-if-a-javascript-string-is-a-url/ + * @param input The input string to check + */ +export function isUrlWithoutProtocol(input: string): boolean { + if (!input.length || input.includes(' ') || input.includes('://')) { + return false; + } + + try { + const url = new URL(`http://${input}`); + const { host } = url; + return ( + host !== '' && // Host is not empty + host.includes('.') && // Host contains at least one dot + !host.endsWith('.') && // No trailing dot + !host.includes('..') && // No consecutive dots + /\.[\w]{2,}$/.test(host) // TLD is at least 2 characters + ); + } catch {} + + return false; +}