From e28527ed91a3f84771639a5d9eefdb58c6e7ce6c Mon Sep 17 00:00:00 2001 From: Friederich Loheide Date: Wed, 23 Sep 2026 06:42:45 +0000 Subject: [PATCH] E-Mails und Druckvorlage im neuen Design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E-Mail: - Nachrichten werden in ein markentreues HTML-Gerüst gelegt (Tabellen + Inline-Styles, damit Outlook & Co. es zuverlässig darstellen); die Kopfleiste nutzt den eingestellten Markenverlauf - neuer Platzhalter {VOUCHER_CARD} rendert den Code als hervorgehobene Karte; bestehende Vorlagen mit {VOUCHER_CODE} funktionieren unverändert weiter, sie werden lediglich eingerahmt - Zeilenumbrüche der Vorlage werden vor dem Einsetzen der Platzhalter umgewandelt, Markup aus dem WYSIWYG-Editor bleibt unangetastet Druck: - neue Standardvorlage als Ticket mit QR-Code, Code, Gültigkeit und Anleitung; zentral in Ui::defaultPrintTemplate() - neuer Platzhalter {QR_CODE}; der QR-Code wird lokal im Browser erzeugt (qrcodejs aus assets/vendor), auch bei Bulk-Erstellung für jede Karte - die Druckvorlage ist am Bildschirm ausgeblendet und erscheint nur im Ausdruck Co-Authored-By: Claude Opus 5 --- admin/settings.php | 26 +- assets/global.css | 4 + includes/Mailer.php | 602 +++++++++++++---------- includes/Ui.php | 20 + index.php | 28 +- lang/de.php | 5 + lang/en.php | 5 + tools/demo/overlay/includes/Database.php | 1 - 8 files changed, 401 insertions(+), 290 deletions(-) diff --git a/admin/settings.php b/admin/settings.php index 79c2532..f81e013 100644 --- a/admin/settings.php +++ b/admin/settings.php @@ -239,26 +239,10 @@ $cs = [ 'smtp_from_name' => $db->getSetting('smtp_from_name', ''), 'system_url' => $db->getSetting('system_url', $autoDetectedUrl), 'email_voucher_subject' => $db->getSetting('email_voucher_subject', '{APP_TITLE} - Ihr WLAN-Zugang'), - 'email_voucher_body' => $db->getSetting('email_voucher_body', "Hallo,\n\nIhr Code: {VOUCHER_CODE}\n\nGültigkeit: 8h\nGeräte: {MAX_USES}\nSite: {SITE_NAME}"), + 'email_voucher_body' => $db->getSetting('email_voucher_body', "Hallo,\n\nhier ist Ihr WLAN-Zugangscode:\n{VOUCHER_CARD}\nMaximale Geräte: {MAX_USES}
\nStandort: {SITE_NAME}\n\n{INSTRUCTIONS}\n\nViele Grüße\n{APP_TITLE}"), 'email_user_notification_subject' => $db->getSetting('email_user_notification_subject', '{APP_TITLE} - Berechtigungen geändert'), 'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"), - 'print_template' => $db->getSetting('print_template', '

{APP_TITLE}

WLAN Code

{VOUCHER_CODE}

Gültig bis: {EXPIRY_DATE} {EXPIRY_TIME}

Site: {SITE_NAME}

Geräte: {MAX_USES}


{INSTRUCTIONS}
'), - 'brand_accent' => $db->getSetting('brand_accent', '') ?: Ui::DEFAULT_ACCENT, - 'brand_accent_dark' => $db->getSetting('brand_accent_dark', '') ?: Ui::DEFAULT_ACCENT_DARK, - 'brand_gradient_from' => $db->getSetting('brand_gradient_from', '') ?: Ui::DEFAULT_GRADIENT_FROM, - 'brand_gradient_to' => $db->getSetting('brand_gradient_to', '') ?: Ui::DEFAULT_GRADIENT_TO, - 'brand_radius' => $db->getSetting('brand_radius', (string)Ui::DEFAULT_RADIUS), - 'login_panel_enabled' => $db->getSetting('login_panel_enabled', '1'), - 'login_brand_name' => $db->getSetting('login_brand_name', ''), - 'login_logo_url' => $db->getSetting('login_logo_url', ''), - 'login_claim_title' => $db->getSetting('login_claim_title', ''), - 'login_claim_text' => $db->getSetting('login_claim_text', ''), - 'login_features' => $db->getSetting('login_features', ''), - 'login_footer' => $db->getSetting('login_footer', ''), - 'login_bg_image' => $db->getSetting('login_bg_image', ''), - 'login_bg_from' => $db->getSetting('login_bg_from', '#3b2f8f'), - 'login_bg_to' => $db->getSetting('login_bg_to', '#6d5ce7'), - 'login_bg_overlay' => $db->getSetting('login_bg_overlay', '40'), + 'print_template' => $db->getSetting('print_template', Ui::defaultPrintTemplate()), 'cron_token' => $db->getSetting('cron_token', ''), 'last_cron_sync' => $db->getSetting('last_cron_sync', ''), ]; @@ -644,12 +628,12 @@ $adminBase = '';
Auto:

-

Platzhalter:

{VOUCHER_CODE}{SITE_NAME}{MAX_USES}{APP_TITLE}{INSTRUCTIONS}
+

{VOUCHER_CARD}{VOUCHER_CODE}{SITE_NAME}{MAX_USES}{APP_TITLE}{INSTRUCTIONS}


-

Platzhalter:

{USER_NAME}{CHANGES}{APP_TITLE}{SYSTEM_URL}
+

{USER_NAME}{CHANGES}{APP_TITLE}{SYSTEM_URL}
@@ -669,7 +653,7 @@ $adminBase = '';

Druck-Template

-

Platzhalter:

{VOUCHER_CODE}{EXPIRY_DATE}{EXPIRY_TIME}{SITE_NAME}{MAX_USES}{APP_TITLE}{INSTRUCTIONS}
+

{QR_CODE}{VOUCHER_CODE}{EXPIRY_DATE}{EXPIRY_TIME}{SITE_NAME}{MAX_USES}{APP_TITLE}{INSTRUCTIONS}

diff --git a/assets/global.css b/assets/global.css index 7657f72..49fe976 100644 --- a/assets/global.css +++ b/assets/global.css @@ -1531,6 +1531,10 @@ input.search-bar, .site-selector .search-bar { min-width: 240px; width: auto; fl .row3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; } @media (max-width: 720px) { .row, .row3 { grid-template-columns: 1fr; } } +/* QR-Code auf der Druckkarte */ +.print-qr { display: flex; justify-content: center; line-height: 0; } +.print-qr img, .print-qr canvas { display: block; } + /* Sprungmarke zum Inhalt (nur bei Tastaturfokus sichtbar) */ .skip-link { position: absolute; diff --git a/includes/Mailer.php b/includes/Mailer.php index 866b2eb..7f5d1ad 100644 --- a/includes/Mailer.php +++ b/includes/Mailer.php @@ -1,265 +1,339 @@ -db = Database::getInstance(); - $this->loadSettings(); - } - - private function loadSettings() { - $this->smtpEnabled = $this->db->getSetting('smtp_enabled', '0') === '1'; - $this->smtpHost = $this->db->getSetting('smtp_host', ''); - $this->smtpPort = (int)$this->db->getSetting('smtp_port', '587'); - $this->smtpUsername = $this->db->getSetting('smtp_username', ''); - $this->smtpPassword = $this->db->getSetting('smtp_password', ''); - $this->smtpEncryption = $this->db->getSetting('smtp_encryption', 'tls'); - $this->fromEmail = $this->db->getSetting('smtp_from_email', 'noreply@' . $_SERVER['HTTP_HOST']); - $this->fromName = $this->db->getSetting('smtp_from_name', $this->db->getSetting('app_title', 'UniFi Voucher System')); - } - - public function sendRaw($to, $subject, $plainBody) { - return $this->send($to, $subject, $plainBody, false); - } - - public function send($to, $subject, $body, $isHtml = false) { - // Bis zu 2 Versuche bei vorübergehenden Zustellfehlern (Retry). - $attempts = 2; - for ($i = 1; $i <= $attempts; $i++) { - if (!$this->smtpEnabled || empty($this->smtpHost)) { - $ok = $this->sendWithPhpMail($to, $subject, $body); - } else { - $ok = $this->sendWithSmtp($to, $subject, $body, $isHtml); - } - if ($ok) { - return true; - } - if ($i < $attempts) { - usleep(500000); // 0,5s vor erneutem Versuch - } - } - error_log("Mailer: Zustellung an {$to} nach {$attempts} Versuchen fehlgeschlagen."); - return false; - } - - private function sendWithPhpMail($to, $subject, $body) { - $headers = "From: {$this->fromName} <{$this->fromEmail}>\r\n"; - $headers .= "Reply-To: {$this->fromEmail}\r\n"; - $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; - - return mail($to, $subject, $body, $headers); - } - - private function sendWithSmtp($to, $subject, $body, $isHtml = false) { - try { - // Verbindung aufbauen - $socket = $this->connectToSmtp(); - - // EHLO - $this->smtpCommand($socket, "EHLO " . $_SERVER['HTTP_HOST']); - - // STARTTLS wenn nötig - if ($this->smtpEncryption === 'tls') { - $this->smtpCommand($socket, "STARTTLS"); - stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); - $this->smtpCommand($socket, "EHLO " . $_SERVER['HTTP_HOST']); - } - - // AUTH LOGIN - $this->smtpCommand($socket, "AUTH LOGIN"); - $this->smtpCommand($socket, base64_encode($this->smtpUsername)); - $this->smtpCommand($socket, base64_encode($this->smtpPassword)); - - // MAIL FROM - $this->smtpCommand($socket, "MAIL FROM:<{$this->fromEmail}>"); - - // RCPT TO - $this->smtpCommand($socket, "RCPT TO:<{$to}>"); - - // DATA - $this->smtpCommand($socket, "DATA"); - - // Headers - $message = "From: {$this->fromName} <{$this->fromEmail}>\r\n"; - $message .= "To: {$to}\r\n"; - $message .= "Subject: =?UTF-8?B?" . base64_encode($subject) . "?=\r\n"; - $message .= "MIME-Version: 1.0\r\n"; - - if ($isHtml) { - $message .= "Content-Type: text/html; charset=UTF-8\r\n"; - } else { - $message .= "Content-Type: text/plain; charset=UTF-8\r\n"; - } - - $message .= "\r\n"; - - // Body - bei Plain Text Zeilenumbrüche konvertieren - if (!$isHtml) { - $body = nl2br($body, false); // Für Plain Text - $body = str_replace('
', "\r\n", $body); - } - - $message .= $body; - $message .= "\r\n.\r\n"; - - fwrite($socket, $message); - $response = fgets($socket); - - // QUIT - $this->smtpCommand($socket, "QUIT"); - fclose($socket); - - return strpos($response, '250') === 0; - - } catch (Exception $e) { - error_log("SMTP Error: " . $e->getMessage()); - return false; - } - } - - private function connectToSmtp() { - $context = stream_context_create([ - 'ssl' => [ - 'verify_peer' => false, - 'verify_peer_name' => false, - 'allow_self_signed' => true - ] - ]); - - if ($this->smtpEncryption === 'ssl') { - $host = 'ssl://' . $this->smtpHost; - } else { - $host = $this->smtpHost; - } - - $socket = stream_socket_client( - $host . ':' . $this->smtpPort, - $errno, - $errstr, - 30, - STREAM_CLIENT_CONNECT, - $context - ); - - if (!$socket) { - throw new Exception("SMTP Connection failed: $errstr ($errno)"); - } - - // Willkommensnachricht lesen - fgets($socket); - - return $socket; - } - - private function smtpCommand($socket, $command) { - fwrite($socket, $command . "\r\n"); - $response = fgets($socket); - - // Prüfen auf Fehler (4xx oder 5xx) - if (preg_match('/^[45]/', $response)) { - throw new Exception("SMTP Error: $response"); - } - - return $response; - } - - // Vordefinierte E-Mail-Templates - public function sendVoucherEmail($to, $voucherCode, $siteName, $maxUses) { - $appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System'); - $instructionHeader = $this->db->getSetting('instruction_header', ''); - $instructionText = $this->db->getSetting('instruction_text', ''); - - // System-URL aus Einstellungen oder automatisch erkennen - $systemUrl = $this->db->getSetting('system_url', ''); - if (empty($systemUrl)) { - $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; - $host = $_SERVER['HTTP_HOST']; - $scriptPath = dirname($_SERVER['SCRIPT_NAME']); - $scriptPath = $scriptPath === '/' ? '' : $scriptPath; - $systemUrl = $protocol . '://' . $host . $scriptPath; - } - - // Template aus Datenbank laden - $subjectTemplate = $this->db->getSetting('email_voucher_subject', '{APP_TITLE} - Ihr WLAN-Zugang'); - $bodyTemplate = $this->db->getSetting('email_voucher_body', "Hallo,\n\nIhr WLAN-Zugangscode lautet:\n\n{VOUCHER_CODE}\n\nGültigkeit: 8 Stunden ab Erstellung\nMaximale Geräte: {MAX_USES}\nStandort: {SITE_NAME}\n\n{INSTRUCTIONS}\n\nMit freundlichen Grüßen\n{APP_TITLE}"); - - // Anleitung formatieren - $instructions = ''; - if ($instructionText) { - $instructions = $instructionHeader . "\n" . $instructionText; - } - - // Platzhalter ersetzen - $placeholders = [ - '{VOUCHER_CODE}' => $voucherCode, - '{SITE_NAME}' => $siteName, - '{MAX_USES}' => $maxUses, - '{APP_TITLE}' => $appTitle, - '{INSTRUCTIONS}' => $instructions, - '{SYSTEM_URL}' => $systemUrl - ]; - - $subject = str_replace(array_keys($placeholders), array_values($placeholders), $subjectTemplate); - $body = str_replace(array_keys($placeholders), array_values($placeholders), $bodyTemplate); - - // HTML oder Plain Text prüfen - $isHtml = strip_tags($body) !== $body; - - return $this->send($to, $subject, $body, $isHtml); - } - - public function sendTestEmail($to) { - $appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System'); - $subject = '[Test] E-Mail-Konfiguration – ' . $appTitle; - $body = "Dies ist eine Test-E-Mail von {$appTitle}.\n\nDie SMTP-Konfiguration ist korrekt eingerichtet."; - return $this->send($to, $subject, $body, false); - } - - public function sendUserNotification($to, $userName, $changes) { - $appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System'); - - // System-URL aus Einstellungen oder automatisch erkennen - $systemUrl = $this->db->getSetting('system_url', ''); - if (empty($systemUrl)) { - $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; - $host = $_SERVER['HTTP_HOST']; - $scriptPath = dirname($_SERVER['SCRIPT_NAME']); - $scriptPath = $scriptPath === '/' ? '' : $scriptPath; - $systemUrl = $protocol . '://' . $host . $scriptPath; - } - - // Template aus Datenbank laden - $subjectTemplate = $this->db->getSetting('email_user_notification_subject', '{APP_TITLE} - Ihre Berechtigungen wurden geändert'); - $bodyTemplate = $this->db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\nEin Administrator hat Ihre Berechtigungen im {APP_TITLE} geändert:\n\n{CHANGES}\n\nSie können sich unter folgender Adresse anmelden:\n{SYSTEM_URL}\n\nMit freundlichen Grüßen\n{APP_TITLE}"); - - // Änderungen formatieren - $changesText = ''; - foreach ($changes as $change) { - $changesText .= "• $change\n"; - } - - // Platzhalter ersetzen - $placeholders = [ - '{USER_NAME}' => $userName, - '{CHANGES}' => $changesText, - '{APP_TITLE}' => $appTitle, - '{SYSTEM_URL}' => $systemUrl - ]; - - $subject = str_replace(array_keys($placeholders), array_values($placeholders), $subjectTemplate); - $body = str_replace(array_keys($placeholders), array_values($placeholders), $bodyTemplate); - - // HTML oder Plain Text prüfen - $isHtml = strip_tags($body) !== $body; - - return $this->send($to, $subject, $body, $isHtml); - } +db = Database::getInstance(); + $this->loadSettings(); + } + + private function loadSettings() { + $this->smtpEnabled = $this->db->getSetting('smtp_enabled', '0') === '1'; + $this->smtpHost = $this->db->getSetting('smtp_host', ''); + $this->smtpPort = (int)$this->db->getSetting('smtp_port', '587'); + $this->smtpUsername = $this->db->getSetting('smtp_username', ''); + $this->smtpPassword = $this->db->getSetting('smtp_password', ''); + $this->smtpEncryption = $this->db->getSetting('smtp_encryption', 'tls'); + $this->fromEmail = $this->db->getSetting('smtp_from_email', 'noreply@' . $_SERVER['HTTP_HOST']); + $this->fromName = $this->db->getSetting('smtp_from_name', $this->db->getSetting('app_title', 'UniFi Voucher System')); + } + + public function sendRaw($to, $subject, $plainBody) { + return $this->send($to, $subject, $plainBody, false); + } + + public function send($to, $subject, $body, $isHtml = false) { + // Bis zu 2 Versuche bei vorübergehenden Zustellfehlern (Retry). + $attempts = 2; + for ($i = 1; $i <= $attempts; $i++) { + if (!$this->smtpEnabled || empty($this->smtpHost)) { + $ok = $this->sendWithPhpMail($to, $subject, $body); + } else { + $ok = $this->sendWithSmtp($to, $subject, $body, $isHtml); + } + if ($ok) { + return true; + } + if ($i < $attempts) { + usleep(500000); // 0,5s vor erneutem Versuch + } + } + error_log("Mailer: Zustellung an {$to} nach {$attempts} Versuchen fehlgeschlagen."); + return false; + } + + private function sendWithPhpMail($to, $subject, $body) { + $headers = "From: {$this->fromName} <{$this->fromEmail}>\r\n"; + $headers .= "Reply-To: {$this->fromEmail}\r\n"; + $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; + + return mail($to, $subject, $body, $headers); + } + + private function sendWithSmtp($to, $subject, $body, $isHtml = false) { + try { + // Verbindung aufbauen + $socket = $this->connectToSmtp(); + + // EHLO + $this->smtpCommand($socket, "EHLO " . $_SERVER['HTTP_HOST']); + + // STARTTLS wenn nötig + if ($this->smtpEncryption === 'tls') { + $this->smtpCommand($socket, "STARTTLS"); + stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + $this->smtpCommand($socket, "EHLO " . $_SERVER['HTTP_HOST']); + } + + // AUTH LOGIN + $this->smtpCommand($socket, "AUTH LOGIN"); + $this->smtpCommand($socket, base64_encode($this->smtpUsername)); + $this->smtpCommand($socket, base64_encode($this->smtpPassword)); + + // MAIL FROM + $this->smtpCommand($socket, "MAIL FROM:<{$this->fromEmail}>"); + + // RCPT TO + $this->smtpCommand($socket, "RCPT TO:<{$to}>"); + + // DATA + $this->smtpCommand($socket, "DATA"); + + // Headers + $message = "From: {$this->fromName} <{$this->fromEmail}>\r\n"; + $message .= "To: {$to}\r\n"; + $message .= "Subject: =?UTF-8?B?" . base64_encode($subject) . "?=\r\n"; + $message .= "MIME-Version: 1.0\r\n"; + + if ($isHtml) { + $message .= "Content-Type: text/html; charset=UTF-8\r\n"; + } else { + $message .= "Content-Type: text/plain; charset=UTF-8\r\n"; + } + + $message .= "\r\n"; + + // Body - bei Plain Text Zeilenumbrüche konvertieren + if (!$isHtml) { + $body = nl2br($body, false); // Für Plain Text + $body = str_replace('
', "\r\n", $body); + } + + $message .= $body; + $message .= "\r\n.\r\n"; + + fwrite($socket, $message); + $response = fgets($socket); + + // QUIT + $this->smtpCommand($socket, "QUIT"); + fclose($socket); + + return strpos($response, '250') === 0; + + } catch (Exception $e) { + error_log("SMTP Error: " . $e->getMessage()); + return false; + } + } + + private function connectToSmtp() { + $context = stream_context_create([ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true + ] + ]); + + if ($this->smtpEncryption === 'ssl') { + $host = 'ssl://' . $this->smtpHost; + } else { + $host = $this->smtpHost; + } + + $socket = stream_socket_client( + $host . ':' . $this->smtpPort, + $errno, + $errstr, + 30, + STREAM_CLIENT_CONNECT, + $context + ); + + if (!$socket) { + throw new Exception("SMTP Connection failed: $errstr ($errno)"); + } + + // Willkommensnachricht lesen + fgets($socket); + + return $socket; + } + + private function smtpCommand($socket, $command) { + fwrite($socket, $command . "\r\n"); + $response = fgets($socket); + + // Prüfen auf Fehler (4xx oder 5xx) + if (preg_match('/^[45]/', $response)) { + throw new Exception("SMTP Error: $response"); + } + + return $response; + } + + // Vordefinierte E-Mail-Templates + /** + * Legt den Nachrichtentext in ein schlichtes, markentreues HTML-Gerüst. + * Bewusst Tabellen + Inline-Styles: nur so rendern Outlook & Co. zuverlässig. + */ + private function brandedHtml(string $title, string $contentHtml, string $footerNote = ''): string + { + $accent = $this->db->getSetting('brand_gradient_from', '') ?: '#5b5bd6'; + $accent2 = $this->db->getSetting('brand_gradient_to', '') ?: '#8b5cf6'; + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $accent)) { $accent = '#5b5bd6'; } + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $accent2)) { $accent2 = '#8b5cf6'; } + + $safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + $year = date('Y'); + $footer = $footerNote !== '' ? '
' . htmlspecialchars($footerNote, ENT_QUOTES, 'UTF-8') . '
' : ''; + + return '' + . '' + . '' . $safeTitle . '' + . '' + . '' + . '
' + . '' + . '' + . '' + . '' + . '
' + . '
' . $safeTitle . '
' + . '
' . $contentHtml . '
' + . '© ' . $year . ' ' . $safeTitle . $footer + . '
'; + } + + /** + * Voucher-Code als hervorgehobene Karte für die E-Mail. + */ + private function voucherCardHtml(string $code, string $siteName, $maxUses): string + { + return '' + . '
' + . '
' + . htmlspecialchars($siteName, ENT_QUOTES, 'UTF-8') . '
' + . '
' + . htmlspecialchars($code, ENT_QUOTES, 'UTF-8') . '
' + . '
' + . htmlspecialchars((string)$maxUses, ENT_QUOTES, 'UTF-8') . ' ' + . htmlspecialchars(function_exists('__') ? __('label_devices') : 'Geräte', ENT_QUOTES, 'UTF-8') . '
' + . '
'; + } + + public function sendVoucherEmail($to, $voucherCode, $siteName, $maxUses) { + $appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System'); + $instructionHeader = $this->db->getSetting('instruction_header', ''); + $instructionText = $this->db->getSetting('instruction_text', ''); + + // System-URL aus Einstellungen oder automatisch erkennen + $systemUrl = $this->db->getSetting('system_url', ''); + if (empty($systemUrl)) { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST']; + $scriptPath = dirname($_SERVER['SCRIPT_NAME']); + $scriptPath = $scriptPath === '/' ? '' : $scriptPath; + $systemUrl = $protocol . '://' . $host . $scriptPath; + } + + // Template aus Datenbank laden + $subjectTemplate = $this->db->getSetting('email_voucher_subject', '{APP_TITLE} - Ihr WLAN-Zugang'); + $bodyTemplate = $this->db->getSetting('email_voucher_body', "Hallo,\n\nhier ist Ihr WLAN-Zugangscode:\n{VOUCHER_CARD}\nMaximale Geräte: {MAX_USES}
\nStandort: {SITE_NAME}\n\n{INSTRUCTIONS}\n\nViele Grüße\n{APP_TITLE}"); + + // Anleitung formatieren + $instructions = ''; + if ($instructionText) { + $instructions = $instructionHeader . "\n" . $instructionText; + } + + // Platzhalter ersetzen + $placeholders = [ + '{VOUCHER_CARD}' => $this->voucherCardHtml($voucherCode, (string)$siteName, $maxUses), + '{VOUCHER_CODE}' => $voucherCode, + '{SITE_NAME}' => $siteName, + '{MAX_USES}' => $maxUses, + '{APP_TITLE}' => $appTitle, + '{INSTRUCTIONS}' => $instructions, + '{SYSTEM_URL}' => $systemUrl + ]; + + $subject = str_replace(array_keys($placeholders), array_values($placeholders), $subjectTemplate); + + // Umbrueche der Vorlage vor dem Einsetzen der Platzhalter umwandeln, + // sonst wuerde das Markup der Voucher-Karte die Erkennung stoeren. + $isHtml = strip_tags($bodyTemplate) !== $bodyTemplate || strpos($bodyTemplate, '{VOUCHER_CARD}') !== false; + $template = $isHtml ? $this->textToHtml($bodyTemplate) : $bodyTemplate; + $body = str_replace(array_keys($placeholders), array_values($placeholders), $template); + + if ($isHtml) { + $body = $this->brandedHtml($appTitle, $body); + } + + return $this->send($to, $subject, $body, $isHtml); + } + + public function sendTestEmail($to) { + $appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System'); + $subject = '[Test] E-Mail-Konfiguration – ' . $appTitle; + $body = "Dies ist eine Test-E-Mail von {$appTitle}.\n\nDie SMTP-Konfiguration ist korrekt eingerichtet."; + return $this->send($to, $subject, $body, false); + } + + public function sendUserNotification($to, $userName, $changes) { + $appTitle = $this->db->getSetting('app_title', 'UniFi Voucher System'); + + // System-URL aus Einstellungen oder automatisch erkennen + $systemUrl = $this->db->getSetting('system_url', ''); + if (empty($systemUrl)) { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST']; + $scriptPath = dirname($_SERVER['SCRIPT_NAME']); + $scriptPath = $scriptPath === '/' ? '' : $scriptPath; + $systemUrl = $protocol . '://' . $host . $scriptPath; + } + + // Template aus Datenbank laden + $subjectTemplate = $this->db->getSetting('email_user_notification_subject', '{APP_TITLE} - Ihre Berechtigungen wurden geändert'); + $bodyTemplate = $this->db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\nEin Administrator hat Ihre Berechtigungen im {APP_TITLE} geändert:\n\n{CHANGES}\n\nSie können sich unter folgender Adresse anmelden:\n{SYSTEM_URL}\n\nMit freundlichen Grüßen\n{APP_TITLE}"); + + // Änderungen formatieren + $changesText = ''; + foreach ($changes as $change) { + $changesText .= "• $change\n"; + } + + // Platzhalter ersetzen + $placeholders = [ + '{USER_NAME}' => $userName, + '{CHANGES}' => $changesText, + '{APP_TITLE}' => $appTitle, + '{SYSTEM_URL}' => $systemUrl + ]; + + $subject = str_replace(array_keys($placeholders), array_values($placeholders), $subjectTemplate); + + $isHtml = strip_tags($bodyTemplate) !== $bodyTemplate; + $template = $isHtml ? $this->textToHtml($bodyTemplate) : $bodyTemplate; + $body = str_replace(array_keys($placeholders), array_values($placeholders), $template); + + if ($isHtml) { + $body = $this->brandedHtml($appTitle, $body); + } + + return $this->send($to, $subject, $body, $isHtml); + } + + /** + * Zeilenumbrüche aus dem Vorlagentext in HTML übernehmen, ohne bereits + * vorhandenes Markup (z. B. aus dem WYSIWYG-Editor) zu zerstören. + */ + private function textToHtml(string $body): string + { + if (preg_match('#<(p|div|ul|ol|h[1-6])[\s>]#i', $body)) { + return $body; + } + + return nl2br($body, false); + } } \ No newline at end of file diff --git a/includes/Ui.php b/includes/Ui.php index 8f3b3d0..c2d3815 100644 --- a/includes/Ui.php +++ b/includes/Ui.php @@ -116,6 +116,26 @@ class Ui . ''; } + /** + * Standard-Druckvorlage (wird nur verwendet, solange keine eigene + * Vorlage gespeichert ist). {QR_CODE} fuellt der Browser. + */ + public static function defaultPrintTemplate(): string + { + $validUntil = function_exists('__') ? __('print_valid_until') : 'Gültig bis'; + $devices = function_exists('__') ? __('print_devices') : 'Geräte'; + + return '
' + . '
{APP_TITLE}
' + . '
{SITE_NAME}
' + . '{QR_CODE}' + . '
{VOUCHER_CODE}
' + . '
' . $validUntil . ' {EXPIRY_DATE} {EXPIRY_TIME} · {MAX_USES} ' . $devices . '
' + . '
{INSTRUCTIONS}
' + . '
'; + } + /** * URL eines Bildes aus den Einstellungen. * Hochgeladene Dateien liegen relativ zur Projektwurzel (uploads/…), diff --git a/index.php b/index.php index 1919c76..2ff5177 100644 --- a/index.php +++ b/index.php @@ -75,7 +75,7 @@ $instructionHeader = $db->getSetting('instruction_header', ''); $instructionText = $db->getSetting('instruction_text', ''); $publicAccess = $db->getSetting('public_access', 0); $smtpEnabled = $db->getSetting('smtp_enabled', '0') === '1'; -$printTemplate = $db->getSetting('print_template', '

{APP_TITLE}

WLAN Zugangscode

{VOUCHER_CODE}

Gültig bis: {EXPIRY_DATE} {EXPIRY_TIME}

Standort: {SITE_NAME}

Maximale Geräte: {MAX_USES}


{INSTRUCTIONS}
'); +$printTemplate = $db->getSetting('print_template', Ui::defaultPrintTemplate()); $defaultExpire = max(1, (int)$db->getSetting('default_expire_minutes', 480)); $defaultMaxUses = max(1, (int)$db->getSetting('default_max_uses', 1)); $maxUsesLimit = max(1, (int)$db->getSetting('max_uses_limit', 10)); @@ -272,9 +272,12 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText, $instructions = $instructionHeader || $instructionText ? htmlspecialchars($instructionHeader) . "\n" . $instructionText : ''; + // {QR_CODE} wird erst im Browser gefuellt (siehe renderPrintQr()). + $qr = ''; + return str_replace( - ['{VOUCHER_CODE}', '{SITE_NAME}', '{MAX_USES}', '{APP_TITLE}', '{INSTRUCTIONS}', '{EXPIRY_DATE}', '{EXPIRY_TIME}'], - [$data['code'], htmlspecialchars($data['site_name']), $data['max_uses'], htmlspecialchars($appTitle), $instructions, $data['expiry_date'], $data['expiry_time']], + ['{QR_CODE}', '{VOUCHER_CODE}', '{SITE_NAME}', '{MAX_USES}', '{APP_TITLE}', '{INSTRUCTIONS}', '{EXPIRY_DATE}', '{EXPIRY_TIME}'], + [$qr, $data['code'], htmlspecialchars($data['site_name']), $data['max_uses'], htmlspecialchars($appTitle), $instructions, $data['expiry_date'], $data['expiry_time']], $template ); } @@ -289,7 +292,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText, - +