Skip to content

Commit 1cada77

Browse files
authored
fix: sanitize result filters before storing in LS (@fehmer) (monkeytypegame#6583)
1 parent 19930a9 commit 1cada77

File tree

4 files changed

+118
-40
lines changed

4 files changed

+118
-40
lines changed

frontend/__tests__/utils/misc.spec.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { deepClone, getErrorMessage, isObject } from "../../src/ts/utils/misc";
1+
import { z } from "zod";
2+
import {
3+
deepClone,
4+
getErrorMessage,
5+
isObject,
6+
sanitize,
7+
} from "../../src/ts/utils/misc";
28
import {
39
getLanguageDisplayString,
410
removeLanguageSize,
@@ -224,4 +230,56 @@ describe("misc.ts", () => {
224230
});
225231
});
226232
});
233+
describe("sanitize function", () => {
234+
const schema = z.object({
235+
name: z.string(),
236+
age: z.number().positive(),
237+
tags: z.array(z.string()),
238+
});
239+
240+
it("should return the same object if it is valid", () => {
241+
const obj = { name: "Alice", age: 30, tags: ["developer", "coder"] };
242+
expect(sanitize(schema, obj)).toEqual(obj);
243+
});
244+
245+
it("should remove properties with invalid values", () => {
246+
const obj = { name: "Alice", age: -5, tags: ["developer", "coder"] };
247+
expect(sanitize(schema, obj)).toEqual({
248+
name: "Alice",
249+
tags: ["developer", "coder"],
250+
age: undefined,
251+
});
252+
});
253+
254+
it("should remove invalid array elements", () => {
255+
const obj = {
256+
name: "Alice",
257+
age: 30,
258+
tags: ["developer", 123, "coder"] as any,
259+
};
260+
expect(sanitize(schema, obj)).toEqual({
261+
name: "Alice",
262+
age: 30,
263+
tags: ["developer", "coder"],
264+
});
265+
});
266+
267+
it("should remove entire property if all array elements are invalid", () => {
268+
const obj = { name: "Alice", age: 30, tags: [123, 456] as any };
269+
expect(sanitize(schema, obj)).toEqual({
270+
name: "Alice",
271+
age: 30,
272+
tags: undefined,
273+
});
274+
});
275+
276+
it("should remove object properties if they are invalid", () => {
277+
const obj = { name: 123 as any, age: 30, tags: ["developer", "coder"] };
278+
expect(sanitize(schema, obj)).toEqual({
279+
age: 30,
280+
tags: ["developer", "coder"],
281+
name: undefined,
282+
});
283+
});
284+
});
227285
});

frontend/src/ts/elements/account/result-filters.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ const resultFiltersLS = new LocalStorageWithSchema({
5555
if (!Misc.isObject(unknown)) {
5656
return defaultResultFilters;
5757
}
58-
return mergeWithDefaultFilters(unknown as ResultFilters);
58+
return mergeWithDefaultFilters(
59+
Misc.sanitize(ResultFiltersSchema, unknown as ResultFilters)
60+
);
5961
},
6062
});
6163

@@ -89,6 +91,7 @@ function save(): void {
8991
export async function load(): Promise<void> {
9092
try {
9193
filters = resultFiltersLS.get();
94+
console.log("###", { filters });
9295

9396
const newTags: Record<string, boolean> = { none: false };
9497
Object.keys(defaultResultFilters.tags).forEach((tag) => {
@@ -889,7 +892,8 @@ $(".group.presetFilterButtons .filterBtns").on(
889892
);
890893

891894
function verifyResultFiltersStructure(filterIn: ResultFilters): ResultFilters {
892-
const filter = Misc.deepClone(filterIn);
895+
const filter = Misc.sanitize(ResultFiltersSchema, Misc.deepClone(filterIn));
896+
893897
Object.entries(defaultResultFilters).forEach((entry) => {
894898
const key = entry[0] as ResultFiltersGroup;
895899
const value = entry[1];

frontend/src/ts/utils/config.ts

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
PartialConfig,
55
FunboxName,
66
} from "@monkeytype/contracts/schemas/configs";
7-
import { typedKeys } from "./misc";
7+
import { sanitize, typedKeys } from "./misc";
88
import * as ConfigSchemas from "@monkeytype/contracts/schemas/configs";
99
import { getDefaultConfig } from "../constants/default-config";
1010
/**
@@ -33,42 +33,7 @@ function mergeWithDefaultConfig(config: PartialConfig): Config {
3333
function sanitizeConfig(
3434
config: ConfigSchemas.PartialConfig
3535
): ConfigSchemas.PartialConfig {
36-
const validate = ConfigSchemas.PartialConfigSchema.safeParse(config);
37-
38-
if (validate.success) {
39-
return config;
40-
}
41-
42-
const errors: Map<string, number[] | undefined> = new Map();
43-
for (const error of validate.error.errors) {
44-
const element = error.path[0] as string;
45-
let val = errors.get(element);
46-
if (typeof error.path[1] === "number") {
47-
val = [...(val ?? []), error.path[1]];
48-
}
49-
errors.set(element, val);
50-
}
51-
52-
return Object.fromEntries(
53-
Object.entries(config).map(([key, value]) => {
54-
if (!errors.has(key)) {
55-
return [key, value];
56-
}
57-
58-
const error = errors.get(key);
59-
60-
if (
61-
Array.isArray(value) &&
62-
error !== undefined && //error is not on the array itself
63-
error.length < value.length //not all items in the array are invalid
64-
) {
65-
//some items of the array are invalid
66-
return [key, value.filter((_element, index) => !error.includes(index))];
67-
} else {
68-
return [key, undefined];
69-
}
70-
})
71-
) as ConfigSchemas.PartialConfig;
36+
return sanitize(ConfigSchemas.PartialConfigSchema, config);
7237
}
7338

7439
export function replaceLegacyValues(

frontend/src/ts/utils/misc.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
CustomTextDataWithTextLen,
1212
Result,
1313
} from "@monkeytype/contracts/schemas/results";
14+
import { z } from "zod";
1415

1516
export function whorf(speed: number, wordlen: number): number {
1617
return Math.min(
@@ -713,4 +714,54 @@ export function promiseWithResolvers<T = void>(): {
713714
return { resolve, reject, promise };
714715
}
715716

717+
/**
718+
* Sanitize object. Remove invalid values based on the schema.
719+
* @param schema zod schema
720+
* @param obj object
721+
* @returns sanitized object
722+
*/
723+
export function sanitize<T extends z.ZodTypeAny>(
724+
schema: T,
725+
obj: z.infer<T>
726+
): z.infer<T> {
727+
const validate = schema.safeParse(obj);
728+
729+
if (validate.success) {
730+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
731+
return obj;
732+
}
733+
734+
const errors: Map<string, number[] | undefined> = new Map();
735+
for (const error of validate.error.errors) {
736+
const element = error.path[0] as string;
737+
let val = errors.get(element);
738+
if (typeof error.path[1] === "number") {
739+
val = [...(val ?? []), error.path[1]];
740+
}
741+
errors.set(element, val);
742+
}
743+
744+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
745+
return Object.fromEntries(
746+
Object.entries(obj).map(([key, value]) => {
747+
if (!errors.has(key)) {
748+
return [key, value];
749+
}
750+
751+
const error = errors.get(key);
752+
753+
if (
754+
Array.isArray(value) &&
755+
error !== undefined && //error is not on the array itself
756+
error.length < value.length //not all items in the array are invalid
757+
) {
758+
//some items of the array are invalid
759+
return [key, value.filter((_element, index) => !error.includes(index))];
760+
} else {
761+
return [key, undefined];
762+
}
763+
})
764+
) as z.infer<T>;
765+
}
766+
716767
// DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES

0 commit comments

Comments
 (0)