From ae9536db3a3091946829e5e2fe0eec72dddcbb54 Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Fri, 12 Jun 2026 12:36:47 +0200 Subject: [PATCH] fix(forms): unsichtbare Unicode-Zeichen in Eingaben strippen + FormCard-Rahmen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cleanText(): Zero-Width-/Bidi-Marks/BOM (Copy-Paste aus iOS/WhatsApp) vor Validierung und im Payload entfernen — Telefonnummern mit U+202D/U+202C fielen sonst durch die Validierung - FormCard.svelte: gerahmter Container, umschließt Kontakt- und Mitmachen-Formular Co-Authored-By: Claude Fable 5 --- .../blocks/InternalComponentBlock.svelte | 9 +++++-- src/lib/components/internal/FormCard.svelte | 27 +++++++++++++++++++ .../internal/mitmachen-form-validation.ts | 12 ++++++++- src/routes/api/mitmachen/+server.ts | 13 +++++---- 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/lib/components/internal/FormCard.svelte diff --git a/src/lib/components/blocks/InternalComponentBlock.svelte b/src/lib/components/blocks/InternalComponentBlock.svelte index 8bf76c5..2d6c29c 100644 --- a/src/lib/components/blocks/InternalComponentBlock.svelte +++ b/src/lib/components/blocks/InternalComponentBlock.svelte @@ -3,6 +3,7 @@ import type { InternalComponentBlockData } from "$lib/block-types"; import ContactFormComponent from "$lib/components/internal/ContactFormComponent.svelte"; import MitmachenFormComponent from "$lib/components/internal/MitmachenFormComponent.svelte"; + import FormCard from "$lib/components/internal/FormCard.svelte"; let { block }: { block: InternalComponentBlockData } = $props(); @@ -12,9 +13,13 @@
{#if component === "form-contact"} - + + + {:else if component === "form-mitmachen"} - + + + {:else}

Unbekannte interne Komponente: {component}

{/if} diff --git a/src/lib/components/internal/FormCard.svelte b/src/lib/components/internal/FormCard.svelte new file mode 100644 index 0000000..cbd11fa --- /dev/null +++ b/src/lib/components/internal/FormCard.svelte @@ -0,0 +1,27 @@ + + +
+ {#if title} +
+

{title}

+ {#if subtitle} +

{subtitle}

+ {/if} +
+ {/if} + {@render children()} +
diff --git a/src/lib/components/internal/mitmachen-form-validation.ts b/src/lib/components/internal/mitmachen-form-validation.ts index 5152ea5..f702094 100644 --- a/src/lib/components/internal/mitmachen-form-validation.ts +++ b/src/lib/components/internal/mitmachen-form-validation.ts @@ -52,8 +52,18 @@ const URL_RE = /\b(?:https?:\/\/|www\.)\S+/gi; const CONTROL_CHAR_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F]/; const PHONE_RE = /^[0-9+()/\-.\s]*$/; +/** Unsichtbare Format-Zeichen: Zero-Width, Bidi-Marks (U+202A–E), Word-Joiner, + * BOM. Landen beim Kopieren aus iOS-Kontakten/WhatsApp in Telefonnummern und + * lassen sonst die Validierung durchfallen. */ +const INVISIBLE_RE = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/g; + +/** Entfernt unsichtbare Format-Zeichen + trimmt. Für Validierung UND Payload. */ +export function cleanText(s: unknown): string { + return typeof s === "string" ? s.replace(INVISIBLE_RE, "").trim() : ""; +} + function trim(s: unknown): string { - return typeof s === "string" ? s.trim() : ""; + return cleanText(s); } export function validateMitmachen(input: Partial): MitmachenErrors { diff --git a/src/routes/api/mitmachen/+server.ts b/src/routes/api/mitmachen/+server.ts index ab20afd..6fa27d3 100644 --- a/src/routes/api/mitmachen/+server.ts +++ b/src/routes/api/mitmachen/+server.ts @@ -3,6 +3,7 @@ import { env as publicEnv } from "$env/dynamic/public"; import { env as privateEnv } from "$env/dynamic/private"; import type { RequestHandler } from "./$types"; import { + cleanText, HONEYPOT_FIELDS, LIMITS, validateFiles, @@ -108,12 +109,14 @@ export const POST: RequestHandler = async ({ request, getClientAddress }) => { ? `${cmsBase}/api/forms/mitmachen/submit-multipart?_environment=${encodeURIComponent(cmsEnv)}` : `${cmsBase}/api/forms/mitmachen/submit-multipart`; + // cleanText: unsichtbare Format-Zeichen (Bidi-Marks aus Copy-Paste) raus — + // gleiche Normalisierung wie in der Validierung. const payload = { - title: fields.title.trim(), - description: fields.description.trim(), - contact_name: fields.contactName.trim(), - phone: fields.phone.trim(), - email: fields.email.trim(), + title: cleanText(fields.title), + description: cleanText(fields.description), + contact_name: cleanText(fields.contactName), + phone: cleanText(fields.phone), + email: cleanText(fields.email), consent_at: new Date().toISOString(), submitted_via: "windwiderstand.de/api/mitmachen", user_agent: (request.headers.get("user-agent") ?? "").slice(0, 200),