mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
refactor(url validation): replace regex-based URL validation by the JavaScript URL constructor (#1650)
Replaced regex-based URL validation with the native JavaScript URL constructor to improve reliability. This approach should be more robust and should help prevent bugs like the one we previously encountered with malformed regex. fix #1539
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<SettingsJellyfinProps> = ({
|
||||
),
|
||||
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<SettingsJellyfinProps> = ({
|
||||
),
|
||||
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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
15
src/utils/urlValidationHelper.ts
Normal file
15
src/utils/urlValidationHelper.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user