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.

This commit is contained in:
smueller
2026-05-22 14:50:20 +02:00
parent 5f5be4a4cb
commit ebf6f82bb6
21 changed files with 1007 additions and 355 deletions

View File

@@ -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:

8
backend/.htaccess Normal file
View File

@@ -0,0 +1,8 @@
# Direkten Zugriff auf Backend-Dateien verhindern (Document Root = public/)
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>

View File

@@ -7,6 +7,8 @@
* Verwendung: GET /api/dns-lookup.php?domain=example.com
*/
require_once __DIR__ . '/../includes/api-helpers.php';
// CORS Headers für Frontend-Zugriff
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
@@ -19,26 +21,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
// Nur GET-Anfragen erlauben
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['error' => '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;
}

View File

@@ -7,6 +7,8 @@
* Verwendung: GET /api/dns-propagation.php?domain=example.com&type=A
*/
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,6 +19,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
if (!checkApiRateLimit('dns-propagation')) {
rejectApiRateLimit();
}
// Öffentliche DNS-Server für Propagation-Check
$dnsServers = [
['name' => '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)));

View File

@@ -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

View File

@@ -7,6 +7,8 @@
* Verwendung: GET /api/reverse-dns.php?ip=8.8.8.8
*/
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,6 +19,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
if (!checkApiRateLimit('reverse-dns')) {
rejectApiRateLimit();
}
$ip = isset($_GET['ip']) ? trim($_GET['ip']) : '';
if (empty($ip)) {

View File

@@ -7,6 +7,8 @@
* Verwendung: GET /api/ssl-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,22 +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('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;
}

View File

@@ -7,6 +7,8 @@
* Verwendung: GET /api/whois-lookup.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,7 +19,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
if (!checkApiRateLimit('whois-lookup')) {
rejectApiRateLimit();
}
$domain = isset($_GET['domain']) ? trim((string) $_GET['domain']) : '';
if (empty($domain)) {
http_response_code(400);

View File

@@ -0,0 +1,64 @@
<?php
/**
* Zentrale Betreff-Konfiguration für das Kontaktformular
*/
/**
* @return array<string, string> 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 '';
}

View File

@@ -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,

View File

@@ -0,0 +1,112 @@
<?php
/**
* Gemeinsame Hilfsfunktionen für öffentliche DNS/API-Endpunkte
*/
/**
* Client-IP für Rate-Limiting (Cloudflare-sicher, kein blindes X-Forwarded-For)
*/
function getApiClientIp(): 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;
}
/**
* Einfaches Rate-Limiting pro Endpunkt und IP
*/
function checkApiRateLimit(string $endpoint, int $maxPerHour = 120): bool {
$ip = getApiClientIp();
$cacheFile = sys_get_temp_dir() . '/hexahost_api_' . md5($endpoint . '_' . $ip) . '.txt';
$currentTime = time();
$data = ['requests' => []];
$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;
}

View File

@@ -160,7 +160,7 @@
<script src="/assets/js/cookie-consent.js" defer></script>
<?php if (isset($additional_scripts)): ?>
<?php foreach ($additional_scripts as $script): ?>
<script src="<?php echo $script; ?>" defer></script>
<script src="<?php echo htmlspecialchars($script, ENT_QUOTES, 'UTF-8'); ?>" defer></script>
<?php endforeach; ?>
<?php endif; ?>
</body>

View File

@@ -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;
}
?>

View File

@@ -0,0 +1,5 @@
<?php
/**
* Kompatibilitäts-Wrapper leitet auf die zentrale Backend-Konfiguration um.
*/
require_once __DIR__ . '/../../backend/config/mail-config.php';

View File

@@ -4,55 +4,28 @@
* E-Mail-Verarbeitung mit SMTP-Integration und Spam-Schutz
*/
// Session starten für CSRF-Validierung
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Konfiguration laden
require_once 'config/mail-config.php';
require_once __DIR__ . '/../backend/includes/functions.php';
require_once __DIR__ . '/../backend/config/mail-config.php';
require_once __DIR__ . '/../backend/config/contact-config.php';
// PHPMailer Autoload (falls via Composer installiert)
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
// Konfiguration verwenden
$config = getHexaHostConfig();
// Betreff-Mapping (zentral definiert)
const SUBJECT_MAP = [
'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',
'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();
$data = ['requests' => []];
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;
});
$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;
}
}
}
// Füge aktuellen Request hinzu
$data = isset($data) ? $data : ['requests' => []];
$data['requests'][] = $current_time;
file_put_contents($cache_file, json_encode($data));
ftruncate($handle, 0);
rewind($handle);
fwrite($handle, json_encode($data));
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
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;
@@ -167,71 +118,58 @@ function sendEmail($data) {
$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;
$subject = '[HexaHost.de] ' . getSubjectLabel($data['subject']);
$replyName = sanitizeHeaderValue($data['firstName'] . ' ' . $data['lastName']);
$replyEmail = sanitizeHeaderValue($data['email']);
// Headers für Spam-Schutz
$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 = '
<!DOCTYPE html>
@@ -267,19 +205,19 @@ function generateEmailHTML($data) {
<div class="field">
<div class="label">Name:</div>
<div class="value">' . $data['firstName'] . ' ' . $data['lastName'] . '</div>
<div class="value">' . htmlspecialchars($data['firstName'] . ' ' . $data['lastName'], ENT_QUOTES, 'UTF-8') . '</div>
</div>
<div class="field">
<div class="label">E-Mail:</div>
<div class="value">' . $data['email'] . '</div>
<div class="value">' . htmlspecialchars($data['email'], ENT_QUOTES, 'UTF-8') . '</div>
</div>';
if (!empty($data['phone'])) {
$html .= '
<div class="field">
<div class="label">Telefon:</div>
<div class="value">' . $data['phone'] . '</div>
<div class="value">' . htmlspecialchars($data['phone'], ENT_QUOTES, 'UTF-8') . '</div>
</div>';
}
@@ -287,19 +225,19 @@ function generateEmailHTML($data) {
$html .= '
<div class="field">
<div class="label">Unternehmen:</div>
<div class="value">' . $data['company'] . '</div>
<div class="value">' . htmlspecialchars($data['company'], ENT_QUOTES, 'UTF-8') . '</div>
</div>';
}
$html .= '
<div class="field">
<div class="label">Nachricht:</div>
<div class="message">' . nl2br($data['message']) . '</div>
<div class="message">' . nl2br(htmlspecialchars($data['message'], ENT_QUOTES, 'UTF-8')) . '</div>
</div>
<div class="field">
<div class="label">IP-Adresse:</div>
<div class="value">' . htmlspecialchars(getClientIP()) . '</div>
<div class="value">' . htmlspecialchars(getClientIP(), ENT_QUOTES, 'UTF-8') . '</div>
</div>
<div class="field">
@@ -319,14 +257,10 @@ 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";
@@ -341,10 +275,8 @@ function generateEmailText($data) {
$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";
@@ -352,40 +284,33 @@ function generateEmailText($data) {
return $text;
}
// Hauptverarbeitung
try {
// CSRF-Token validieren
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.'
'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.'
'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 = [];
@@ -400,53 +325,87 @@ try {
echo json_encode([
'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.'
'message' => 'Bitte wählen Sie einen gültigen Betreff.',
]);
exit;
}
$email = trim((string) $_POST['email']);
if (!isValidEmail($email)) {
http_response_code(400);
echo json_encode([
'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;
}
// 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
'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']),
];
// E-Mail senden
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.'
'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.'
'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.'
'message' => 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
]);
}
?>

View File

@@ -1,5 +1,8 @@
<?php
require_once __DIR__ . '/../backend/includes/functions.php';
require_once __DIR__ . '/../backend/config/contact-config.php';
$preselected_subject = getPreselectedContactSubject();
// Page configuration
$page_title = 'Kontakt - HexaHost.de | Hosting aus Niederbayern';
@@ -129,21 +132,9 @@ includeHeader($page_title, $page_description, $current_page, $additional_scripts
<label for="subject">Betreff *</label>
<select id="subject" name="subject" required>
<option value="">Bitte wählen...</option>
<option value="allgemeine-anfrage">Allgemeine Anfrage</option>
<option value="vpc-anfrage">Virtual Private Container</option>
<option value="vps-anfrage">Virtual Private Server</option>
<option value="mail-gateway-anfrage">Mail Gateway</option>
<option value="webhosting-anfrage">Webhosting</option>
<option value="it-beratung">IT-Beratung</option>
<option value="it-support">IT-Support & Fehlerbehebung</option>
<option value="netzwerk-wlan">Netzwerk & WLAN-Einrichtung</option>
<option value="it-sicherheit-backup">IT-Sicherheit & Backup</option>
<option value="webseiten-hosting-service">Webseiten- & Hosting-Service</option>
<option value="wartung-betreuung">Wartung & Betreuung</option>
<option value="support">Technischer Support</option>
<option value="beratung">Persönliche Beratung</option>
<option value="migration">Migration/Umzug</option>
<option value="sonstiges">Sonstiges</option>
<?php foreach (getContactSubjectMap() as $subjectKey => $subjectLabel): ?>
<option value="<?php echo htmlspecialchars($subjectKey, ENT_QUOTES, 'UTF-8'); ?>"<?php echo $preselected_subject === $subjectKey ? ' selected' : ''; ?>><?php echo htmlspecialchars($subjectLabel, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">

182
scripts/publish-to-main.ps1 Normal file
View File

@@ -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
}

187
scripts/publish-to-main.sh Normal file
View File

@@ -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

46
scripts/run-build.ps1 Normal file
View File

@@ -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

58
scripts/run-build.sh Normal file
View File

@@ -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

View File

@@ -1,72 +1,48 @@
<?php
/**
* HexaHost.de E-Mail Test
* Testet die E-Mail-Funktionalität ohne PHPMailer
* HexaHost.de E-Mail Test (nur CLI oder lokale Entwicklung)
*/
// Konfiguration laden
require_once 'config.php';
if (PHP_SAPI !== 'cli') {
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
$isLocal = in_array($remoteAddr, ['127.0.0.1', '::1'], true)
|| filter_var($remoteAddr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
if (!$isLocal) {
http_response_code(403);
exit('Forbidden');
}
}
require_once __DIR__ . '/../backend/config/mail-config.php';
// Test-E-Mail senden
function testEmail() {
$config = getHexaHostConfig();
// Test-Daten
$test_data = [
'firstName' => '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));
}
if (PHP_SAPI === 'cli') {
echo testEmail() ? "Test-E-Mail gesendet.\n" : "Fehler beim Senden.\n";
exit;
}
// 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.";
}
echo testEmail()
? 'Test-E-Mail wurde gesendet.'
: 'Fehler beim Senden der Test-E-Mail.';
} else {
echo "<h1>HexaHost.de E-Mail Test</h1>";
echo "<p>Klicken Sie auf den Link, um eine Test-E-Mail zu senden:</p>";
echo "<a href='?test=1'>Test-E-Mail senden</a>";
// Konfiguration anzeigen
echo "<h2>Aktuelle Konfiguration:</h2>";
$config = getHexaHostConfig();
echo "<pre>";
print_r($config);
echo "</pre>";
echo '<h1>HexaHost.de E-Mail Test</h1>';
echo '<p><a href="?test=1">Test-E-Mail senden</a></p>';
}
?>