fix: mirror server custom CSS validation on the client
The form only enforced the length cap, so CSS with an external url() or @import passed client validation and was rejected by the server. Add the same tokenizer based check with the same error message so it is caught inline before submit.
This commit is contained in:
1 parent
a4cc322a55
commit
ea23610b3a
4 files changed
+41
No files matched your search
Generated
+20
@@ -8,6 +8,7 @@
|
||||
"name": "client",
|
||||
"version": "3.8.1",
|
||||
"dependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
@@ -303,6 +304,25 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "4.2.0",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"dependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { isTokenString, isTokenURL, tokenize } from "@csstools/css-tokenizer";
|
||||
|
||||
// Mirrors the server check (server/src/utils/customCss.ts) so the form rejects
|
||||
// the same CSS the API would.
|
||||
const EXTERNAL_TARGET = /^\s*(?:https?:|\/\/)/i;
|
||||
|
||||
export const cssReferencesExternalResource = (css?: string | null): boolean => {
|
||||
if (typeof css !== "string" || css === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return tokenize({ css }).some(
|
||||
(token) =>
|
||||
(isTokenURL(token) || isTokenString(token)) && EXTERNAL_TARGET.test(token[4].value)
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import type { FieldPath } from "react-hook-form";
|
||||
import { STATUS_PAGE_THEMES, STATUS_PAGE_THEME_MODES } from "@/Types/StatusPage";
|
||||
import { cssReferencesExternalResource } from "@/Utils/customCss";
|
||||
|
||||
// Wizard step a field is validated on. Attached inline to each field below so
|
||||
// the grouping lives next to the field definition; unannotated fields default
|
||||
@@ -61,6 +62,9 @@ export const statusPageSchema = z.object({
|
||||
customCSS: z
|
||||
.string()
|
||||
.max(100000, "Custom CSS must be at most 100000 characters")
|
||||
.refine((css) => !cssReferencesExternalResource(css), {
|
||||
message: "Custom CSS cannot reference external URLs or use @import",
|
||||
})
|
||||
.optional()
|
||||
.register(statusPageStepRegistry, { step: 1 }),
|
||||
theme: z
|
||||
|
||||
Reference in new issue
Block a user