mirror of
https://git.hexahost.dev/smueller/HexaHost-Frontend.git
synced 2026-06-02 03:58:43 +00:00
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:
21
README.md
21
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:
|
||||
|
||||
8
backend/.htaccess
Normal file
8
backend/.htaccess
Normal 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>
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* HexaDNS - DNS Lookup API
|
||||
*
|
||||
*
|
||||
* Führt echte DNS-Abfragen durch und gibt die Ergebnisse als JSON zurück.
|
||||
*
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* HexaDNS - DNS Propagation Check API
|
||||
*
|
||||
*
|
||||
* Prüft DNS-Records bei verschiedenen öffentlichen DNS-Servern
|
||||
*
|
||||
*
|
||||
* 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)));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* HexaDNS - Reverse DNS Lookup API
|
||||
*
|
||||
*
|
||||
* Löst eine IP-Adresse zu einem Hostnamen auf
|
||||
*
|
||||
*
|
||||
* 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)) {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* HexaDNS - SSL Certificate Check API
|
||||
*
|
||||
*
|
||||
* Prüft SSL-Zertifikat-Informationen einer Domain
|
||||
*
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* HexaDNS - WHOIS Lookup API
|
||||
*
|
||||
*
|
||||
* Ruft WHOIS-Informationen für eine Domain ab
|
||||
*
|
||||
*
|
||||
* 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);
|
||||
|
||||
64
backend/config/contact-config.php
Normal file
64
backend/config/contact-config.php
Normal 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 '';
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
112
backend/includes/api-helpers.php
Normal file
112
backend/includes/api-helpers.php
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
?>
|
||||
5
public/config/mail-config.php
Normal file
5
public/config/mail-config.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
/**
|
||||
* Kompatibilitäts-Wrapper – leitet auf die zentrale Backend-Konfiguration um.
|
||||
*/
|
||||
require_once __DIR__ . '/../../backend/config/mail-config.php';
|
||||
@@ -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();
|
||||
|
||||
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 = '
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
@@ -267,39 +205,39 @@ 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>';
|
||||
}
|
||||
|
||||
|
||||
if (!empty($data['company'])) {
|
||||
$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">
|
||||
@@ -315,138 +253,159 @@ function generateEmailHTML($data) {
|
||||
</div>
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
|
||||
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.',
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -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
182
scripts/publish-to-main.ps1
Normal 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
187
scripts/publish-to-main.sh
Normal 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
46
scripts/run-build.ps1
Normal 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
58
scripts/run-build.sh
Normal 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
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
// 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 "<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>";
|
||||
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 '<h1>HexaHost.de E-Mail Test</h1>';
|
||||
echo '<p><a href="?test=1">Test-E-Mail senden</a></p>';
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user