web/advanced: check if imported settings are valid

This commit is contained in:
dumbmoron 2024-07-30 16:53:46 +00:00
parent 3d34e09e1c
commit d1930c1dbc
No known key found for this signature in database
3 changed files with 144 additions and 36 deletions

View file

@ -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"
}

View file

@ -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 @@
<div class="button-row" id="settings-data-transfer">
<ActionButton id="import-settings" click={importSettings}>
<IconFileImport /> import
<IconFileImport /> {$t("settings.advanced.import")}
</ActionButton>
{#if $storedSettings.schemaVersion}
<ActionButton id="export-settings" click={exportSettings}>
<IconFileExport /> export
<IconFileExport /> {$t("settings.advanced.export")}
</ActionButton>
{/if}
</div>

View file

@ -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<string, unknown>;
const _reference = reference as Record<string, unknown>;
if (!validateTypes(_input[key], _reference[key])) {
return false;
}
}
return true;
}
function validateLiteral(value: Optional<string>, allowed: readonly string[]) {
return value === undefined || allowed.includes(value);
}
function validateLiterals(literals: [Optional<string>, 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 ]
])
);
}