mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-19 00:57:55 -05:00
Safe string schema with check against zalgo text
This commit is contained in:
parent
3ac6712091
commit
e1331ef4bb
|
|
@ -1,20 +1,20 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
_action,
|
||||
actuallyNonEmptyStringOrNull,
|
||||
customCssVarObject,
|
||||
falsyToNull,
|
||||
id,
|
||||
safeStringSchema,
|
||||
} from "~/utils/zod";
|
||||
import { TEAM, TEAM_MEMBER_ROLES } from "./team-constants";
|
||||
|
||||
export const teamParamsSchema = z.object({ customUrl: z.string() });
|
||||
|
||||
export const createTeamSchema = z.object({
|
||||
name: z.preprocess(
|
||||
actuallyNonEmptyStringOrNull,
|
||||
z.string().min(TEAM.NAME_MIN_LENGTH).max(TEAM.NAME_MAX_LENGTH),
|
||||
),
|
||||
name: safeStringSchema({
|
||||
min: TEAM.NAME_MIN_LENGTH,
|
||||
max: TEAM.NAME_MAX_LENGTH,
|
||||
}),
|
||||
});
|
||||
|
||||
export const teamProfilePageActionSchema = z.union([
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
_action,
|
||||
actuallyNonEmptyStringOrNull,
|
||||
checkboxValueToBoolean,
|
||||
id,
|
||||
modeShort,
|
||||
optionalId,
|
||||
safeJSONParse,
|
||||
safeStringSchema,
|
||||
stageId,
|
||||
} from "~/utils/zod";
|
||||
import { bracketIdx } from "../tournament-bracket/tournament-bracket-schemas.server";
|
||||
import { TOURNAMENT } from "./tournament-constants";
|
||||
|
||||
export const teamName = z.preprocess(
|
||||
actuallyNonEmptyStringOrNull,
|
||||
z.string().max(TOURNAMENT.TEAM_NAME_MAX_LENGTH),
|
||||
);
|
||||
export const teamName = safeStringSchema({
|
||||
max: TOURNAMENT.TEAM_NAME_MAX_LENGTH,
|
||||
});
|
||||
|
||||
export const registerSchema = z.union([
|
||||
z.object({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import "~/styles/u-edit.css";
|
|||
import { isCustomUrl } from "~/utils/urls";
|
||||
import {
|
||||
actualNumber,
|
||||
actuallyNonEmptyStringOrNull,
|
||||
checkboxValueToDbBoolean,
|
||||
customCssVarObject,
|
||||
dbBoolean,
|
||||
|
|
@ -13,6 +12,7 @@ import {
|
|||
id,
|
||||
processMany,
|
||||
safeJSONParse,
|
||||
safeNullableStringSchema,
|
||||
undefinedToNull,
|
||||
weaponSplId,
|
||||
} from "~/utils/zod";
|
||||
|
|
@ -62,10 +62,7 @@ export const userEditActionSchema = z
|
|||
.transform((val) => val?.toLowerCase())
|
||||
.nullable(),
|
||||
),
|
||||
customName: z.preprocess(
|
||||
actuallyNonEmptyStringOrNull,
|
||||
z.string().max(USER.CUSTOM_NAME_MAX_LENGTH).nullable(),
|
||||
),
|
||||
customName: safeNullableStringSchema({ max: USER.CUSTOM_NAME_MAX_LENGTH }),
|
||||
battlefy: z.preprocess(
|
||||
falsyToNull,
|
||||
z.string().max(USER.BATTLEFY_MAX_LENGTH).nullable(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeFriendCode } from "./zod";
|
||||
import { hasZalgo, normalizeFriendCode } from "./zod";
|
||||
|
||||
describe("normalizeFriendCode", () => {
|
||||
it("returns well formatted friend code as is", () => {
|
||||
|
|
@ -18,3 +18,25 @@ describe("normalizeFriendCode", () => {
|
|||
expect(normalizeFriendCode("SW-1234-56789012")).toBe("1234-5678-9012");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasZalgo", () => {
|
||||
it("returns true for text containing Zalgo characters", () => {
|
||||
expect(hasZalgo("z͎͗ͣḁ̵̑l̉̃ͦg̐̓̒o͓̔ͥ")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for text without Zalgo characters", () => {
|
||||
expect(hasZalgo("normal text")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for an empty string", () => {
|
||||
expect(hasZalgo("")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for text with special but non-Zalgo characters", () => {
|
||||
expect(hasZalgo("!@#$%^&*()")).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts japanese characters", () => {
|
||||
expect(hasZalgo("こんにちは")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -123,10 +123,50 @@ export function safeJSONParse(value: unknown): unknown {
|
|||
const EMPTY_CHARACTERS = ["\u200B", "\u200C", "\u200D", "\u200E", "\u200F"];
|
||||
const EMPTY_CHARACTERS_REGEX = new RegExp(EMPTY_CHARACTERS.join("|"), "g");
|
||||
|
||||
const zalgoRe = /%CC%/g;
|
||||
export const hasZalgo = (txt: string) => zalgoRe.test(encodeURIComponent(txt));
|
||||
|
||||
/** Non-empty string that has the given length (max and optionally min). Prevents z͎͗ͣḁ̵̑l̉̃ͦg̐̓̒o͓̔ͥ text as well as filters out characters that have no width. */
|
||||
export const safeStringSchema = ({ min, max }: { min?: number; max: number }) =>
|
||||
z.preprocess(
|
||||
actuallyNonEmptyStringOrNull, // if this returns null, none of the checks below will run because it's not a string
|
||||
z
|
||||
.string()
|
||||
.min(min ?? 0)
|
||||
.max(max)
|
||||
.refine((text) => !hasZalgo(text), {
|
||||
message: "Includes not allowed characters.",
|
||||
}),
|
||||
);
|
||||
|
||||
/** Nullable string that has the given length (max and optionally min). Prevents z͎͗ͣḁ̵̑l̉̃ͦg̐̓̒o͓̔ͥ text as well as filters out characters that have no width. */
|
||||
export const safeNullableStringSchema = ({
|
||||
min,
|
||||
max,
|
||||
}: { min?: number; max: number }) =>
|
||||
z.preprocess(
|
||||
actuallyNonEmptyStringOrNull,
|
||||
z
|
||||
.string()
|
||||
.min(min ?? 0)
|
||||
.max(max)
|
||||
.nullable()
|
||||
.refine(
|
||||
(text) => {
|
||||
if (typeof text !== "string") return true;
|
||||
|
||||
return !hasZalgo(text);
|
||||
},
|
||||
{
|
||||
message: "Includes not allowed characters.",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Processes the input value and returns a non-empty string with invisible characters cleaned out or null.
|
||||
*/
|
||||
export function actuallyNonEmptyStringOrNull(value: unknown) {
|
||||
function actuallyNonEmptyStringOrNull(value: unknown) {
|
||||
if (typeof value !== "string") return value;
|
||||
|
||||
const trimmed = value.replace(EMPTY_CHARACTERS_REGEX, "").trim();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user