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 = '';
= __('settings_tpl_voucher_mail') ?>
- Platzhalter:
{VOUCHER_CODE}{SITE_NAME}{MAX_USES}{APP_TITLE}{INSTRUCTIONS}
+ = __('settings_placeholders') ?>
{VOUCHER_CARD}{VOUCHER_CODE}{SITE_NAME}{MAX_USES}{APP_TITLE}{INSTRUCTIONS}
= __('settings_card_hint') ?>
= __('settings_tpl_user_notify') ?>
- Platzhalter:
{USER_NAME}{CHANGES}{APP_TITLE}{SYSTEM_URL}
+ = __('settings_placeholders') ?>
{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}
+ = __('settings_placeholders') ?>
{QR_CODE}{VOUCHER_CODE}{EXPIRY_DATE}{EXPIRY_TIME}{SITE_NAME}{MAX_USES}{APP_TITLE}{INSTRUCTIONS}
= __('settings_qr_hint') ?>
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,
-
+
= Ui::script('assets/vendor/qrcodejs/qrcode.min.js') ?>