From d1930c1dbcf7628748251a88f64f6718ef9aaaa6 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Tue, 30 Jul 2024 16:53:46 +0000 Subject: [PATCH] web/advanced: check if imported settings are valid --- web/i18n/en/settings.json | 6 +- .../settings/TransferSettings.svelte | 89 +++++++++++-------- web/src/lib/settings/validate.ts | 85 ++++++++++++++++++ 3 files changed, 144 insertions(+), 36 deletions(-) create mode 100644 web/src/lib/settings/validate.ts diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index 69555ab3..34116b90 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -101,5 +101,9 @@ "advanced.debug.description": "gives you access to a page with app & device info useful for debugging.", "advanced.data": "settings data", - "advanced.reset": "reset all settings" + "advanced.reset": "reset all settings", + "advanced.import": "import", + "advanced.import.no_data": "failed loading setting data from file", + "advanced.import.invalid": "file does not contain valid cobalt settings", + "advanced.export": "export" } diff --git a/web/src/components/settings/TransferSettings.svelte b/web/src/components/settings/TransferSettings.svelte index 3db4ee8b..7ca29ca4 100644 --- a/web/src/components/settings/TransferSettings.svelte +++ b/web/src/components/settings/TransferSettings.svelte @@ -6,6 +6,7 @@ updateSetting, loadFromString } from "$lib/state/settings"; + import { validateSettings } from "$lib/settings/validate"; import ActionButton from "$components/buttons/ActionButton.svelte"; @@ -15,17 +16,57 @@ const updateSettings = (reader: FileReader) => { try { const data = reader.result?.toString(); - if (!data) { - throw "data is missing"; - } + if (!data) + throw $t('settings.advanced.import.no_data'); - // TODO: input is not validated at all here, which means - // someone can potentially import a broken config. - // i don't know if we should do something about it - // or just thug it out. - updateSetting(loadFromString(data)); + const loadedSettings = loadFromString(data); + if (!validateSettings(loadedSettings)) + throw $t('settings.advanced.import.invalid'); + + createDialog({ + id: "import-confirm", + type: "small", + icon: "warn-red", + title: $t("dialog.safety.title"), + bodyText: $t("dialog.import.body"), + buttons: [ + { + text: $t("dialog.button.cancel"), + main: false, + action: () => {}, + }, + { + text: $t("dialog.button.import"), + color: "red", + main: true, + timeout: 5000, + action: () => updateSetting(loadFromString(data)) + }, + ], + }); } catch (e) { - alert(e); + let message; + + if (e instanceof Error) + message = e.message; + else if (typeof e === 'string') + message = e; + else + message = $t('settings.advanced.import.no_data'); + + createDialog({ + id: "settings-import-error", + type: "small", + meowbalt: "error", + bodyText: message, + buttons: [ + { + text: $t("dialog.button.gotit"), + main: true, + action: () => {}, + }, + ], + }); } }; @@ -37,33 +78,11 @@ const target = e.target as HTMLInputElement; const reader = new FileReader(); - reader.onload = function () { - createDialog({ - id: "import-confirm", - type: "small", - icon: "warn-red", - title: $t("dialog.safety.title"), - bodyText: $t("dialog.import.body"), - buttons: [ - { - text: $t("dialog.button.cancel"), - main: false, - action: () => {}, - }, - { - text: $t("dialog.button.import"), - color: "red", - main: true, - timeout: 5000, - action: () => updateSettings(reader), - }, - ], - }); - }; + reader.onload = () => updateSettings(reader); if (target.files?.length === 1) { reader.readAsText(target.files[0]); - } else alert("file missing"); + } }; pseudoinput.click(); }; @@ -83,11 +102,11 @@
- import + {$t("settings.advanced.import")} {#if $storedSettings.schemaVersion} - export + {$t("settings.advanced.export")} {/if}
diff --git a/web/src/lib/settings/validate.ts b/web/src/lib/settings/validate.ts new file mode 100644 index 00000000..9b85bcce --- /dev/null +++ b/web/src/lib/settings/validate.ts @@ -0,0 +1,85 @@ +import type { Optional } from '$lib/types/generic'; +import defaultSettings from './defaults' +import { + downloadModeOptions, + filenameStyleOptions, + savingMethodOptions, + themeOptions, + videoQualityOptions, + youtubeVideoCodecOptions, + type PartialSettings, +} from '$lib/types/settings'; + +function validateTypes(input: unknown, reference = defaultSettings as unknown) { + if (typeof input === 'undefined') + return true; + + if (typeof input !== typeof reference) + return false; + + if (typeof reference !== 'object') + return true; + + if (reference === null || input === null) + return input === reference; + + if (Array.isArray(reference)) { + // TODO: we dont expect the reference array to hold any + // elements, but we should at maybe check whether + // the input array types are all matching. + return true; + } + + // we know that `input` is an `object` based on the first + // two `if`s, but for some reason typescript doesn't. :) + if (typeof input !== 'object') + return false; + + const keys = new Set([ + ...Object.keys(input), + ...Object.keys(reference) + ]); + + for (const key of keys) { + const _input = input as Record; + const _reference = reference as Record; + + if (!validateTypes(_input[key], _reference[key])) { + return false; + } + } + + return true; +} + +function validateLiteral(value: Optional, allowed: readonly string[]) { + return value === undefined || allowed.includes(value); +} + +function validateLiterals(literals: [Optional, readonly string[]][]) { + for (const [ value, allowed ] of literals) { + if (!validateLiteral(value, allowed)) + return false; + } + + return true; +} + +// performs a basic check on an "untrusted" settings object. +export function validateSettings(settings: PartialSettings) { + if (!settings?.schemaVersion) { + return false; + } + + return ( + validateTypes(settings) + && validateLiterals([ + [ settings?.appearance?.theme , themeOptions ], + [ settings?.save?.downloadMode , downloadModeOptions ], + [ settings?.save?.filenameStyle , filenameStyleOptions ], + [ settings?.save?.videoQuality , videoQualityOptions ], + [ settings?.save?.youtubeVideoCodec, youtubeVideoCodecOptions ], + [ settings?.save?.savingMethod , savingMethodOptions ] + ]) + ); +}