From fd84651a16c9ca3dcf9656098366a4dc4ea285b8 Mon Sep 17 00:00:00 2001 From: AcKindle3 Date: Fri, 7 Apr 2023 17:13:00 -0400 Subject: [PATCH] Localstorage: Use `zod` to parse type `FormData`. Local storage is an untyped interface external to the frontend code itself. The `data` field after `JSON.parse`'d from `raw_data` can be further validated using `zod`'s schema `formDataSchema`. The test case `server_upgrade_alert hide_duration_expired` in `navbar_alerts.test.js` has a bug at `start_time`, which is fixed in this commit. `start_time` is a mock value of `Date.now()` used in `localstorage.ts`, which will concatenate with a number `expires`. So `start_time` was supposed to be an integer value. Before fix, `new Date(1620327447050)` returns a `Date` object which is wrongly concatenated with `expires`. Fixes #24997. --- web/src/localstorage.ts | 36 ++++++++++++++++++++------------- web/tests/navbar_alerts.test.js | 4 ++-- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/web/src/localstorage.ts b/web/src/localstorage.ts index 2d7a39cd0f..39ced48441 100644 --- a/web/src/localstorage.ts +++ b/web/src/localstorage.ts @@ -1,10 +1,18 @@ +import {z} from "zod"; + import * as blueslip from "./blueslip"; -type FormData = { - data: unknown; - __valid: true; - expires: number | null; -}; +const formDataSchema = z + .object({ + data: z.unknown(), + __valid: z.literal(true), + expires: z.number().nullable(), + }) + // z.unknown by default marks the field as optional. + // Use zod transform to make optional data field non-optional. + .transform((o) => ({data: o.data, ...o})); + +type FormData = z.infer; export type LocalStorage = { setExpiry(expires: number, isGlobal: boolean): LocalStorage; @@ -53,18 +61,18 @@ const ls = { return undefined; } data = JSON.parse(raw_data); + data = formDataSchema.parse(data); + if ( + // JSON forms of data with `Infinity` turns into `null`, + // so if null then it hasn't expired since nothing was specified. + data.expires === null || + !ls.isExpired(data.expires) + ) { + return data; + } } catch { // data stays undefined } - if ( - data && - data.__valid && - // JSON forms of data with `Infinity` turns into `null`, - // so if null then it hasn't expired since nothing was specified. - (data.expires === null || !ls.isExpired(data.expires)) - ) { - return data; - } return undefined; }, diff --git a/web/tests/navbar_alerts.test.js b/web/tests/navbar_alerts.test.js index 43214fac12..26188c8e19 100644 --- a/web/tests/navbar_alerts.test.js +++ b/web/tests/navbar_alerts.test.js @@ -91,7 +91,7 @@ test("profile_incomplete_alert", () => { test("server_upgrade_alert hide_duration_expired", ({override}) => { const ls = localstorage(); - const start_time = new Date(1620327447050); // Thursday 06/5/2021 07:02:27 AM (UTC+0) + const start_time = 1620327447050; // Thursday 06/5/2021 07:02:27 AM (UTC+0) override(Date, "now", () => start_time); assert.equal(ls.get("lastUpgradeNagDismissalTime"), undefined); @@ -99,7 +99,7 @@ test("server_upgrade_alert hide_duration_expired", ({override}) => { navbar_alerts.dismiss_upgrade_nag(ls); assert.equal(navbar_alerts.should_show_server_upgrade_notification(ls), false); - override(Date, "now", () => addDays(start_time, 8)); // Friday 14/5/2021 07:02:27 AM (UTC+0) + override(Date, "now", () => addDays(start_time, 8).getTime()); // Friday 14/5/2021 07:02:27 AM (UTC+0) assert.equal(navbar_alerts.should_show_server_upgrade_notification(ls), true); navbar_alerts.dismiss_upgrade_nag(ls); assert.equal(navbar_alerts.should_show_server_upgrade_notification(ls), false);