- Ordnerstruktur mit public/-Docroot, Deny-.htaccess für app/bin/config/data/storage - CLAUDE.md mit Leitplanken (self-hosted only, Single Source of Truth, Component-first) - Design-Tokens aus alter Seite extrahiert (Akzent #e20612, Coolvetica/Abel als woff2) - Front Controller mit Routen-Register, Sitemap, Canonical, JSON-LD (SportsClub) - Startseite: Hero, Instagram-Feed (lokaler Cache), Historie/Sportheim, Stats, Partner, Kontakt - Kontaktformular via PHPMailer/Brevo mit Honeypot + HMAC-Time-Trap (kein reCAPTCHA) - bin/instagram-sync.php: Scraper mit Strategie-Kette, flock, atomarem Cache, lokalen Bildern Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
2.9 KiB
PHP
93 lines
2.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* POST /kontakt-senden — Kontaktformular validieren und via Brevo SMTP versenden.
|
|
* Antwort: PRG-Redirect zu /#kontakt (?sent=1 | ?error=…) oder JSON bei fetch (form.js).
|
|
*/
|
|
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
|
|
$wantsJson = str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json');
|
|
|
|
$respond = static function (bool $ok, string $error = '') use ($wantsJson): never {
|
|
if ($wantsJson) {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode(['ok' => $ok, 'error' => $error ?: null]);
|
|
} else {
|
|
header('Location: ' . url('') . ($ok ? '?sent=1' : '?error=' . $error) . '#kontakt', true, 303);
|
|
}
|
|
exit;
|
|
};
|
|
|
|
$field = static fn (string $key): string => trim((string) ($_POST[$key] ?? ''));
|
|
|
|
// --- Spam-Checks: Bots bekommen ein stilles "OK" (kein Feedback-Kanal) ---
|
|
if ($field('website') !== '' || !form_token_valid($field('ft'))) {
|
|
$respond(true);
|
|
}
|
|
|
|
// --- Validierung ---
|
|
$name = $field('name');
|
|
$email = $field('email');
|
|
$phone = $field('phone');
|
|
$interest = $field('interest');
|
|
$subject = $field('subject');
|
|
$message = $field('message');
|
|
|
|
$interests = ['', 'Herrenfußball', 'Damenfußball', 'Jugendfußball', 'Turnen', 'Sonstiges'];
|
|
|
|
$valid = $name !== '' && mb_strlen($name) <= 200
|
|
&& filter_var($email, FILTER_VALIDATE_EMAIL) !== false
|
|
&& mb_strlen($phone) <= 50
|
|
&& in_array($interest, $interests, true)
|
|
&& $subject !== '' && mb_strlen($subject) <= 200
|
|
&& $message !== '' && mb_strlen($message) <= 5000
|
|
&& ($_POST['privacy'] ?? '') === '1'
|
|
&& !preg_match('/[\r\n]/', $name . $subject);
|
|
|
|
if (!$valid) {
|
|
$respond(false, 'validation');
|
|
}
|
|
|
|
// --- Versand ---
|
|
$smtp = config('smtp');
|
|
|
|
$body = "Kontaktanfrage ü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"
|
|
. str_repeat('-', 40) . "\n\n"
|
|
. $message . "\n\n"
|
|
. str_repeat('-', 40) . "\n"
|
|
. 'Datenschutz zugestimmt: ja (' . date('d.m.Y H:i') . ")\n";
|
|
|
|
try {
|
|
$mail = new PHPMailer(true);
|
|
$mail->isSMTP();
|
|
$mail->Host = $smtp['host'];
|
|
$mail->Port = (int) $smtp['port'];
|
|
$mail->SMTPAuth = true;
|
|
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
|
$mail->Username = $smtp['username'];
|
|
$mail->Password = $smtp['password'];
|
|
$mail->CharSet = PHPMailer::CHARSET_UTF8;
|
|
|
|
$mail->setFrom($smtp['from'], $smtp['from_name']);
|
|
$mail->addAddress($smtp['to']);
|
|
$mail->addReplyTo($email, $name);
|
|
$mail->Subject = 'Kontaktanfrage: ' . $subject;
|
|
$mail->Body = $body;
|
|
|
|
$mail->send();
|
|
} catch (Throwable $e) {
|
|
error_log('[' . date('c') . '] Mailversand fehlgeschlagen: ' . $e->getMessage() . "\n", 3, STORAGE_PATH . '/logs/mail.log');
|
|
$respond(false, 'mail');
|
|
}
|
|
|
|
$respond(true);
|