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