From ebf6f82bb6836291fda51384c58c9a3cfe6d0f47 Mon Sep 17 00:00:00 2001 From: smueller Date: Fri, 22 May 2026 14:50:20 +0200 Subject: [PATCH] Enhance API functionality and security: Added rate limiting and domain validation across multiple API endpoints, improved error handling for missing or invalid parameters, and refactored email handling in contact form for better security and maintainability. Updated README.md with production build instructions and prerequisites. --- README.md | 21 ++ backend/.htaccess | 8 + backend/api/dns-lookup.php | 23 +- backend/api/dns-propagation.php | 29 ++- backend/api/ping-check.php | 21 +- backend/api/reverse-dns.php | 10 +- backend/api/ssl-check.php | 23 +- backend/api/whois-lookup.php | 12 +- backend/config/contact-config.php | 64 +++++ backend/config/mail-config.php | 21 +- backend/includes/api-helpers.php | 112 +++++++++ backend/includes/footer.php | 2 +- backend/includes/functions.php | 52 ++++ public/config/mail-config.php | 5 + public/contact-handler.php | 381 +++++++++++++----------------- public/contact.php | 21 +- scripts/publish-to-main.ps1 | 182 ++++++++++++++ scripts/publish-to-main.sh | 187 +++++++++++++++ scripts/run-build.ps1 | 46 ++++ scripts/run-build.sh | 58 +++++ scripts/test-email.php | 84 +++---- 21 files changed, 1007 insertions(+), 355 deletions(-) create mode 100644 backend/.htaccess create mode 100644 backend/config/contact-config.php create mode 100644 backend/includes/api-helpers.php create mode 100644 public/config/mail-config.php create mode 100644 scripts/publish-to-main.ps1 create mode 100644 scripts/publish-to-main.sh create mode 100644 scripts/run-build.ps1 create mode 100644 scripts/run-build.sh diff --git a/README.md b/README.md index 2449d77..c68747e 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,27 @@ HexaHost-Frontend/ ### Produktion Für den Produktivbetrieb `public/` als Webroot konfigurieren. +### Production-Build & Veröffentlichung + +Der Quellcode bleibt auf `dev`, der veröffentlichte Stand liegt auf `main` (ohne Kommentare, obfuskiertes JS). + +**Voraussetzungen:** Node.js 18+ (inkl. npm), PHP 8+ CLI, Git + +```powershell +# Windows +.\scripts\run-build.ps1 +.\scripts\publish-to-main.ps1 -Push +``` + +```bash +# Linux / macOS +chmod +x scripts/*.sh +./scripts/run-build.sh +./scripts/publish-to-main.sh --push +``` + +Details: `scripts/build/README.md` + ## 🔗 Backend-Integration Das Backend-Repository enthält folgende wiederverwendbare Komponenten: diff --git a/backend/.htaccess b/backend/.htaccess new file mode 100644 index 0000000..4e55896 --- /dev/null +++ b/backend/.htaccess @@ -0,0 +1,8 @@ +# Direkten Zugriff auf Backend-Dateien verhindern (Document Root = public/) + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/backend/api/dns-lookup.php b/backend/api/dns-lookup.php index ad7cf59..9b9bd5f 100644 --- a/backend/api/dns-lookup.php +++ b/backend/api/dns-lookup.php @@ -1,12 +1,14 @@ 'Nur GET-Anfragen erlaubt']); exit; } -// Domain-Parameter prüfen -$domain = isset($_GET['domain']) ? trim($_GET['domain']) : ''; - -if (empty($domain)) { - http_response_code(400); - echo json_encode(['error' => 'Domain-Parameter fehlt']); - exit; +if (!checkApiRateLimit('dns-lookup')) { + rejectApiRateLimit(); } -// Domain validieren (einfache Prüfung) -if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { +$domain = getValidatedDomainParam(); + +if ($domain === null) { http_response_code(400); - echo json_encode(['error' => 'Ungültiges Domain-Format']); + echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']); exit; } diff --git a/backend/api/dns-propagation.php b/backend/api/dns-propagation.php index 62f2949..15b8f1e 100644 --- a/backend/api/dns-propagation.php +++ b/backend/api/dns-propagation.php @@ -1,12 +1,14 @@ 'Google', 'ip' => '8.8.8.8', 'location' => 'Global'], @@ -29,18 +35,12 @@ $dnsServers = [ ['name' => 'Level3', 'ip' => '4.2.2.1', 'location' => 'USA'], ]; -$domain = isset($_GET['domain']) ? trim($_GET['domain']) : ''; +$domain = getValidatedDomainParam(); $type = isset($_GET['type']) ? strtoupper(trim($_GET['type'])) : 'A'; -if (empty($domain)) { +if ($domain === null) { http_response_code(400); - echo json_encode(['error' => 'Domain-Parameter fehlt']); - exit; -} - -if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { - http_response_code(400); - echo json_encode(['error' => 'Ungültiges Domain-Format']); + echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']); exit; } @@ -100,7 +100,12 @@ function queryDnsServer(string $domain, string $type, string $server): array { $records = []; // Versuche zuerst dig zu verwenden (genauer) - $digResult = @shell_exec("dig @{$server} {$domain} {$type} +short +time=2 +tries=1 2>/dev/null"); + $digResult = @shell_exec( + 'dig @' . escapeshellarg($server) . ' ' + . escapeshellarg($domain) . ' ' + . escapeshellarg($type) + . ' +short +time=2 +tries=1 2>/dev/null' + ); if ($digResult !== null && trim($digResult) !== '') { $lines = array_filter(explode("\n", trim($digResult))); diff --git a/backend/api/ping-check.php b/backend/api/ping-check.php index 0ee176a..1fa326b 100644 --- a/backend/api/ping-check.php +++ b/backend/api/ping-check.php @@ -7,6 +7,8 @@ * Verwendung: GET /api/ping-check.php?domain=example.com */ +require_once __DIR__ . '/../includes/api-helpers.php'; + header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, OPTIONS'); @@ -17,21 +19,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; } -$domain = isset($_GET['domain']) ? trim($_GET['domain']) : ''; - -if (empty($domain)) { - http_response_code(400); - echo json_encode(['error' => 'Domain-Parameter fehlt']); - exit; +if (!checkApiRateLimit('ping-check')) { + rejectApiRateLimit(); } -// Protokoll und Pfad entfernen -$domain = preg_replace('/^(https?:\/\/)?/', '', $domain); -$domain = explode('/', $domain)[0]; +$domain = getValidatedDomainParam(); -if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { +if ($domain === null) { http_response_code(400); - echo json_encode(['error' => 'Ungültiges Domain-Format']); + echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']); exit; } @@ -99,7 +95,8 @@ function checkIcmpPing(string $domain): array { ]; // Versuche ping-Kommando - $pingResult = @shell_exec("ping -c 3 -W 2 {$domain} 2>/dev/null"); + $safeDomain = escapeshellarg($domain); + $pingResult = @shell_exec("ping -c 3 -W 2 {$safeDomain} 2>/dev/null"); if ($pingResult) { // Prüfe auf erfolgreiche Antworten diff --git a/backend/api/reverse-dns.php b/backend/api/reverse-dns.php index 4aa832d..661429a 100644 --- a/backend/api/reverse-dns.php +++ b/backend/api/reverse-dns.php @@ -1,12 +1,14 @@ 'Domain-Parameter fehlt']); - exit; +if (!checkApiRateLimit('ssl-check')) { + rejectApiRateLimit(); } -// Protokoll und Pfad entfernen -$domain = preg_replace('/^(https?:\/\/)?/', '', $domain); -$domain = explode('/', $domain)[0]; -$domain = explode(':', $domain)[0]; // Port entfernen +$domain = getValidatedDomainParam(); -if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { +if ($domain === null) { http_response_code(400); - echo json_encode(['error' => 'Ungültiges Domain-Format']); + echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']); exit; } diff --git a/backend/api/whois-lookup.php b/backend/api/whois-lookup.php index 5dc9ed6..c0a76f1 100644 --- a/backend/api/whois-lookup.php +++ b/backend/api/whois-lookup.php @@ -1,12 +1,14 @@ Betreff-Schlüssel => Anzeigename + */ +function getContactSubjectMap(): array { + return [ + 'allgemeine-anfrage' => 'Allgemeine Anfrage', + 'vpc-anfrage' => 'Virtual Private Container Anfrage', + 'vps-anfrage' => 'Virtual Private Server Anfrage', + 'mail-gateway-anfrage' => 'Mail Gateway Anfrage', + 'webhosting-anfrage' => 'Webhosting Anfrage', + 'it-beratung' => 'IT-Beratung', + 'it-support' => 'IT-Support & Fehlerbehebung', + 'netzwerk-wlan' => 'Netzwerk & WLAN-Einrichtung', + 'it-sicherheit-backup' => 'IT-Sicherheit & Backup', + 'webseiten-hosting-service' => 'Webseiten- & Hosting-Service', + 'wartung-betreuung' => 'Wartung & Betreuung', + 'support' => 'Technischer Support', + 'beratung' => 'Persönliche Beratung', + 'migration' => 'Migration/Umzug', + 'sonstiges' => 'Sonstige Anfrage', + ]; +} + +/** + * @param string $subjectKey + */ +function isAllowedContactSubject(string $subjectKey): bool { + return array_key_exists($subjectKey, getContactSubjectMap()); +} + +/** + * Betreff aus ?product= oder ?package= für die Kontaktseite ableiten + */ +function getPreselectedContactSubject(): string { + $productMap = [ + 'vpc' => 'vpc-anfrage', + 'vps' => 'vps-anfrage', + 'mail-gateway' => 'mail-gateway-anfrage', + 'webhosting' => 'webhosting-anfrage', + ]; + + if (!empty($_GET['product'])) { + $product = strtolower(preg_replace('/[^a-z0-9-]/', '', (string) $_GET['product'])); + if (isset($productMap[$product])) { + return $productMap[$product]; + } + } + + if (!empty($_GET['package'])) { + $package = strtolower(preg_replace('/[^a-z0-9-]/', '', (string) $_GET['package'])); + foreach ($productMap as $productId => $subjectKey) { + if (str_starts_with($package, $productId . '-')) { + return $subjectKey; + } + } + } + + return ''; +} diff --git a/backend/config/mail-config.php b/backend/config/mail-config.php index 46f0164..55850c0 100644 --- a/backend/config/mail-config.php +++ b/backend/config/mail-config.php @@ -140,24 +140,6 @@ function isValidEmail($email) { return true; } -// CSRF Token generieren (wird in functions.php verwendet) -// Hinweis: Diese Funktion existiert auch in functions.php - hier nur als Fallback -if (!function_exists('generateCSRFToken')) { - function generateCSRFToken() { - if (!isset($_SESSION['csrf_token'])) { - $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); - } - return $_SESSION['csrf_token']; - } -} - -// CSRF Token validieren -if (!function_exists('validateCSRFToken')) { - function validateCSRFToken($token) { - return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); - } -} - /** * Hilfsfunktion zum Abrufen der Konfiguration als Array * Kompatibilität mit contact-handler.php @@ -183,6 +165,9 @@ function getHexaHostConfig($key = null) { // Sicherheit 'max_requests_per_hour' => MAX_REQUESTS_PER_HOUR, 'honeypot_field' => 'website', + 'enable_csrf' => ENABLE_CSRF_PROTECTION, + 'min_message_length' => MIN_MESSAGE_LENGTH, + 'max_message_length' => MAX_MESSAGE_LENGTH, // Debug 'debug_mode' => DEBUG_MODE, diff --git a/backend/includes/api-helpers.php b/backend/includes/api-helpers.php new file mode 100644 index 0000000..9a98864 --- /dev/null +++ b/backend/includes/api-helpers.php @@ -0,0 +1,112 @@ + []]; + + $handle = @fopen($cacheFile, 'c+'); + if ($handle === false) { + return true; + } + + try { + if (!flock($handle, LOCK_EX)) { + return true; + } + + $contents = stream_get_contents($handle); + if ($contents !== false && $contents !== '') { + $decoded = json_decode($contents, true); + if (is_array($decoded) && isset($decoded['requests'])) { + $data = $decoded; + } + } + + $data['requests'] = array_values(array_filter( + $data['requests'], + static fn($timestamp) => ($currentTime - (int) $timestamp) < 3600 + )); + + if (count($data['requests']) >= $maxPerHour) { + return false; + } + + $data['requests'][] = $currentTime; + ftruncate($handle, 0); + rewind($handle); + fwrite($handle, json_encode($data)); + } finally { + flock($handle, LOCK_UN); + fclose($handle); + } + + return true; +} + +/** + * Domain aus GET-Parameter normalisieren und validieren + */ +function getValidatedDomainParam(string $param = 'domain'): ?string { + if (empty($_GET[$param])) { + return null; + } + + $domain = trim((string) $_GET[$param]); + $domain = preg_replace('/^(https?:\/\/)?/', '', $domain); + $domain = explode('/', $domain)[0]; + $domain = explode(':', $domain)[0]; + + if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { + return null; + } + + return $domain; +} + +/** + * Rate-Limit-JSON-Antwort senden und beenden + */ +function rejectApiRateLimit(): void { + http_response_code(429); + echo json_encode(['error' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.']); + exit; +} diff --git a/backend/includes/footer.php b/backend/includes/footer.php index b00634e..03118f4 100644 --- a/backend/includes/footer.php +++ b/backend/includes/footer.php @@ -160,7 +160,7 @@ - + diff --git a/backend/includes/functions.php b/backend/includes/functions.php index 24d736c..2141db4 100644 --- a/backend/includes/functions.php +++ b/backend/includes/functions.php @@ -95,4 +95,56 @@ function generateCSRFToken() { } return $_SESSION['csrf_token']; } + +/** + * CSRF-Token prüfen und nach Erfolg invalidieren (Replay-Schutz) + */ +function validateCSRFToken($token) { + if (!isset($_SESSION['csrf_token']) || !is_string($token)) { + return false; + } + if (!hash_equals($_SESSION['csrf_token'], $token)) { + return false; + } + unset($_SESSION['csrf_token']); + return true; +} + +/** + * Werte für E-Mail-Header bereinigen (Header-Injection verhindern) + */ +function sanitizeHeaderValue(string $value): string { + return str_replace(["\r", "\n", "\0"], '', trim($value)); +} + +/** + * Client-IP für Logging (Cloudflare / vertrauenswürdiger Reverse-Proxy) + */ +function getClientIP(): string { + if (!empty($_SERVER['HTTP_CF_CONNECTING_IP']) + && filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)) { + return $_SERVER['HTTP_CF_CONNECTING_IP']; + } + + $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + $isTrustedProxy = filter_var( + $remoteAddr, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE + ) === false; + + if ($isTrustedProxy) { + foreach (['HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'] as $header) { + if (empty($_SERVER[$header])) { + continue; + } + $ip = trim(explode(',', $_SERVER[$header])[0]); + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + } + } + + return $remoteAddr; +} ?> \ No newline at end of file diff --git a/public/config/mail-config.php b/public/config/mail-config.php new file mode 100644 index 0000000..40221dd --- /dev/null +++ b/public/config/mail-config.php @@ -0,0 +1,5 @@ + 'Allgemeine Anfrage', - 'vpc-anfrage' => 'Virtual Private Container Anfrage', - 'vps-anfrage' => 'Virtual Private Server Anfrage', - 'mail-gateway-anfrage' => 'Mail Gateway Anfrage', - 'webhosting-anfrage' => 'Webhosting Anfrage', - 'support' => 'Technischer Support', - 'beratung' => 'Persönliche Beratung', - 'migration' => 'Migration/Umzug', - 'sonstiges' => 'Sonstige Anfrage' -]; - -// CSRF-Token validieren und invalidieren (verhindert Replay-Attacks) -function validateCSRFToken($token) { - if (isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token)) { - // Token nach erfolgreicher Validierung invalidieren - unset($_SESSION['csrf_token']); - return true; - } - return false; -} - // CORS Headers für AJAX-Requests (nur eigene Domain erlauben) $allowed_origins = [ 'https://hexahost.de', 'https://www.hexahost.de', - 'http://localhost', // Für Entwicklung - 'http://127.0.0.1' // Für Entwicklung + 'https://dev.hexahost.de', + 'http://localhost', + 'http://127.0.0.1', ]; $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; -if (in_array($origin, $allowed_origins)) { +if (in_array($origin, $allowed_origins, true)) { header('Access-Control-Allow-Origin: ' . $origin); } @@ -60,104 +33,82 @@ header('Access-Control-Allow-Methods: POST'); header('Access-Control-Allow-Headers: Content-Type'); header('Content-Type: application/json; charset=utf-8'); -// Nur POST-Requests erlauben if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'message' => 'Method not allowed']); exit; } -// Rate Limiting function checkRateLimit($ip) { global $config; $cache_file = sys_get_temp_dir() . '/hexahost_contact_' . md5($ip) . '.txt'; $current_time = time(); - - if (file_exists($cache_file)) { - $data = json_decode(file_get_contents($cache_file), true); - if ($data && isset($data['requests'])) { - // Entferne alte Einträge (älter als 1 Stunde) - $data['requests'] = array_filter($data['requests'], function($timestamp) use ($current_time) { - return ($current_time - $timestamp) < 3600; - }); - - if (count($data['requests']) >= $config['max_requests_per_hour']) { - return false; + $data = ['requests' => []]; + + $handle = @fopen($cache_file, 'c+'); + if ($handle === false) { + return true; + } + + try { + if (!flock($handle, LOCK_EX)) { + return true; + } + + $contents = stream_get_contents($handle); + if ($contents !== false && $contents !== '') { + $decoded = json_decode($contents, true); + if (is_array($decoded) && isset($decoded['requests'])) { + $data = $decoded; } } + + $data['requests'] = array_values(array_filter( + $data['requests'], + static fn($timestamp) => ($current_time - (int) $timestamp) < 3600 + )); + + if (count($data['requests']) >= $config['max_requests_per_hour']) { + return false; + } + + $data['requests'][] = $current_time; + ftruncate($handle, 0); + rewind($handle); + fwrite($handle, json_encode($data)); + } finally { + flock($handle, LOCK_UN); + fclose($handle); } - - // Füge aktuellen Request hinzu - $data = isset($data) ? $data : ['requests' => []]; - $data['requests'][] = $current_time; - file_put_contents($cache_file, json_encode($data)); - + return true; } -// Honeypot Check function checkHoneypot($data) { global $config; $honeypot_field = $config['honeypot_field']; - - // Das Honeypot-Feld sollte leer sein (verstecktes Feld) - if (!empty($data[$honeypot_field])) { - return false; - } - - return true; + return empty($data[$honeypot_field]); } -// E-Mail-Validierung -function validateEmail($email) { - return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; +function sanitizeFormField($input) { + return strip_tags(trim((string) $input)); } -// Input-Sanitization -function sanitizeInput($input) { - return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8'); +function getSubjectLabel($subjectKey) { + $map = getContactSubjectMap(); + return $map[$subjectKey] ?? 'Neue Kontaktanfrage'; } -// Sichere IP-Adressen-Erkennung (auch hinter Proxies/Cloudflare) -function getClientIP() { - $ip_keys = [ - 'HTTP_CF_CONNECTING_IP', // Cloudflare - 'HTTP_X_FORWARDED_FOR', // Proxy - 'HTTP_X_REAL_IP', // Nginx Proxy - 'REMOTE_ADDR' // Standard - ]; - - foreach ($ip_keys as $key) { - if (!empty($_SERVER[$key])) { - // Bei X-Forwarded-For kann eine Liste von IPs kommen - $ip = explode(',', $_SERVER[$key])[0]; - $ip = trim($ip); - - // Validiere IP-Format - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - return $ip; - } - } - } - - // Fallback auf REMOTE_ADDR (auch private IPs für lokale Entwicklung) - return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; -} - -// SMTP E-Mail-Versand mit PHPMailer function sendEmail($data) { global $config; - - // PHPMailer laden (falls verfügbar) + if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) { - // Fallback: Native PHP mail() Funktion return sendEmailNative($data); } - + try { $mail = new PHPMailer\PHPMailer\PHPMailer(true); - - // Server-Einstellungen + $mail->isSMTP(); $mail->Host = $config['smtp_host']; $mail->SMTPAuth = true; @@ -166,73 +117,60 @@ function sendEmail($data) { $mail->SMTPSecure = $config['smtp_encryption']; $mail->Port = $config['smtp_port']; $mail->CharSet = 'UTF-8'; - - // Absender + $mail->setFrom($config['from_email'], $config['from_name']); - $mail->addReplyTo($data['email'], $data['firstName'] . ' ' . $data['lastName']); - - // Empfänger + $mail->addReplyTo( + sanitizeHeaderValue($data['email']), + sanitizeHeaderValue($data['firstName'] . ' ' . $data['lastName']) + ); $mail->addAddress($config['to_email'], $config['to_name']); - - // Betreff (nutzt zentrale SUBJECT_MAP Konstante) - $subject = SUBJECT_MAP[$data['subject']] ?? 'Neue Kontaktanfrage'; + + $subject = getSubjectLabel($data['subject']); $mail->Subject = '[HexaHost.de] ' . $subject; - - // HTML E-Mail-Inhalt - $html_content = generateEmailHTML($data); + $mail->isHTML(true); - $mail->Body = $html_content; + $mail->Body = generateEmailHTML($data); $mail->AltBody = generateEmailText($data); - - // Anti-Spam Headers + $mail->addCustomHeader('X-Mailer', 'HexaHost Contact Form'); $mail->addCustomHeader('X-Priority', '3'); $mail->addCustomHeader('X-MSMail-Priority', 'Normal'); $mail->addCustomHeader('Importance', 'Normal'); $mail->addCustomHeader('X-Report-Abuse', 'Please report abuse here: abuse@hexahost.de'); - - // DKIM, SPF, DMARC werden über DNS konfiguriert - + $mail->send(); return true; - } catch (Exception $e) { error_log('HexaHost Contact Form Error: ' . $e->getMessage()); return false; } } -// Fallback: Native PHP mail() Funktion function sendEmailNative($data) { global $config; - - // Betreff (nutzt zentrale SUBJECT_MAP Konstante) - $subject = SUBJECT_MAP[$data['subject']] ?? 'Neue Kontaktanfrage'; - $subject = '[HexaHost.de] ' . $subject; - - // Headers für Spam-Schutz + + $subject = '[HexaHost.de] ' . getSubjectLabel($data['subject']); + $replyName = sanitizeHeaderValue($data['firstName'] . ' ' . $data['lastName']); + $replyEmail = sanitizeHeaderValue($data['email']); + $headers = [ 'From: ' . $config['from_name'] . ' <' . $config['from_email'] . '>', - 'Reply-To: ' . $data['firstName'] . ' ' . $data['lastName'] . ' <' . $data['email'] . '>', + 'Reply-To: ' . $replyName . ' <' . $replyEmail . '>', 'MIME-Version: 1.0', 'Content-Type: text/html; charset=UTF-8', 'X-Mailer: HexaHost Contact Form', 'X-Priority: 3', 'X-MSMail-Priority: Normal', 'Importance: Normal', - 'X-Report-Abuse: Please report abuse here: abuse@hexahost.de' + 'X-Report-Abuse: Please report abuse here: abuse@hexahost.de', ]; - - $message = generateEmailHTML($data); - - return mail($config['to_email'], $subject, $message, implode("\r\n", $headers)); + + return mail($config['to_email'], $subject, generateEmailHTML($data), implode("\r\n", $headers)); } -// HTML E-Mail-Template function generateEmailHTML($data) { - // Betreff (nutzt zentrale SUBJECT_MAP Konstante) - $subject_text = SUBJECT_MAP[$data['subject']] ?? 'Neue Kontaktanfrage'; - + $subject_text = htmlspecialchars(getSubjectLabel($data['subject']), ENT_QUOTES, 'UTF-8'); + $html = ' @@ -267,39 +205,39 @@ function generateEmailHTML($data) {
Name:
-
' . $data['firstName'] . ' ' . $data['lastName'] . '
+
' . htmlspecialchars($data['firstName'] . ' ' . $data['lastName'], ENT_QUOTES, 'UTF-8') . '
E-Mail:
-
' . $data['email'] . '
+
' . htmlspecialchars($data['email'], ENT_QUOTES, 'UTF-8') . '
'; - + if (!empty($data['phone'])) { $html .= '
Telefon:
-
' . $data['phone'] . '
+
' . htmlspecialchars($data['phone'], ENT_QUOTES, 'UTF-8') . '
'; } - + if (!empty($data['company'])) { $html .= '
Unternehmen:
-
' . $data['company'] . '
+
' . htmlspecialchars($data['company'], ENT_QUOTES, 'UTF-8') . '
'; } - + $html .= '
Nachricht:
-
' . nl2br($data['message']) . '
+
' . nl2br(htmlspecialchars($data['message'], ENT_QUOTES, 'UTF-8')) . '
IP-Adresse:
-
' . htmlspecialchars(getClientIP()) . '
+
' . htmlspecialchars(getClientIP(), ENT_QUOTES, 'UTF-8') . '
@@ -315,138 +253,159 @@ function generateEmailHTML($data) {
'; - + return $html; } -// Text-Version der E-Mail function generateEmailText($data) { - // Betreff (nutzt zentrale SUBJECT_MAP Konstante) - $subject_text = SUBJECT_MAP[$data['subject']] ?? 'Neue Kontaktanfrage'; - $text = "NEUE KONTAKTANFRAGE - HexaHost.de\n"; $text .= "=====================================\n\n"; - $text .= "Betreff: " . $subject_text . "\n"; + $text .= "Betreff: " . getSubjectLabel($data['subject']) . "\n"; $text .= "Name: " . $data['firstName'] . " " . $data['lastName'] . "\n"; $text .= "E-Mail: " . $data['email'] . "\n"; - + if (!empty($data['phone'])) { $text .= "Telefon: " . $data['phone'] . "\n"; } - + if (!empty($data['company'])) { $text .= "Unternehmen: " . $data['company'] . "\n"; } - + $text .= "\nNachricht:\n"; $text .= "----------\n"; $text .= $data['message'] . "\n\n"; - $text .= "IP-Adresse: " . getClientIP() . "\n"; $text .= "Zeitstempel: " . date('d.m.Y H:i:s') . "\n\n"; - $text .= "---\n"; $text .= "Diese E-Mail wurde automatisch vom HexaHost.de Kontaktformular generiert.\n"; $text .= "© " . date('Y') . " HexaHost.de - Alle Rechte vorbehalten"; - + return $text; } -// Hauptverarbeitung try { - // CSRF-Token validieren - if (empty($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { - http_response_code(403); - echo json_encode([ - 'success' => false, - 'message' => 'Ungültige Sitzung. Bitte laden Sie die Seite neu und versuchen Sie es erneut.' - ]); - exit; + if (!empty($config['enable_csrf'])) { + if (empty($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + http_response_code(403); + echo json_encode([ + 'success' => false, + 'message' => 'Ungültige Sitzung. Bitte laden Sie die Seite neu und versuchen Sie es erneut.', + ]); + exit; + } } - - // Rate Limiting Check - $client_ip = getClientIP(); - if (!checkRateLimit($client_ip)) { + + if (!checkRateLimit(getClientIP())) { http_response_code(429); echo json_encode([ - 'success' => false, - 'message' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.' + 'success' => false, + 'message' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.', ]); exit; } - - // Honeypot Check + if (!checkHoneypot($_POST)) { http_response_code(400); - echo json_encode([ - 'success' => false, - 'message' => 'Ungültige Anfrage.' - ]); + echo json_encode(['success' => false, 'message' => 'Ungültige Anfrage.']); exit; } - - // Pflichtfelder prüfen + $required_fields = ['firstName', 'lastName', 'email', 'subject', 'message', 'privacy']; $missing_fields = []; - + foreach ($required_fields as $field) { if (empty($_POST[$field])) { $missing_fields[] = $field; } } - + if (!empty($missing_fields)) { http_response_code(400); echo json_encode([ - 'success' => false, + 'success' => false, 'message' => 'Bitte füllen Sie alle Pflichtfelder aus.', - 'missing_fields' => $missing_fields + 'missing_fields' => $missing_fields, ]); exit; } - - // E-Mail-Validierung - if (!validateEmail($_POST['email'])) { + + $subjectKey = trim((string) $_POST['subject']); + if (!isAllowedContactSubject($subjectKey)) { http_response_code(400); echo json_encode([ - 'success' => false, - 'message' => 'Bitte geben Sie eine gültige E-Mail-Adresse ein.' + 'success' => false, + 'message' => 'Bitte wählen Sie einen gültigen Betreff.', ]); exit; } - - // Daten sanitieren - $data = [ - 'firstName' => sanitizeInput($_POST['firstName']), - 'lastName' => sanitizeInput($_POST['lastName']), - 'email' => sanitizeInput($_POST['email']), - 'phone' => sanitizeInput($_POST['phone'] ?? ''), - 'company' => sanitizeInput($_POST['company'] ?? ''), - 'subject' => sanitizeInput($_POST['subject']), - 'message' => sanitizeInput($_POST['message']), - 'privacy' => isset($_POST['privacy']) ? true : false - ]; - - // E-Mail senden - if (sendEmail($data)) { + + $email = trim((string) $_POST['email']); + if (!isValidEmail($email)) { + http_response_code(400); echo json_encode([ - 'success' => true, - 'message' => 'Ihre Nachricht wurde erfolgreich gesendet! Wir melden uns in Kürze bei Ihnen.' + 'success' => false, + 'message' => 'Bitte geben Sie eine gültige E-Mail-Adresse ein.', + ]); + exit; + } + + $message = trim((string) $_POST['message']); + $messageLength = mb_strlen($message, 'UTF-8'); + + if ($messageLength < $config['min_message_length']) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'message' => 'Ihre Nachricht ist zu kurz.', + ]); + exit; + } + + if ($messageLength > $config['max_message_length']) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'message' => 'Ihre Nachricht ist zu lang.', + ]); + exit; + } + + $data = [ + 'firstName' => sanitizeFormField($_POST['firstName']), + 'lastName' => sanitizeFormField($_POST['lastName']), + 'email' => sanitizeHeaderValue($email), + 'phone' => sanitizeFormField($_POST['phone'] ?? ''), + 'company' => sanitizeFormField($_POST['company'] ?? ''), + 'subject' => $subjectKey, + 'message' => sanitizeFormField($message), + 'privacy' => isset($_POST['privacy']), + ]; + + if (sendEmail($data)) { + if (LOG_EMAILS) { + logEmail('sent', [ + 'subject' => $subjectKey, + 'email' => $data['email'], + 'ip' => getClientIP(), + ]); + } + echo json_encode([ + 'success' => true, + 'message' => 'Ihre Nachricht wurde erfolgreich gesendet! Wir melden uns in Kürze bei Ihnen.', ]); } else { http_response_code(500); echo json_encode([ - 'success' => false, - 'message' => 'Beim Senden der Nachricht ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.' + 'success' => false, + 'message' => 'Beim Senden der Nachricht ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.', ]); } - } catch (Exception $e) { error_log('HexaHost Contact Form Error: ' . $e->getMessage()); http_response_code(500); echo json_encode([ - 'success' => false, - 'message' => 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.' + 'success' => false, + 'message' => 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.', ]); } -?> \ No newline at end of file diff --git a/public/contact.php b/public/contact.php index 2a3d0bb..9298548 100644 --- a/public/contact.php +++ b/public/contact.php @@ -1,5 +1,8 @@ Betreff *
diff --git a/scripts/publish-to-main.ps1 b/scripts/publish-to-main.ps1 new file mode 100644 index 0000000..eda1e43 --- /dev/null +++ b/scripts/publish-to-main.ps1 @@ -0,0 +1,182 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Erstellt einen Production-Build und veröffentlicht ihn auf den Branch main. + +.DESCRIPTION + 1. Wechselt auf main und setzt ihn auf den Stand von dev + 2. Entfernt Kommentare, minifiziert CSS, obfuskiert JavaScript + 3. Committet und pusht main (optional) + 4. Wechselt zurück auf dev (Quellcode bleibt unverändert) + +.PARAMETER Push + Pusht main nach origin (Standard: nur lokaler Commit) + +.PARAMETER DryRun + Führt Git-Schritte nur simuliert aus (Build wird trotzdem erstellt) + +.PARAMETER Message + Commit-Nachricht für den Production-Build + +.EXAMPLE + .\scripts\publish-to-main.ps1 + +.EXAMPLE + .\scripts\publish-to-main.ps1 -Push +#> +[CmdletBinding()] +param( + [switch]$Push, + [switch]$DryRun, + [switch]$AllowDirty, + [string]$Message = "" +) + +$ErrorActionPreference = "Stop" +$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$BuildDir = Join-Path $Root "scripts\build" +$OriginalBranch = "" + +function Write-Step([string]$Text) { + Write-Host "" + Write-Host "==> $Text" -ForegroundColor Cyan +} + +function Ensure-GitClean { + $status = git -C $Root status --porcelain + if ($status) { + throw "Uncommittete Änderungen im Repository. Bitte zuerst committen oder stashen." + } +} + +function Resolve-NodeTool([string]$ToolName) { + $command = Get-Command $ToolName -ErrorAction SilentlyContinue + if ($command) { + return $command.Source + } + + $candidates = @( + (Join-Path $env:ProgramFiles "nodejs\$ToolName.cmd"), + (Join-Path ${env:ProgramFiles(x86)} "nodejs\$ToolName.cmd"), + (Join-Path $env:LOCALAPPDATA "Programs\nodejs\$ToolName.cmd"), + "c:\Program Files\cursor\resources\app\resources\helpers\node.exe" + ) + + foreach ($candidate in $candidates) { + if ($ToolName -eq "node" -and (Test-Path $candidate)) { + return $candidate + } + if ($ToolName -ne "node" -and (Test-Path $candidate)) { + return $candidate + } + } + + return $null +} + +function Ensure-Node { + $script:NodeExe = Resolve-NodeTool "node" + $script:NpmExe = Resolve-NodeTool "npm" + + if (-not $script:NodeExe) { + throw "Node.js ist nicht installiert. Bitte Node.js 18+ installieren: https://nodejs.org/" + } + if (-not $script:NpmExe) { + throw "npm wurde nicht gefunden. Bitte Node.js inkl. npm installieren und PATH setzen." + } +} + +try { + Set-Location $Root + Ensure-Node + if (-not $AllowDirty) { + Ensure-GitClean + } else { + Write-Warning "AllowDirty aktiv – uncommittete Änderungen werden mit veröffentlicht." + } + + $OriginalBranch = (git branch --show-current).Trim() + if ($OriginalBranch -ne "dev") { + Write-Warning "Empfohlen: Auf Branch 'dev' starten (aktuell: $OriginalBranch)" + } + + if ([string]::IsNullOrWhiteSpace($Message)) { + $Message = "chore(release): production build $(Get-Date -Format 'yyyy-MM-dd HH:mm')" + } + + Write-Step "Installiere Build-Abhängigkeiten" + Set-Location $BuildDir + if (-not $DryRun) { + & $NpmExe ci --no-fund --no-audit + if ($LASTEXITCODE -ne 0) { throw "npm ci fehlgeschlagen" } + } + + Write-Step "Wechsle auf main und synchronisiere mit dev" + Set-Location $Root + if ($DryRun) { + Write-Host "[DryRun] git checkout main" + Write-Host "[DryRun] git reset --hard dev" + } else { + git checkout main + git reset --hard dev + } + + Write-Step "Production-Build (Kommentare entfernen, JS obfuscaten)" + Set-Location $BuildDir + if ($DryRun) { + Write-Host "[DryRun] npm run build:in-place" + } else { + & $NpmExe run build:in-place + if ($LASTEXITCODE -ne 0) { throw "Production-Build fehlgeschlagen" } + } + + Write-Step "Production-Build committen" + Set-Location $Root + if ($DryRun) { + Write-Host "[DryRun] git add -A" + Write-Host "[DryRun] git commit -m `"$Message`"" + } else { + git add -A + $null = git diff --cached --quiet + if ($LASTEXITCODE -eq 0) { + Write-Warning "Keine Build-Änderungen – nichts zu committen." + } else { + git commit -m $Message + } + } + + if ($Push) { + Write-Step "Push nach origin/main" + if ($DryRun) { + Write-Host "[DryRun] git push origin main" + } else { + git push origin main + } + } else { + Write-Host "Hinweis: Ohne -Push wurde nur lokal auf main gebaut." -ForegroundColor Yellow + } + + Write-Step "Zurück auf $OriginalBranch" + if (-not $DryRun) { + if ([string]::IsNullOrWhiteSpace($OriginalBranch)) { + git checkout dev + } else { + git checkout $OriginalBranch + } + } + + Write-Host "" + Write-Host "Production-Release abgeschlossen." -ForegroundColor Green + if (-not $Push -and -not $DryRun) { + Write-Host "Zum Veröffentlichen: git push origin main" -ForegroundColor Yellow + } +} +catch { + Write-Host "" + Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red + Set-Location $Root + if ($OriginalBranch -and -not $DryRun) { + git checkout $OriginalBranch 2>$null + } + exit 1 +} diff --git a/scripts/publish-to-main.sh b/scripts/publish-to-main.sh new file mode 100644 index 0000000..f2f4aaa --- /dev/null +++ b/scripts/publish-to-main.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# +# Erstellt einen Production-Build und veröffentlicht ihn auf den Branch main. +# +# 1. Wechselt auf main und setzt ihn auf den Stand von dev +# 2. Entfernt Kommentare, minifiziert CSS, obfuskiert JavaScript +# 3. Committet und pusht main (optional) +# 4. Wechselt zurück auf den ursprünglichen Branch (dev bleibt unverändert) +# +# Nutzung: +# ./scripts/publish-to-main.sh +# ./scripts/publish-to-main.sh --push +# ./scripts/publish-to-main.sh --dry-run +# ./scripts/publish-to-main.sh --allow-dirty --message "chore(release): v1.2" +# + +set -euo pipefail + +PUSH=false +DRY_RUN=false +ALLOW_DIRTY=false +MESSAGE="" + +usage() { + cat <<'EOF' +Usage: publish-to-main.sh [OPTIONS] + +Options: + --push Push nach origin/main + --dry-run Git-Schritte nur anzeigen (Build wird ausgeführt) + --allow-dirty Uncommittete Änderungen erlauben + --message TEXT Commit-Nachricht + -h, --help Hilfe anzeigen +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --push) + PUSH=true + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --allow-dirty) + ALLOW_DIRTY=true + shift + ;; + --message) + MESSAGE="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unbekannte Option: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +BUILD_DIR="$ROOT/scripts/build" +ORIGINAL_BRANCH="" + +step() { + echo "" + echo "==> $1" +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "FEHLER: '$1' nicht gefunden. Bitte installieren und PATH setzen." >&2 + exit 1 + fi +} + +ensure_git_clean() { + if [[ -n "$(git -C "$ROOT" status --porcelain)" ]]; then + echo "FEHLER: Uncommittete Änderungen. Bitte zuerst committen oder stashen." >&2 + exit 1 + fi +} + +cleanup_on_error() { + echo "" + echo "FEHLER: Abgebrochen." >&2 + cd "$ROOT" || true + if [[ -n "$ORIGINAL_BRANCH" && "$DRY_RUN" == false ]]; then + git checkout "$ORIGINAL_BRANCH" 2>/dev/null || true + fi +} + +trap cleanup_on_error ERR + +require_command node +require_command npm +require_command git + +cd "$ROOT" + +if [[ "$ALLOW_DIRTY" == false ]]; then + ensure_git_clean +else + echo "WARNUNG: --allow-dirty aktiv – uncommittete Änderungen werden mit veröffentlicht." >&2 +fi + +ORIGINAL_BRANCH="$(git branch --show-current | tr -d '[:space:]')" +if [[ "$ORIGINAL_BRANCH" != "dev" ]]; then + echo "WARNUNG: Empfohlen auf Branch 'dev' zu starten (aktuell: ${ORIGINAL_BRANCH:-detached})" >&2 +fi + +if [[ -z "$MESSAGE" ]]; then + MESSAGE="chore(release): production build $(date '+%Y-%m-%d %H:%M')" +fi + +step "Installiere Build-Abhängigkeiten" +cd "$BUILD_DIR" +if [[ "$DRY_RUN" == false ]]; then + npm ci --no-fund --no-audit +fi + +step "Wechsle auf main und synchronisiere mit dev" +cd "$ROOT" +if [[ "$DRY_RUN" == true ]]; then + echo "[DryRun] git checkout main" + echo "[DryRun] git reset --hard dev" +else + git checkout main + git reset --hard dev +fi + +step "Production-Build (Kommentare entfernen, JS obfuscaten)" +cd "$BUILD_DIR" +if [[ "$DRY_RUN" == true ]]; then + echo "[DryRun] npm run build:in-place" +else + npm run build:in-place +fi + +step "Production-Build committen" +cd "$ROOT" +if [[ "$DRY_RUN" == true ]]; then + echo "[DryRun] git add -A" + echo "[DryRun] git commit -m \"$MESSAGE\"" +else + git add -A + if git diff --cached --quiet; then + echo "WARNUNG: Keine Build-Änderungen – nichts zu committen." >&2 + else + git commit -m "$MESSAGE" + fi +fi + +if [[ "$PUSH" == true ]]; then + step "Push nach origin/main" + if [[ "$DRY_RUN" == true ]]; then + echo "[DryRun] git push origin main" + else + git push origin main + fi +else + echo "Hinweis: Ohne --push wurde nur lokal auf main gebaut." +fi + +step "Zurück auf ${ORIGINAL_BRANCH:-dev}" +if [[ "$DRY_RUN" == false ]]; then + if [[ -n "$ORIGINAL_BRANCH" ]]; then + git checkout "$ORIGINAL_BRANCH" + else + git checkout dev + fi +fi + +trap - ERR + +echo "" +echo "Production-Release abgeschlossen." +if [[ "$PUSH" == false && "$DRY_RUN" == false ]]; then + echo "Zum Veröffentlichen: git push origin main" +fi diff --git a/scripts/run-build.ps1 b/scripts/run-build.ps1 new file mode 100644 index 0000000..b60b860 --- /dev/null +++ b/scripts/run-build.ps1 @@ -0,0 +1,46 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Erstellt ein Production-Bundle unter dist/ (ohne Branch-Wechsel). +#> +[CmdletBinding()] +param( + [switch]$InPlace +) + +$ErrorActionPreference = "Stop" +$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$BuildDir = Join-Path $Root "scripts\build" + +function Resolve-NodeTool([string]$ToolName) { + $command = Get-Command $ToolName -ErrorAction SilentlyContinue + if ($command) { return $command.Source } + + $candidates = @( + (Join-Path $env:ProgramFiles "nodejs\$ToolName.cmd"), + (Join-Path ${env:ProgramFiles(x86)} "nodejs\$ToolName.cmd") + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { return $candidate } + } + + return $null +} + +$npm = Resolve-NodeTool "npm" +if (-not $npm) { + throw "npm nicht gefunden. Bitte Node.js installieren." +} + +Set-Location $BuildDir +& $npm ci --no-fund --no-audit +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +if ($InPlace) { + & $npm run build:in-place +} else { + & $npm run build +} + +exit $LASTEXITCODE diff --git a/scripts/run-build.sh b/scripts/run-build.sh new file mode 100644 index 0000000..7009c73 --- /dev/null +++ b/scripts/run-build.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# +# Erstellt ein Production-Bundle unter dist/ (ohne Branch-Wechsel). +# +# Nutzung: +# ./scripts/run-build.sh +# ./scripts/run-build.sh --in-place +# + +set -euo pipefail + +IN_PLACE=false + +usage() { + cat <<'EOF' +Usage: run-build.sh [OPTIONS] + +Options: + --in-place Build direkt im Repository (statt dist/) + -h, --help Hilfe anzeigen +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --in-place) + IN_PLACE=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unbekannte Option: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +BUILD_DIR="$ROOT/scripts/build" + +if ! command -v npm >/dev/null 2>&1; then + echo "FEHLER: npm nicht gefunden. Bitte Node.js installieren." >&2 + exit 1 +fi + +cd "$BUILD_DIR" +npm ci --no-fund --no-audit + +if [[ "$IN_PLACE" == true ]]; then + npm run build:in-place +else + npm run build +fi diff --git a/scripts/test-email.php b/scripts/test-email.php index b87dec3..e3feb43 100644 --- a/scripts/test-email.php +++ b/scripts/test-email.php @@ -1,72 +1,48 @@ 'Test', - 'lastName' => 'Benutzer', - 'email' => 'test@example.com', - 'phone' => '+49 123 456789', - 'company' => 'Test GmbH', - 'subject' => 'test-email', - 'message' => 'Dies ist eine Test-E-Mail vom HexaHost.de Kontaktformular.' - ]; - - // E-Mail-Inhalt erstellen + $subject = '[HexaHost.de] Test-E-Mail'; $message = "Test-E-Mail von HexaHost.de\n\n"; - $message .= "Name: " . $test_data['firstName'] . " " . $test_data['lastName'] . "\n"; - $message .= "E-Mail: " . $test_data['email'] . "\n"; - $message .= "Telefon: " . $test_data['phone'] . "\n"; - $message .= "Unternehmen: " . $test_data['company'] . "\n"; - $message .= "Nachricht: " . $test_data['message'] . "\n\n"; $message .= "Zeitstempel: " . date('d.m.Y H:i:s') . "\n"; - $message .= "IP-Adresse: " . $_SERVER['REMOTE_ADDR'] . "\n"; - - // Headers + $headers = [ 'From: ' . $config['from_name'] . ' <' . $config['from_email'] . '>', - 'Reply-To: ' . $test_data['firstName'] . ' ' . $test_data['lastName'] . ' <' . $test_data['email'] . '>', 'MIME-Version: 1.0', 'Content-Type: text/plain; charset=UTF-8', - 'X-Mailer: HexaHost Test Email' + 'X-Mailer: HexaHost Test Email', ]; - - // E-Mail senden - $result = mail($config['to_email'], $subject, $message, implode("\r\n", $headers)); - - return $result; + + return mail($config['to_email'], $subject, $message, implode("\r\n", $headers)); } -// Test ausführen -if (isset($_GET['test'])) { - $result = testEmail(); - - if ($result) { - echo "✅ Test-E-Mail wurde erfolgreich gesendet!"; - } else { - echo "❌ Fehler beim Senden der Test-E-Mail."; - } -} else { - echo "

HexaHost.de E-Mail Test

"; - echo "

Klicken Sie auf den Link, um eine Test-E-Mail zu senden:

"; - echo "Test-E-Mail senden"; - - // Konfiguration anzeigen - echo "

Aktuelle Konfiguration:

"; - $config = getHexaHostConfig(); - echo "
";
-    print_r($config);
-    echo "
"; +if (PHP_SAPI === 'cli') { + echo testEmail() ? "Test-E-Mail gesendet.\n" : "Fehler beim Senden.\n"; + exit; +} + +if (isset($_GET['test'])) { + echo testEmail() + ? 'Test-E-Mail wurde gesendet.' + : 'Fehler beim Senden der Test-E-Mail.'; +} else { + echo '

HexaHost.de E-Mail Test

'; + echo '

Test-E-Mail senden

'; } -?> \ No newline at end of file