diff --git a/.gitignore b/.gitignore index 7c563b0..9273be7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Secrets — nur config.example.php wird committed /config/config.php +/config/config.production.php # Composer /vendor/ @@ -8,6 +9,10 @@ /storage/logs/* /storage/cache/* /storage/ratelimit/* +# Druck-QR-Codes (bin/qr.php) — Zwischenprodukte für Canva, kein Repo-Inhalt. +# Wichtig: sie enthalten die base_url fest eingebacken, ein versehentlich +# eingecheckter localhost-Code wäre eine Falle. +/storage/qr/ !/storage/logs/.gitkeep !/storage/cache/.gitkeep !/storage/ratelimit/.gitkeep @@ -22,6 +27,15 @@ /public/assets/img/crests/* !/public/assets/img/crests/.gitkeep +# Dateiablage /dateien — Nutzerdaten und Laufzeit-Kram der Files Gallery. +# Committed bleiben nur system/users/*/config.php (Rechte, keine Passwörter) und +# system/include/* (Skin). system/config/config.php generiert die App selbst und +# überschreibt sie bei jedem Update — unsere Werte stehen in _filesconfig.php. +/storage/dateien/uploads/* +!/storage/dateien/uploads/.gitkeep +/storage/dateien/system/cache/ +/storage/dateien/system/config/ + # Referenz-Material der alten Seite — NIE committen # (.context_db enthält personenbezogene Daten: Mail-Logs, Hashes) /.context/ diff --git a/app/actions/contact-submit.php b/app/actions/contact-submit.php index 1bba360..022f912 100644 --- a/app/actions/contact-submit.php +++ b/app/actions/contact-submit.php @@ -7,7 +7,12 @@ declare(strict_types=1); * Antwort: PRG-Redirect zurück zur absendenden Seite (#kontakt, ?sent=1 | ?error=…), * JSON bei fetch (form.js) oder — bei Feldfehlern ohne JS — die Seite direkt * mit HTTP 422, erhaltenen Eingaben und Feldfehlern (render_form_invalid()). - * Das Formular kommt von / wie auch von /kontakt. + * Das Formular kommt von / wie auch von /kontakt und /turnen. + * + * Eine Action für alle Varianten: `department` (Hidden-Feld) wählt über + * contact_variant() Auswahlliste, Feldregeln und Empfänger. Eine zweite Action + * für die Turnabteilung hätte den kompletten Spam-Schutz verdoppelt — und damit + * eine zweite Stelle, die man bei jeder Änderung mitziehen müsste. */ $field = static fn (string $key): string => trim((string) ($_POST[$key] ?? '')); @@ -36,8 +41,7 @@ $token = $field('ft'); // Ohne vollständige config/config.php liefe die Seite auf der Vorlage (öffentliches // app_secret, kein SMTP-Passwort) — dann gar nicht erst verarbeiten. -if (config_problems() !== []) { - error_log('[' . date('c') . "] {$route}: Formular abgewiesen, Konfiguration unvollständig (php bin/preflight.php).\n", 3, STORAGE_PATH . '/logs/mail.log'); +if (!config_ready($route)) { $respond(false, 'mail'); } @@ -60,7 +64,16 @@ $subject = $field('subject'); $message = $field('message'); $privacy = ($_POST['privacy'] ?? '') === '1'; -$interests = ['', 'Herrenfußball', 'Frauenfußball', 'Jugendfußball', 'Turnen', 'Sonstiges']; +// Variante bestimmen (unbekannter Wert fällt auf die allgemeine zurück, siehe +// contact_variant()) — dieselbe Quelle, aus der die Komponente ihr Formular baut. +$variant = contact_variant($field('department')); +$interests = $variant['interests']; +if (!$variant['interest_required']) { + $interests[] = ''; // die allgemeine Variante darf das Feld leer lassen +} +if (!$variant['ask_subject']) { + $subject = ''; // Feld gibt es dort nicht, ein mitgeschicktes ignorieren +} // --- Validierung (Whitelist-Prinzip, Fehler pro Feld) --- $errors = []; @@ -74,12 +87,14 @@ if (mb_strlen($phone) > 50) { $errors['contact-phone'] = 'Die Telefonnummer ist zu lang (max. 50 Zeichen).'; } if (!in_array($interest, $interests, true)) { - $errors['contact-interest'] = 'Bitte wähle einen der angebotenen Bereiche.'; + $errors['contact-interest'] = $variant['interest_required'] + ? 'Bitte wähle aus, für welche Gruppe du dich interessierst.' + : 'Bitte wähle einen der angebotenen Bereiche.'; } -if ($subject === '' || mb_strlen($subject) > 200 || preg_match('/[\r\n]/', $subject)) { +if ($variant['ask_subject'] && ($subject === '' || mb_strlen($subject) > 200 || preg_match('/[\r\n]/', $subject))) { $errors['contact-subject'] = 'Bitte gib einen Betreff an (max. 200 Zeichen, einzeilig).'; } -if ($message === '' || mb_strlen($message) > 5000) { +if (($variant['message_required'] && $message === '') || mb_strlen($message) > 5000) { $errors['contact-message'] = 'Bitte schreib uns eine Nachricht (max. 5000 Zeichen).'; } if (!$privacy) { @@ -124,23 +139,28 @@ if (!rate_limit_ok('daily', 200, 86400)) { } // --- Versand --- -$body = "Kontaktanfrage über tsv08kulmbach.de\n" +// Ohne Betreff-Feld trägt die Auswahl den Mail-Betreff. +$mailSubject = $variant['subject_prefix'] . ': ' . ($subject !== '' ? $subject : $interest); + +$body = $variant['context'] . " über tsv08kulmbach.de\n" . str_repeat('-', 40) . "\n" . "Name: {$name}\n" . "E-Mail: {$email}\n" . ($phone !== '' ? "Telefon: {$phone}\n" : '') . ($interest !== '' ? "Interesse: {$interest}\n" : '') - . "Betreff: {$subject}\n" + . ($subject !== '' ? "Betreff: {$subject}\n" : '') . str_repeat('-', 40) . "\n\n" - . $message . "\n\n" + . ($message !== '' ? $message . "\n\n" : "(keine Nachricht angegeben)\n\n") . str_repeat('-', 40) . "\n" . 'Datenschutz zugestimmt: ja (' . date('d.m.Y H:i') . ")\n"; $sent = send_mail([ - 'context' => 'Kontaktanfrage', - 'subject' => 'Kontaktanfrage: ' . $subject, + 'context' => $variant['context'], + 'subject' => $mailSubject, 'body' => $body, 'reply_to' => ['email' => $email, 'name' => $name], + // Leer heißt: Standardempfänger aus config('smtp.to') (send_mail()). + 'to' => $variant['to'] !== '' ? $variant['to'] : null, ]); $respond($sent, $sent ? '' : 'mail'); diff --git a/app/actions/membership-submit.php b/app/actions/membership-submit.php index fad94e7..6fbd219 100644 --- a/app/actions/membership-submit.php +++ b/app/actions/membership-submit.php @@ -30,8 +30,7 @@ $token = $field('ft'); // Ohne vollständige config/config.php liefe die Seite auf der Vorlage (öffentliches // app_secret, kein SMTP-Passwort) — dann gar nicht erst verarbeiten. -if (config_problems() !== []) { - error_log('[' . date('c') . "] {$route}: Formular abgewiesen, Konfiguration unvollständig (php bin/preflight.php).\n", 3, STORAGE_PATH . '/logs/mail.log'); +if (!config_ready($route)) { $respond(false, 'mail'); } @@ -156,7 +155,7 @@ $body = "Neuer Mitgliedsantrag (Fußball) über tsv08kulmbach.de\n" . "Satzung anerkannt: ja\n" . "Datenschutz: ja\n" . 'Eingegangen: ' . date('d.m.Y H:i') . "\n\n" - . "Hinweis: Bankdaten werden online nicht erhoben – Beitrittserklärung\n" + . "Hinweis: Bankdaten werden online nicht erhoben. Beitrittserklärung\n" . "per Post zur Unterschrift zusenden.\n"; // Mail 1 (intern) — erfolgskritisch. diff --git a/app/actions/sportheimbuchung-senden.php b/app/actions/sportheimbuchung-senden.php index fa3c452..9d71ff6 100644 --- a/app/actions/sportheimbuchung-senden.php +++ b/app/actions/sportheimbuchung-senden.php @@ -29,8 +29,7 @@ $token = $field('ft'); // Ohne vollständige config/config.php liefe die Seite auf der Vorlage (öffentliches // app_secret, kein SMTP-Passwort) — dann gar nicht erst verarbeiten. -if (config_problems() !== []) { - error_log('[' . date('c') . "] {$route}: Formular abgewiesen, Konfiguration unvollständig (php bin/preflight.php).\n", 3, STORAGE_PATH . '/logs/mail.log'); +if (!config_ready($route)) { $respond(false, 'mail'); } diff --git a/app/components/contact-form.php b/app/components/contact-form.php index 64e59e9..f1e1350 100644 --- a/app/components/contact-form.php +++ b/app/components/contact-form.php @@ -9,6 +9,11 @@ declare(strict_types=1); * $return string optional — Slug der absendenden Seite (Default '' = Startseite). Die Action * leitet nach PRG hierher zurück (whitelist-validiert), damit /kontakt nicht auf die * Startseite springt. + * $department string optional — '' (allgemein) oder 'turnen'. Steuert Auswahlliste, + * Empfänger und Feldregeln über contact_variant() (helpers.php, einzige Quelle). + * Die Turn-Variante geht an die Abteilung statt an info@ und braucht keinen + * Betreff, weil die Disziplin-Auswahl ihn ersetzt. Bewusst dieselbe Komponente + * und dieselbe Action: ein zweites Formular hieße ein zweiter Spam-Schutz. * Felder rendern ausschließlich über component('form-field') — siehe CLAUDE.md. * Liest Flash-Status aus Query-Params (?sent=1 / ?error=...) — PRG-Muster. * Spam-Schutz: Honeypot "company_url" + signierter Timestamp "ft" (form_token()). @@ -17,26 +22,29 @@ $club = json_load('club'); $return = $return ?? ''; $intro = $intro ?? null; -$interests = ['Herrenfußball', 'Frauenfußball', 'Jugendfußball', 'Turnen', 'Sonstiges']; +$variant = contact_variant((string) ($department ?? '')); +$interests = $variant['interests']; +// Sichtbare Direkt-Adresse: bei einer Abteilungsanfrage die der Abteilung. +$directMail = $variant['to'] !== '' ? $variant['to'] : $club['email']; // Fehlerquelle: PRG-Query (?error=…) oder — beim 422-Re-Render ohne JS — die // serverseitigen Feldfehler aus der Action. $sent = isset($_GET['sent']); $error = isset($_GET['error']) ? (string) $_GET['error'] : (form_has_errors() ? 'validation' : null); $errorMessages = [ - 'validation' => 'Bitte prüfe deine Eingaben – die markierten Felder brauchen noch etwas Aufmerksamkeit.', + 'validation' => 'Bitte prüfe deine Eingaben: Die markierten Felder brauchen noch etwas Aufmerksamkeit.', 'ratelimit' => 'Zu viele Anfragen in kurzer Zeit. Bitte versuche es in ein paar Minuten erneut.', - 'busy' => 'Bei uns gehen gerade ungewöhnlich viele Anfragen ein. Bitte versuche es später noch einmal oder schreib uns direkt an ' . $club['email'] . '.', - 'mail' => 'Deine Nachricht konnte gerade nicht versendet werden. Bitte versuche es später erneut oder schreib uns direkt an ' . $club['email'] . '.', + 'busy' => 'Bei uns gehen gerade ungewöhnlich viele Anfragen ein. Bitte versuche es später noch einmal oder schreib uns direkt an ' . $directMail . '.', + 'mail' => 'Deine Nachricht konnte gerade nicht versendet werden. Bitte versuche es später erneut oder schreib uns direkt an ' . $directMail . '.', ]; $f = [ 'name' => ['type' => 'text', 'name' => 'name', 'id' => 'contact-name', 'label' => 'Name', 'required' => true, 'autocomplete' => 'name', 'maxlength' => 200], 'email' => ['type' => 'email', 'name' => 'email', 'id' => 'contact-email', 'label' => 'E-Mail', 'required' => true, 'autocomplete' => 'email', 'maxlength' => 200], 'phone' => ['type' => 'tel', 'name' => 'phone', 'id' => 'contact-phone', 'label' => 'Telefonnummer', 'autocomplete' => 'tel', 'maxlength' => 50], - 'interest' => ['type' => 'select', 'name' => 'interest', 'id' => 'contact-interest', 'label' => 'Ich interessiere mich für', 'options' => $interests], + 'interest' => ['type' => 'select', 'name' => 'interest', 'id' => 'contact-interest', 'label' => 'Ich interessiere mich für', 'options' => $interests, 'required' => $variant['interest_required']], 'subject' => ['type' => 'text', 'name' => 'subject', 'id' => 'contact-subject', 'label' => 'Betreff', 'required' => true, 'maxlength' => 200], - 'message' => ['type' => 'textarea', 'name' => 'message', 'id' => 'contact-message', 'label' => 'Nachricht', 'required' => true, 'maxlength' => 5000, 'rows' => 6], + 'message' => ['type' => 'textarea', 'name' => 'message', 'id' => 'contact-message', 'label' => 'Nachricht', 'required' => $variant['message_required'], 'maxlength' => 5000, 'rows' => 6], 'privacy' => [ 'type' => 'checkbox', 'name' => 'privacy', 'id' => 'contact-privacy', 'required' => true, 'label' => 'Ich stimme den Datenschutzbestimmungen zu.', @@ -50,7 +58,7 @@ $f = [
= e($intro['text']) ?>
-Oder direkt per Mail an = e($club['email']) ?>
+Oder direkt per Mail an = e($directMail) ?>