'form.interest.visit', 'info' => 'form.interest.info', 'apply' => 'form.interest.apply', ]; public function index(): void { if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } $locale = LocaleController::current(); $escapeContactValue = static fn(string $value): string => htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); $containsHeaderInjection = static function (string $value): bool { return (bool) preg_match('/[\r\n]/', $value); }; // ── Pull flashed state ──────────────────────────────────────── $formSuccess = !empty($_SESSION['form_success']); $formErrors = $_SESSION['form_errors'] ?? []; $formFieldErrors = $_SESSION['form_field_errors'] ?? []; $formData = $_SESSION['form_data'] ?? null; unset( $_SESSION['form_success'], $_SESSION['form_errors'], $_SESSION['form_field_errors'], $_SESSION['form_data'], ); if ($formSuccess) { $formData = self::emptyFormData(); $formFieldErrors = []; } elseif (!is_array($formData)) { $formData = self::emptyFormData(); $formFieldErrors = is_array($formFieldErrors) ? $formFieldErrors : []; } else { $formFieldErrors = is_array($formFieldErrors) ? $formFieldErrors : []; } // ── CSRF token ──────────────────────────────────────────────── if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } // ── Form processing ─────────────────────────────────────────── if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { $csrfToken = (string) ($_POST['csrf_token'] ?? ''); if (!hash_equals($_SESSION['csrf_token'] ?? '', $csrfToken)) { $_SESSION['form_errors'] = ['form.error.csrf']; header('Location: /#kontakt'); exit; } $formData['fname'] = trim((string) ($_POST['fname'] ?? '')); $formData['lname'] = trim((string) ($_POST['lname'] ?? '')); $formData['email'] = trim((string) ($_POST['email'] ?? '')); $formData['phone'] = trim((string) ($_POST['phone'] ?? '')); $formData['interest'] = trim((string) ($_POST['interest'] ?? 'visit')); $formData['message'] = trim((string) ($_POST['message'] ?? '')); // Honeypot: bots succeed silently. $honeypot = trim((string) ($_POST['website'] ?? '')); if ($honeypot !== '') { $_SESSION['form_success'] = true; header('Location: /#kontakt'); exit; } // Per-field errors enable aria-invalid + aria-describedby. $formFieldErrors = []; if ($formData['fname'] === '') { $formFieldErrors['fname'][] = 'form.error.fname_required'; } if ($formData['lname'] === '') { $formFieldErrors['lname'][] = 'form.error.lname_required'; } if ($formData['email'] === '' || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) { $formFieldErrors['email'][] = 'form.error.email_invalid'; } if ($formData['message'] === '') { $formFieldErrors['message'][] = 'form.error.message_required'; } if ($containsHeaderInjection($formData['email']) || $containsHeaderInjection($formData['fname'] . ' ' . $formData['lname'])) { $formErrors[] = 'form.error.header_injection'; } $formTime = isset($_POST['form_time']) ? (int) $_POST['form_time'] : 0; if ($formTime > 0 && (time() - $formTime) < 3) { $formErrors[] = 'form.error.too_fast'; } $lastSubmit = $_SESSION['last_contact_submit'] ?? 0; if ($lastSubmit && (time() - $lastSubmit) < 60) { $formErrors[] = 'form.error.rate_limit'; } if (empty($formErrors) && empty($formFieldErrors)) { $interestKey = self::INTEREST_KEYS[$formData['interest']] ?? 'form.interest.visit'; $interestLabel = I18n::t($interestKey, [], $locale); $to = 'mki@kies-media.de'; $subject = 'Kontaktanfrage: ' . $interestLabel; $body = sprintf( "Von: %s %s\nE-Mail: %s\n%sAnliegen: %s\n\n%s", $formData['fname'], $formData['lname'], $formData['email'], $formData['phone'] !== '' ? "Telefon: {$formData['phone']}\n" : '', $interestLabel, $formData['message'] ); $headers = "From: {$formData['email']}\r\n"; $headers .= "Reply-To: {$formData['email']}\r\n"; $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; $headers .= "X-Mailer: PHP/" . phpversion(); if (mail($to, $subject, $body, $headers)) { $_SESSION['last_contact_submit'] = time(); $_SESSION['form_success'] = true; header('Location: /#kontakt'); exit; } $formErrors[] = 'form.error.send_failed'; } $_SESSION['form_errors'] = $formErrors; $_SESSION['form_field_errors'] = $formFieldErrors; $_SESSION['form_data'] = $formData; header('Location: /#kontakt'); exit; } // ── Structured data (JSON-LD) — localized ──────────────────── $structuredData = json_encode([ '@context' => 'https://schema.org', '@type' => 'RealEstateListing', 'name' => I18n::t('structured.listing_name', [], $locale), 'description'=> I18n::t('structured.listing_description', [], $locale), 'url' => I18n::t('site.canonical_base', [], $locale) . '/', 'image' => I18n::t('site.canonical_base', [], $locale) . '/bilder/Außenansicht-2.png', 'datePosted' => '2026-05-14', 'address' => [ '@type' => 'PostalAddress', 'streetAddress' => I18n::t('address.street', [], $locale), 'addressLocality' => I18n::t('address.city', [], $locale), 'postalCode' => '98553', 'addressCountry' => 'DE', ], 'offers' => [ '@type' => 'Offer', 'price' => '1300', 'priceCurrency' => 'EUR', 'priceSpecification' => [ '@type' => 'UnitPriceSpecification', 'price' => '1300', 'priceCurrency' => 'EUR', 'unitCode' => 'MON', 'description' => I18n::t('structured.price_description', [], $locale), ], ], 'floorSize' => [ '@type' => 'QuantitativeValue', 'value' => '227', 'unitCode' => 'MTK', ], 'numberOfRooms' => [ '@type' => 'QuantitativeValue', 'value' => '6', ], ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $this->render('home/index', [ 'formSuccess' => $formSuccess, 'formErrors' => $formErrors, 'formFieldErrors' => $formFieldErrors, 'formData' => $formData, 'interestKeys' => self::INTEREST_KEYS, 'escapeContactValue' => $escapeContactValue, 'structuredData' => $structuredData, ]); } /** * @return array{fname: string, lname: string, email: string, phone: string, interest: string, message: string} */ private static function emptyFormData(): array { return [ 'fname' => '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'visit', 'message' => '', ]; } }