diff --git a/src/components/Settings/Notifications/NotificationsGotify/index.tsx b/src/components/Settings/Notifications/NotificationsGotify/index.tsx index 6c33f6dbc..461103511 100644 --- a/src/components/Settings/Notifications/NotificationsGotify/index.tsx +++ b/src/components/Settings/Notifications/NotificationsGotify/index.tsx @@ -3,6 +3,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/solid'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; @@ -51,10 +52,10 @@ const NotificationsGotify = () => { .required(intl.formatMessage(messages.validationUrlRequired)), otherwise: Yup.string().nullable(), }) - .matches( - // eslint-disable-next-line no-useless-escape - /^(https?:)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i, - intl.formatMessage(messages.validationUrlRequired) + .test( + 'valid-url', + intl.formatMessage(messages.validationUrlRequired), + isValidURL ) .test( 'no-trailing-slash', diff --git a/src/components/Settings/Notifications/NotificationsNtfy/index.tsx b/src/components/Settings/Notifications/NotificationsNtfy/index.tsx index 30906a83e..3ffdd55c0 100644 --- a/src/components/Settings/Notifications/NotificationsNtfy/index.tsx +++ b/src/components/Settings/Notifications/NotificationsNtfy/index.tsx @@ -4,6 +4,7 @@ import SensitiveInput from '@app/components/Common/SensitiveInput'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; @@ -54,10 +55,10 @@ const NotificationsNtfy = () => { .required(intl.formatMessage(messages.validationNtfyUrl)), otherwise: Yup.string().nullable(), }) - .matches( - // eslint-disable-next-line no-useless-escape - /^(https?:)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i, - intl.formatMessage(messages.validationNtfyUrl) + .test( + 'valid-url', + intl.formatMessage(messages.validationNtfyUrl), + isValidURL ), topic: Yup.string() .when('enabled', { diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx index 44f9dfa0e..0595090f3 100644 --- a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx +++ b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx @@ -3,6 +3,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; import { ArrowPathIcon, @@ -107,10 +108,10 @@ const NotificationsWebhook = () => { .required(intl.formatMessage(messages.validationWebhookUrl)), otherwise: Yup.string().nullable(), }) - .matches( - // eslint-disable-next-line no-useless-escape - /^(https?:)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i, - intl.formatMessage(messages.validationWebhookUrl) + .test( + 'valid-url', + intl.formatMessage(messages.validationWebhookUrl), + isValidURL ), jsonPayload: Yup.string() .when('enabled', { diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx index 38228bf03..bf22e94d0 100644 --- a/src/components/Settings/RadarrModal/index.tsx +++ b/src/components/Settings/RadarrModal/index.tsx @@ -3,6 +3,7 @@ import SensitiveInput from '@app/components/Common/SensitiveInput'; import type { RadarrTestResponse } from '@app/components/Settings/SettingsServices'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; import { Transition } from '@headlessui/react'; import type { RadarrSettings } from '@server/lib/settings'; import axios from 'axios'; @@ -117,9 +118,10 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => { intl.formatMessage(messages.validationMinimumAvailabilityRequired) ), externalUrl: Yup.string() - .matches( - /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, - intl.formatMessage(messages.validationApplicationUrl) + .test( + 'valid-url', + intl.formatMessage(messages.validationApplicationUrl), + isValidURL ) .test( 'no-trailing-slash', diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index e66519f08..b4d36cfe5 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -6,6 +6,7 @@ import LibraryItem from '@app/components/Settings/LibraryItem'; import useSettings from '@app/hooks/useSettings'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ApiErrorCode } from '@server/constants/error'; import { MediaServerType } from '@server/constants/server'; @@ -140,10 +141,7 @@ const SettingsJellyfin: React.FC = ({ ), jellyfinExternalUrl: Yup.string() .nullable() - .matches( - /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, - intl.formatMessage(messages.validationUrl) - ) + .test('valid-url', intl.formatMessage(messages.validationUrl), isValidURL) .test( 'no-trailing-slash', intl.formatMessage(messages.validationUrlTrailingSlash), @@ -151,10 +149,7 @@ const SettingsJellyfin: React.FC = ({ ), jellyfinForgotPasswordUrl: Yup.string() .nullable() - .matches( - /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, - intl.formatMessage(messages.validationUrl) - ) + .test('valid-url', intl.formatMessage(messages.validationUrl), isValidURL) .test( 'no-trailing-slash', intl.formatMessage(messages.validationUrlTrailingSlash), diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index d1e809a28..d4f52d1b4 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -13,6 +13,7 @@ import useLocale from '@app/hooks/useLocale'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowPathIcon } from '@heroicons/react/24/solid'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; @@ -88,9 +89,10 @@ const SettingsMain = () => { intl.formatMessage(messages.validationApplicationTitle) ), applicationUrl: Yup.string() - .matches( - /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, - intl.formatMessage(messages.validationApplicationUrl) + .test( + 'valid-url', + intl.formatMessage(messages.validationApplicationUrl), + isValidURL ) .test( 'no-trailing-slash', diff --git a/src/components/Settings/SettingsPlex.tsx b/src/components/Settings/SettingsPlex.tsx index 973c5ede4..2c765597d 100644 --- a/src/components/Settings/SettingsPlex.tsx +++ b/src/components/Settings/SettingsPlex.tsx @@ -8,6 +8,7 @@ import LibraryItem from '@app/components/Settings/LibraryItem'; import SettingsBadge from '@app/components/Settings/SettingsBadge'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowPathIcon, @@ -191,9 +192,10 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => { otherwise: Yup.string().nullable(), }), tautulliExternalUrl: Yup.string() - .matches( - /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, - intl.formatMessage(messages.validationUrl) + .test( + 'valid-url', + intl.formatMessage(messages.validationUrl), + isValidURL ) .test( 'no-trailing-slash', diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx index 9fb28d9d1..90f4953cc 100644 --- a/src/components/Settings/SonarrModal/index.tsx +++ b/src/components/Settings/SonarrModal/index.tsx @@ -3,6 +3,7 @@ import SensitiveInput from '@app/components/Common/SensitiveInput'; import type { SonarrTestResponse } from '@app/components/Settings/SettingsServices'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; import { Transition } from '@headlessui/react'; import type { SonarrSettings } from '@server/lib/settings'; import axios from 'axios'; @@ -126,9 +127,10 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => { ) : Yup.number(), externalUrl: Yup.string() - .matches( - /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, - intl.formatMessage(messages.validationApplicationUrl) + .test( + 'valid-url', + intl.formatMessage(messages.validationApplicationUrl), + isValidURL ) .test( 'no-trailing-slash', diff --git a/src/utils/urlValidationHelper.ts b/src/utils/urlValidationHelper.ts new file mode 100644 index 000000000..ffd81f50d --- /dev/null +++ b/src/utils/urlValidationHelper.ts @@ -0,0 +1,15 @@ +export function isValidURL(value: unknown) { + try { + let url: URL; + if (typeof value === 'string') { + url = new URL(value); + } else if (value instanceof URL) { + url = value; + } else { + return false; + } + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } +}