diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 100032beff17..d6965c3f5616 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -108,6 +108,7 @@ import * as FPSCounter from "../elements/fps-counter"; import { migrateConfig } from "../utils/config"; import { PartialConfigSchema } from "@monkeytype/contracts/schemas/configs"; import { Command, CommandsSubgroup } from "./types"; +import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; const layoutsPromise = JSONData.getLayoutsList(); layoutsPromise @@ -362,8 +363,9 @@ export const commands: CommandsSubgroup = { exec: async ({ input }): Promise => { if (input === undefined || input === "") return; try { - const parsedConfig = PartialConfigSchema.strip().parse( - JSON.parse(input) + const parsedConfig = parseJsonWithSchema( + input, + PartialConfigSchema.strip() ); await UpdateConfig.apply(migrateConfig(parsedConfig)); UpdateConfig.saveFullConfigToLocalStorage(); diff --git a/frontend/src/ts/controllers/analytics-controller.ts b/frontend/src/ts/controllers/analytics-controller.ts index 56fbc7eaf086..afb7df31c37b 100644 --- a/frontend/src/ts/controllers/analytics-controller.ts +++ b/frontend/src/ts/controllers/analytics-controller.ts @@ -6,13 +6,17 @@ import { } from "firebase/analytics"; import { app as firebaseApp } from "../firebase"; import { createErrorMessage } from "../utils/misc"; +import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; +import { z } from "zod"; let analytics: AnalyticsType; -type AcceptedCookies = { - security: boolean; - analytics: boolean; -}; +const AcceptedCookiesSchema = z.object({ + security: z.boolean(), + analytics: z.boolean(), +}); + +type AcceptedCookies = z.infer; export async function log( eventName: string, @@ -26,9 +30,14 @@ export async function log( } const lsString = localStorage.getItem("acceptedCookies"); -let acceptedCookies; +let acceptedCookies: AcceptedCookies | null; if (lsString !== undefined && lsString !== null && lsString !== "") { - acceptedCookies = JSON.parse(lsString) as AcceptedCookies; + try { + acceptedCookies = parseJsonWithSchema(lsString, AcceptedCookiesSchema); + } catch (e) { + console.error("Failed to parse accepted cookies:", e); + acceptedCookies = null; + } } else { acceptedCookies = null; } diff --git a/frontend/src/ts/event-handlers/account.ts b/frontend/src/ts/event-handlers/account.ts index af42fdc14a23..d9327ad794d0 100644 --- a/frontend/src/ts/event-handlers/account.ts +++ b/frontend/src/ts/event-handlers/account.ts @@ -5,6 +5,8 @@ import { isAuthenticated } from "../firebase"; import * as Notifications from "../elements/notifications"; import * as EditResultTagsModal from "../modals/edit-result-tags"; import * as AddFilterPresetModal from "../modals/new-filter-preset"; +import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; +import { z } from "zod"; const accountPage = document.querySelector("#pageAccount") as HTMLElement; @@ -36,12 +38,15 @@ $(accountPage).on("click", ".editProfileButton", () => { EditProfileModal.show(); }); +const TagsArraySchema = z.array(z.string()); + $(accountPage).on("click", ".group.history .resultEditTagsButton", (e) => { const resultid = $(e.target).attr("data-result-id"); const tags = $(e.target).attr("data-tags"); + EditResultTagsModal.show( resultid ?? "", - JSON.parse(tags ?? "[]") as string[], + parseJsonWithSchema(tags ?? "[]", TagsArraySchema), "accountPage" ); }); diff --git a/frontend/src/ts/modals/import-export-settings.ts b/frontend/src/ts/modals/import-export-settings.ts index ff0cb82d4add..1994064dc1ee 100644 --- a/frontend/src/ts/modals/import-export-settings.ts +++ b/frontend/src/ts/modals/import-export-settings.ts @@ -3,6 +3,7 @@ import * as UpdateConfig from "../config"; import * as Notifications from "../elements/notifications"; import AnimatedModal from "../utils/animated-modal"; import { migrateConfig } from "../utils/config"; +import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; type State = { mode: "import" | "export"; @@ -47,8 +48,9 @@ const modal = new AnimatedModal({ return; } try { - const parsedConfig = PartialConfigSchema.strip().parse( - JSON.parse(state.value) + const parsedConfig = parseJsonWithSchema( + state.value, + PartialConfigSchema.strip() ); await UpdateConfig.apply(migrateConfig(parsedConfig)); } catch (e) { diff --git a/frontend/src/ts/test/custom-text.ts b/frontend/src/ts/test/custom-text.ts index a0d59cb2ff44..9837fc554656 100644 --- a/frontend/src/ts/test/custom-text.ts +++ b/frontend/src/ts/test/custom-text.ts @@ -29,7 +29,7 @@ const customTextLongLS = new LocalStorageWithSchema({ fallback: {}, }); -const CustomTextSettingsSchema = z.object({ +export const CustomTextSettingsSchema = z.object({ text: z.array(z.string()), mode: CustomTextModeSchema, limit: z.object({ value: z.number(), mode: CustomTextLimitModeSchema }), diff --git a/frontend/src/ts/test/wikipedia.ts b/frontend/src/ts/test/wikipedia.ts index 5bed9a7c6749..44a616ce088b 100644 --- a/frontend/src/ts/test/wikipedia.ts +++ b/frontend/src/ts/test/wikipedia.ts @@ -2,6 +2,8 @@ import * as Loader from "../elements/loader"; import * as Misc from "../utils/misc"; import * as Strings from "../utils/strings"; import * as JSONData from "../utils/json-data"; +import { z } from "zod"; +import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; export async function getTLD( languageGroup: JSONData.LanguageGroup @@ -241,6 +243,18 @@ type SectionObject = { author: string; }; +// Section Schema +const SectionSchema = z.object({ + query: z.object({ + pages: z.record( + z.string(), + z.object({ + extract: z.string(), + }) + ), + }), +}); + export async function getSection(language: string): Promise { // console.log("Getting section"); Loader.show(); @@ -285,10 +299,17 @@ export async function getSection(language: string): Promise { sectionReq.onload = (): void => { if (sectionReq.readyState === 4) { if (sectionReq.status === 200) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - let sectionText = JSON.parse(sectionReq.responseText).query.pages[ - pageid.toString() - ].extract as string; + const parsedResponse = parseJsonWithSchema( + sectionReq.responseText, + SectionSchema + ); + const page = parsedResponse.query.pages[pageid.toString()]; + if (!page) { + Loader.hide(); + rej("Page not found"); + return; + } + let sectionText = page.extract; // Converting to one paragraph sectionText = sectionText.replace(/<\/p>

+/g, " "); diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 036e8d4700eb..352a258ba7ad 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -11,7 +11,11 @@ import * as Loader from "../elements/loader"; import * as AccountButton from "../elements/account-button"; import { restart as restartTest } from "../test/test-logic"; import * as ChallengeController from "../controllers/challenge-controller"; -import { Mode, Mode2 } from "@monkeytype/contracts/schemas/shared"; +import { + DifficultySchema, + Mode2Schema, + ModeSchema, +} from "@monkeytype/contracts/schemas/shared"; import { CustomBackgroundFilter, CustomBackgroundFilterSchema, @@ -19,7 +23,6 @@ import { CustomBackgroundSizeSchema, CustomThemeColors, CustomThemeColorsSchema, - Difficulty, } from "@monkeytype/contracts/schemas/configs"; import { z } from "zod"; import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; @@ -129,24 +132,36 @@ export function loadCustomThemeFromUrl(getOverride?: string): void { } } -type SharedTestSettings = [ - Mode | null, - Mode2 | null, - CustomText.CustomTextData | null, - boolean | null, - boolean | null, - string | null, - Difficulty | null, - string | null -]; +const TestSettingsSchema = z.tuple([ + ModeSchema.nullable(), + Mode2Schema.nullable(), + CustomText.CustomTextSettingsSchema.nullable(), + z.boolean().nullable(), //punctuation + z.boolean().nullable(), //numbers + z.string().nullable(), //language + DifficultySchema.nullable(), + z.string().nullable(), //funbox +]); + +type SharedTestSettings = z.infer; export function loadTestSettingsFromUrl(getOverride?: string): void { const getValue = Misc.findGetParameter("testSettings", getOverride); if (getValue === null) return; - const de = JSON.parse( - decompressFromURI(getValue) ?? "" - ) as SharedTestSettings; + let de: SharedTestSettings; + try { + const decompressed = decompressFromURI(getValue) ?? ""; + const parsed = parseJsonWithSchema(decompressed, TestSettingsSchema); + de = parsed as SharedTestSettings; // Assign after refinement + } catch (e) { + console.error("Failed to parse test settings:", e); + Notifications.add( + "Failed to load test settings from URL: " + (e as Error).message, + 0 + ); + return; + } const applied: Record = {};