111 lines
4.4 KiB
JavaScript
111 lines
4.4 KiB
JavaScript
/** Field block types that don't carry a submittable value. */ const NON_DATA_BLOCKS = new Set([
|
|
'message'
|
|
]);
|
|
/**
|
|
* Keys injected by the captcha widget itself, not by the form definition.
|
|
* Cloudflare Turnstile adds a hidden <input name="cf-turnstile-response"> after
|
|
* a successful challenge; reCAPTCHA adds 'g-recaptcha-response'. Since the
|
|
* plugin drives Turnstile end-to-end, these are legitimate artifacts — they
|
|
* must not count as "unknown fields" and trip the anti-tampering check.
|
|
*/ const CAPTCHA_KEYS = new Set([
|
|
'cf-turnstile-response',
|
|
'g-recaptcha-response'
|
|
]);
|
|
/** Hard ceiling on a single field's length, independent of the form config. */ const MAX_FIELD_LENGTH = 5000;
|
|
/**
|
|
* Default name for a GDPR consent field. A checkbox with this name is treated
|
|
* as a consent gate: it MUST be checked for the submission to go through,
|
|
* enforced here server-side regardless of how the field was configured in the
|
|
* panel (so an editor can't weaken it by forgetting `required` or, worse,
|
|
* pre-ticking it with defaultValue: true — which GDPR forbids). Configurable
|
|
* via FormsOption.consentFieldName.
|
|
*/ const DEFAULT_CONSENT_FIELD = 'consent';
|
|
/**
|
|
* Checks submitted data against the form's own definition, rather than trusting
|
|
* whatever arrived.
|
|
*
|
|
* The server action is a public endpoint: a caller can skip the rendered form
|
|
* and post arbitrary keys. Without this, unknown fields would be stored,
|
|
* required fields could be missing, and an oversized value could sail through.
|
|
* So we load the form, keep only keys that are real fields, reject when a
|
|
* required one is blank, and cap length.
|
|
*
|
|
* Returns the loaded form on success so the caller doesn't fetch it twice, and
|
|
* a code + offending field on failure so the frontend can point at it.
|
|
*/ export async function validateSubmission(payload, formId, data, consentFieldName = DEFAULT_CONSENT_FIELD) {
|
|
let form;
|
|
try {
|
|
form = await payload.findByID({
|
|
id: formId,
|
|
collection: 'forms',
|
|
depth: 0
|
|
});
|
|
} catch {
|
|
return {
|
|
ok: false,
|
|
reason: 'not_found'
|
|
};
|
|
}
|
|
const fields = (form.fields ?? []).filter((f)=>typeof f.name === 'string' && !NON_DATA_BLOCKS.has(f.blockType ?? ''));
|
|
const known = new Map(fields.map((f)=>[
|
|
f.name,
|
|
f
|
|
]));
|
|
const cleaned = {};
|
|
for (const field of fields){
|
|
const value = data[field.name];
|
|
const isBlank = value == null || typeof value === 'string' && value.trim() === '' || value === false;
|
|
// GDPR consent gate: a field matching the consent name must be truthy
|
|
// (checked). Enforced independently of `required`, so it can't be weakened
|
|
// in the panel. This is the one field where server-side enforcement is the
|
|
// legal guarantee — the frontend can't bypass it, the editor can't misset it.
|
|
if (field.name === consentFieldName) {
|
|
if (value !== true) {
|
|
return {
|
|
field: field.name,
|
|
ok: false,
|
|
reason: 'consent'
|
|
};
|
|
}
|
|
} else if (field.required && isBlank) {
|
|
return {
|
|
field: field.name,
|
|
kind: 'required',
|
|
ok: false,
|
|
reason: 'invalid'
|
|
};
|
|
}
|
|
if (typeof value === 'string' && value.length > MAX_FIELD_LENGTH) {
|
|
return {
|
|
field: field.name,
|
|
kind: 'too_long',
|
|
ok: false,
|
|
reason: 'invalid'
|
|
};
|
|
}
|
|
// Only carry through keys that belong to the form — unknown keys from a
|
|
// hand-crafted request are dropped, not stored.
|
|
if (value !== undefined) {
|
|
cleaned[field.name] = value;
|
|
}
|
|
}
|
|
// Reject outright if the payload carried keys the form doesn't define — a
|
|
// sign the request wasn't produced by the rendered form. Captcha keys are
|
|
// exempt: the widget injects them into the rendered form, so they're expected,
|
|
// not tampering.
|
|
const unknownKeys = Object.keys(data).filter((k)=>!known.has(k) && !CAPTCHA_KEYS.has(k));
|
|
if (unknownKeys.length > 0) {
|
|
return {
|
|
kind: 'unknown_fields',
|
|
ok: false,
|
|
reason: 'invalid'
|
|
};
|
|
}
|
|
return {
|
|
cleaned,
|
|
form,
|
|
ok: true
|
|
};
|
|
}
|
|
|
|
//# sourceMappingURL=validateSubmission.js.map
|