Validate custom locale and javascript strings (#4893)

* Validate locale json string
* Validate custom javascript string
This commit is contained in:
WithoutPants
2024-06-11 11:36:24 +10:00
committed by GitHub
parent 621e890a48
commit bf25759a57
3 changed files with 105 additions and 27 deletions

View File

@@ -273,9 +273,14 @@ export interface ISettingModal<T> {
subHeading?: React.ReactNode; subHeading?: React.ReactNode;
value: T | undefined; value: T | undefined;
close: (v?: T) => void; 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; modalProps?: ModalProps;
validate?: (v: T) => boolean | undefined; validate?: (v: T) => boolean | undefined;
error?: string | undefined;
} }
export const SettingModal = <T extends {}>(props: ISettingModal<T>) => { export const SettingModal = <T extends {}>(props: ISettingModal<T>) => {
@@ -289,6 +294,7 @@ export const SettingModal = <T extends {}>(props: ISettingModal<T>) => {
renderField, renderField,
modalProps, modalProps,
validate, validate,
error,
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
@@ -306,7 +312,7 @@ export const SettingModal = <T extends {}>(props: ISettingModal<T>) => {
{headingID ? <FormattedMessage id={headingID} /> : heading} {headingID ? <FormattedMessage id={headingID} /> : heading}
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
{renderField(currentValue, setCurrentValue)} {renderField(currentValue, setCurrentValue, error)}
{subHeadingID ? ( {subHeadingID ? (
<div className="sub-heading"> <div className="sub-heading">
{intl.formatMessage({ id: subHeadingID })} {intl.formatMessage({ id: subHeadingID })}
@@ -341,9 +347,14 @@ interface IModalSetting<T> extends ISetting {
buttonText?: string; buttonText?: string;
buttonTextID?: string; buttonTextID?: string;
onChange: (v: T) => void; 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; renderValue?: (v: T | undefined) => JSX.Element;
modalProps?: ModalProps; modalProps?: ModalProps;
validateChange?: (v: T) => void | undefined;
} }
export const ModalSetting = <T extends {}>(props: IModalSetting<T>) => { export const ModalSetting = <T extends {}>(props: IModalSetting<T>) => {
@@ -364,10 +375,29 @@ export const ModalSetting = <T extends {}>(props: IModalSetting<T>) => {
modalProps, modalProps,
disabled, disabled,
advanced, advanced,
validateChange,
} = props; } = props;
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [error, setError] = useState<string>();
const { advancedMode } = useSettings(); 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; if (advanced && !advancedMode) return null;
return ( return (
@@ -380,10 +410,8 @@ export const ModalSetting = <T extends {}>(props: IModalSetting<T>) => {
subHeading={subHeading} subHeading={subHeading}
value={value} value={value}
renderField={renderField} renderField={renderField}
close={(v) => { close={onClose}
if (v !== undefined) onChange(v); error={error}
setShowModal(false);
}}
{...modalProps} {...modalProps}
/> />
) : undefined} ) : undefined}

View File

@@ -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 <h1>{error.message}</h1>; if (error) return <h1>{error.message}</h1>;
if (loading) return <LoadingIndicator />; if (loading) return <LoadingIndicator />;
@@ -726,16 +760,23 @@ export const SettingsInterfacePanel: React.FC = () => {
subHeadingID="config.ui.custom_javascript.description" subHeadingID="config.ui.custom_javascript.description"
value={iface.javascript ?? undefined} value={iface.javascript ?? undefined}
onChange={(v) => saveInterface({ javascript: v })} onChange={(v) => saveInterface({ javascript: v })}
renderField={(value, setValue) => ( validateChange={validateJavascriptString}
<Form.Control renderField={(value, setValue, err) => (
as="textarea" <>
value={value} <Form.Control
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => as="textarea"
setValue(e.currentTarget.value) value={value}
} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
rows={16} setValue(e.currentTarget.value)
className="text-input code" }
/> rows={16}
className="text-input code"
isInvalid={!!err}
/>
<Form.Control.Feedback type="invalid">
{err}
</Form.Control.Feedback>
</>
)} )}
renderValue={() => { renderValue={() => {
return <></>; return <></>;
@@ -756,16 +797,23 @@ export const SettingsInterfacePanel: React.FC = () => {
subHeadingID="config.ui.custom_locales.description" subHeadingID="config.ui.custom_locales.description"
value={iface.customLocales ?? undefined} value={iface.customLocales ?? undefined}
onChange={(v) => saveInterface({ customLocales: v })} onChange={(v) => saveInterface({ customLocales: v })}
renderField={(value, setValue) => ( validateChange={validateLocaleString}
<Form.Control renderField={(value, setValue, err) => (
as="textarea" <>
value={value} <Form.Control
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => as="textarea"
setValue(e.currentTarget.value) value={value}
} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
rows={16} setValue(e.currentTarget.value)
className="text-input code" }
/> rows={16}
className="text-input code"
isInvalid={!!err}
/>
<Form.Control.Feedback type="invalid">
{err}
</Form.Control.Feedback>
</>
)} )}
renderValue={() => { renderValue={() => {
return <></>; return <></>;

View File

@@ -1017,6 +1017,8 @@
"errors": { "errors": {
"header": "Error", "header": "Error",
"image_index_greater_than_zero": "Image index must be greater than 0", "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.", "lazy_component_error_help": "If you recently upgraded Stash, please reload the page or clear your browser cache.",
"loading_type": "Error loading {type}", "loading_type": "Error loading {type}",
"something_went_wrong": "Something went wrong." "something_went_wrong": "Something went wrong."