mirror of
https://github.com/wukko/cobalt.git
synced 2024-11-15 12:50:01 +00:00
web/advanced: check if imported settings are valid
This commit is contained in:
parent
3d34e09e1c
commit
d1930c1dbc
3 changed files with 144 additions and 36 deletions
|
@ -101,5 +101,9 @@
|
||||||
"advanced.debug.description": "gives you access to a page with app & device info useful for debugging.",
|
"advanced.debug.description": "gives you access to a page with app & device info useful for debugging.",
|
||||||
|
|
||||||
"advanced.data": "settings data",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
updateSetting,
|
updateSetting,
|
||||||
loadFromString
|
loadFromString
|
||||||
} from "$lib/state/settings";
|
} from "$lib/state/settings";
|
||||||
|
import { validateSettings } from "$lib/settings/validate";
|
||||||
|
|
||||||
import ActionButton from "$components/buttons/ActionButton.svelte";
|
import ActionButton from "$components/buttons/ActionButton.svelte";
|
||||||
|
|
||||||
|
@ -15,17 +16,57 @@
|
||||||
const updateSettings = (reader: FileReader) => {
|
const updateSettings = (reader: FileReader) => {
|
||||||
try {
|
try {
|
||||||
const data = reader.result?.toString();
|
const data = reader.result?.toString();
|
||||||
if (!data) {
|
if (!data)
|
||||||
throw "data is missing";
|
throw $t('settings.advanced.import.no_data');
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: input is not validated at all here, which means
|
const loadedSettings = loadFromString(data);
|
||||||
// someone can potentially import a broken config.
|
if (!validateSettings(loadedSettings))
|
||||||
// i don't know if we should do something about it
|
throw $t('settings.advanced.import.invalid');
|
||||||
// or just thug it out.
|
|
||||||
updateSetting(loadFromString(data));
|
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) {
|
} 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 target = e.target as HTMLInputElement;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = function () {
|
reader.onload = () => updateSettings(reader);
|
||||||
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),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (target.files?.length === 1) {
|
if (target.files?.length === 1) {
|
||||||
reader.readAsText(target.files[0]);
|
reader.readAsText(target.files[0]);
|
||||||
} else alert("file missing");
|
}
|
||||||
};
|
};
|
||||||
pseudoinput.click();
|
pseudoinput.click();
|
||||||
};
|
};
|
||||||
|
@ -83,11 +102,11 @@
|
||||||
|
|
||||||
<div class="button-row" id="settings-data-transfer">
|
<div class="button-row" id="settings-data-transfer">
|
||||||
<ActionButton id="import-settings" click={importSettings}>
|
<ActionButton id="import-settings" click={importSettings}>
|
||||||
<IconFileImport /> import
|
<IconFileImport /> {$t("settings.advanced.import")}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{#if $storedSettings.schemaVersion}
|
{#if $storedSettings.schemaVersion}
|
||||||
<ActionButton id="export-settings" click={exportSettings}>
|
<ActionButton id="export-settings" click={exportSettings}>
|
||||||
<IconFileExport /> export
|
<IconFileExport /> {$t("settings.advanced.export")}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
85
web/src/lib/settings/validate.ts
Normal file
85
web/src/lib/settings/validate.ts
Normal 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 ]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue