From bf25759a574b54397eb91f760991917c2e5b50c3 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:36:24 +1000 Subject: [PATCH] Validate custom locale and javascript strings (#4893) * Validate locale json string * Validate custom javascript string --- ui/v2.5/src/components/Settings/Inputs.tsx | 42 +++++++-- .../SettingsInterfacePanel.tsx | 88 ++++++++++++++----- ui/v2.5/src/locales/en-GB.json | 2 + 3 files changed, 105 insertions(+), 27 deletions(-) diff --git a/ui/v2.5/src/components/Settings/Inputs.tsx b/ui/v2.5/src/components/Settings/Inputs.tsx index fc23782e7..61353011f 100644 --- a/ui/v2.5/src/components/Settings/Inputs.tsx +++ b/ui/v2.5/src/components/Settings/Inputs.tsx @@ -273,9 +273,14 @@ export interface ISettingModal { subHeading?: React.ReactNode; value: T | undefined; close: (v?: T) => void; - renderField: (value: T | undefined, setValue: (v?: T) => void) => JSX.Element; + renderField: ( + value: T | undefined, + setValue: (v?: T) => void, + error?: string + ) => JSX.Element; modalProps?: ModalProps; validate?: (v: T) => boolean | undefined; + error?: string | undefined; } export const SettingModal = (props: ISettingModal) => { @@ -289,6 +294,7 @@ export const SettingModal = (props: ISettingModal) => { renderField, modalProps, validate, + error, } = props; const intl = useIntl(); @@ -306,7 +312,7 @@ export const SettingModal = (props: ISettingModal) => { {headingID ? : heading} - {renderField(currentValue, setCurrentValue)} + {renderField(currentValue, setCurrentValue, error)} {subHeadingID ? (
{intl.formatMessage({ id: subHeadingID })} @@ -341,9 +347,14 @@ interface IModalSetting extends ISetting { buttonText?: string; buttonTextID?: string; onChange: (v: T) => void; - renderField: (value: T | undefined, setValue: (v?: T) => void) => JSX.Element; + renderField: ( + value: T | undefined, + setValue: (v?: T) => void, + error?: string + ) => JSX.Element; renderValue?: (v: T | undefined) => JSX.Element; modalProps?: ModalProps; + validateChange?: (v: T) => void | undefined; } export const ModalSetting = (props: IModalSetting) => { @@ -364,10 +375,29 @@ export const ModalSetting = (props: IModalSetting) => { modalProps, disabled, advanced, + validateChange, } = props; const [showModal, setShowModal] = useState(false); + const [error, setError] = useState(); const { advancedMode } = useSettings(); + function onClose(v: T | undefined) { + setError(undefined); + if (v !== undefined) { + if (validateChange) { + try { + validateChange(v); + } catch (e) { + setError((e as Error).message); + return; + } + } + + onChange(v); + } + setShowModal(false); + } + if (advanced && !advancedMode) return null; return ( @@ -380,10 +410,8 @@ export const ModalSetting = (props: IModalSetting) => { subHeading={subHeading} value={value} renderField={renderField} - close={(v) => { - if (v !== undefined) onChange(v); - setShowModal(false); - }} + close={onClose} + error={error} {...modalProps} /> ) : undefined} diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 086b27cdb..5e7b6db27 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -135,6 +135,40 @@ export const SettingsInterfacePanel: React.FC = () => { }); } + function validateLocaleString(v: string) { + if (!v) return; + try { + JSON.parse(v); + } catch (e) { + throw new Error( + intl.formatMessage( + { id: "errors.invalid_json_string" }, + { + error: (e as SyntaxError).message, + } + ) + ); + } + } + + function validateJavascriptString(v: string) { + if (!v) return; + try { + // creates a function from the string to validate it but does not execute it + // eslint-disable-next-line @typescript-eslint/no-implied-eval + new Function(v); + } catch (e) { + throw new Error( + intl.formatMessage( + { id: "errors.invalid_javascript_string" }, + { + error: (e as SyntaxError).message, + } + ) + ); + } + } + if (error) return

{error.message}

; if (loading) return ; @@ -726,16 +760,23 @@ export const SettingsInterfacePanel: React.FC = () => { subHeadingID="config.ui.custom_javascript.description" value={iface.javascript ?? undefined} onChange={(v) => saveInterface({ javascript: v })} - renderField={(value, setValue) => ( - ) => - setValue(e.currentTarget.value) - } - rows={16} - className="text-input code" - /> + validateChange={validateJavascriptString} + renderField={(value, setValue, err) => ( + <> + ) => + setValue(e.currentTarget.value) + } + rows={16} + className="text-input code" + isInvalid={!!err} + /> + + {err} + + )} renderValue={() => { return <>; @@ -756,16 +797,23 @@ export const SettingsInterfacePanel: React.FC = () => { subHeadingID="config.ui.custom_locales.description" value={iface.customLocales ?? undefined} onChange={(v) => saveInterface({ customLocales: v })} - renderField={(value, setValue) => ( - ) => - setValue(e.currentTarget.value) - } - rows={16} - className="text-input code" - /> + validateChange={validateLocaleString} + renderField={(value, setValue, err) => ( + <> + ) => + setValue(e.currentTarget.value) + } + rows={16} + className="text-input code" + isInvalid={!!err} + /> + + {err} + + )} renderValue={() => { return <>; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 76d573fef..d1072183a 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1017,6 +1017,8 @@ "errors": { "header": "Error", "image_index_greater_than_zero": "Image index must be greater than 0", + "invalid_javascript_string": "Invalid javascript code: {error}", + "invalid_json_string": "Invalid JSON string: {error}", "lazy_component_error_help": "If you recently upgraded Stash, please reload the page or clear your browser cache.", "loading_type": "Error loading {type}", "something_went_wrong": "Something went wrong."