Template
Neuer statischer Webauftritt für die Olzinger GmbH (Heizung, Sanitär, Lüftung, regenerative Energien – Ergolding bei Landshut, seit 1985). - Hero mit live berechnetem WebGL-„Wärmebild“: Maus als Wärmequelle, Wärmespur, Isothermen und Temperaturanzeige; Preloader „Thermostat“ - Gestapelte Leistungskarten mit animierten SVG-Illustrationen - Interaktives Energie-Haus (Wärmepumpe, Solarthermie, Speicher, Fußbodenheizung, Bad, Lüftung) mit Tabs, Hotspots und Autoplay - Heizungs-Finder mit Empfehlung und Übernahme ins Kontaktformular - Ablauf als sich füllende Rohrleitung, Zeitleiste mit Zählwerk, Live-Öffnungsstatus inkl. bayerischer Feiertage - Kontaktformular mit PHP-Versand (Honeypot, Zeitfalle) und mailto-Fallback; Impressum, Datenschutz, 404-Seite - Smooth-Scroll (Lenis) und GSAP-Animationen, alles lokal gehostet; reduzierte Bewegung, Betrieb ohne JavaScript, axe ohne Befunde - SEO: Schema.org HVACBusiness, Open-Graph-Bild, Sitemap, robots.txt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kjd7UMh3HKyVziCgiZ9A2i
118 lines
3.6 KiB
PHP
118 lines
3.6 KiB
PHP
<?php
|
||
/**
|
||
* Olzinger Haustechnik – Versand des Kontaktformulars
|
||
*
|
||
* Wird per fetch() aus assets/js/contact.js aufgerufen und antwortet mit JSON.
|
||
* Ist PHP auf dem Server nicht verfügbar, öffnet das Formular automatisch das
|
||
* E-Mail-Programm des Besuchers (Fallback in contact.js).
|
||
*
|
||
* Schutzmaßnahmen: Honeypot-Feld, Mindest-Ausfüllzeit, Längenbegrenzung,
|
||
* Validierung der E-Mail-Adresse (verhindert Header-Injection über Reply-To),
|
||
* kodierter Betreff und feste Empfängeradresse.
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
const RECIPIENT = 'kontakt@olzinger.de';
|
||
// Absender sollte eine Adresse der eigenen Domain sein (SPF/DKIM), sonst landen Mails im Spam.
|
||
const SENDER = 'kontakt@olzinger.de';
|
||
const MIN_SECONDS = 3;
|
||
|
||
date_default_timezone_set('Europe/Berlin');
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('X-Content-Type-Options: nosniff');
|
||
header('Cache-Control: no-store');
|
||
|
||
function respond(int $status, array $payload): void
|
||
{
|
||
http_response_code($status);
|
||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
function field(string $key, int $max): string
|
||
{
|
||
$value = isset($_POST[$key]) && is_string($_POST[$key]) ? trim($_POST[$key]) : '';
|
||
$value = str_replace(["\r\n", "\r"], "\n", $value);
|
||
// Steuerzeichen außer Zeilenumbruch und Tab entfernen
|
||
$value = preg_replace('/[^\P{C}\n\t]/u', '', $value) ?? '';
|
||
return function_exists('mb_substr') ? mb_substr($value, 0, $max, 'UTF-8') : substr($value, 0, $max);
|
||
}
|
||
|
||
function single_line(string $value): string
|
||
{
|
||
return trim(preg_replace('/\s+/u', ' ', $value) ?? '');
|
||
}
|
||
|
||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||
header('Allow: POST');
|
||
respond(405, ['ok' => false, 'error' => 'method_not_allowed']);
|
||
}
|
||
|
||
// Honeypot: echte Besucher sehen dieses Feld nicht.
|
||
if (field('website', 200) !== '') {
|
||
respond(200, ['ok' => true]);
|
||
}
|
||
|
||
// Zeitfalle: Bots senden Formulare meist sofort ab.
|
||
$started = (int) ($_POST['ts'] ?? 0);
|
||
if ($started > 0 && (microtime(true) * 1000 - $started) < MIN_SECONDS * 1000) {
|
||
respond(200, ['ok' => true]);
|
||
}
|
||
|
||
$name = single_line(field('name', 120));
|
||
$email = single_line(field('email', 200));
|
||
$phone = single_line(field('phone', 60));
|
||
$place = single_line(field('place', 120));
|
||
$topic = single_line(field('topic', 120));
|
||
$message = field('message', 5000);
|
||
$privacy = !empty($_POST['privacy']);
|
||
|
||
$errors = [];
|
||
if ($name === '') {
|
||
$errors[] = 'name';
|
||
}
|
||
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
|
||
$errors[] = 'email';
|
||
}
|
||
if ((function_exists('mb_strlen') ? mb_strlen($message, 'UTF-8') : strlen($message)) < 5) {
|
||
$errors[] = 'message';
|
||
}
|
||
if (!$privacy) {
|
||
$errors[] = 'privacy';
|
||
}
|
||
if ($errors) {
|
||
respond(422, ['ok' => false, 'errors' => $errors]);
|
||
}
|
||
|
||
$subject = 'Anfrage über olzinger.de' . ($topic !== '' ? ' – ' . $topic : '');
|
||
$body = implode("\n", [
|
||
'Neue Anfrage über das Kontaktformular auf olzinger.de',
|
||
'',
|
||
'Anliegen: ' . ($topic !== '' ? $topic : '–'),
|
||
'Name: ' . $name,
|
||
'E-Mail: ' . $email,
|
||
'Telefon: ' . ($phone !== '' ? $phone : '–'),
|
||
'PLZ / Ort: ' . ($place !== '' ? $place : '–'),
|
||
'',
|
||
'Nachricht:',
|
||
$message,
|
||
'',
|
||
'—',
|
||
'Gesendet am ' . date('d.m.Y') . ' um ' . date('H:i') . ' Uhr',
|
||
]);
|
||
|
||
$headers = implode("\r\n", [
|
||
'From: Website Olzinger <' . SENDER . '>',
|
||
'Reply-To: ' . $email,
|
||
'MIME-Version: 1.0',
|
||
'Content-Type: text/plain; charset=UTF-8',
|
||
'Content-Transfer-Encoding: 8bit',
|
||
'X-Mailer: olzinger.de',
|
||
]);
|
||
|
||
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
|
||
$sent = @mail(RECIPIENT, $encodedSubject, $body, $headers, '-f' . SENDER);
|
||
|
||
respond($sent ? 200 : 500, ['ok' => (bool) $sent]);
|