Session-Fallback (Legacy-Verhalten) $now = time(); $timestamps = $_SESSION['voucher_create_times'] ?? []; $timestamps = array_values(array_filter($timestamps, function ($t) use ($now, $window) { return ($now - $t) < $window; })); if (count($timestamps) + $voucherCount > $maxVouchers) { $_SESSION['voucher_create_times'] = $timestamps; return true; } for ($i = 0; $i < $voucherCount; $i++) { $timestamps[] = $now; } $_SESSION['voucher_create_times'] = $timestamps; return false; } /** * Validiert die Voucher-Gueltigkeit (Minuten). Anonyme Nutzer duerfen nur den * konfigurierten Default oder Werte aktiver Templates verwenden – das Feld ist * ein Hidden-Input und damit beliebig manipulierbar. Eingeloggte Nutzer werden * auf maximal 1 Jahr begrenzt. */ function sanitizeExpireMinutes($expireMinutes, $isLoggedIn, $templates, $defaultExpire) { $expireMinutes = (int)$expireMinutes; if ($isLoggedIn) { return max(1, min(525600, $expireMinutes)); } $allowed = array_map(function ($t) { return (int)$t['expire_minutes']; }, $templates); $allowed[] = $defaultExpire; return in_array($expireMinutes, $allowed, true) ? $expireMinutes : $defaultExpire; } /** Minuten menschenlesbar formatieren (z.B. 480 -> "8 Stunden"). */ function formatDuration($minutes) { $minutes = (int)$minutes; if ($minutes >= 1440 && $minutes % 1440 === 0) { $days = $minutes / 1440; return $days === 1 ? __('dur_day_one') : __('dur_days', ['n' => $days]); } if ($minutes >= 60 && $minutes % 60 === 0) { $hours = $minutes / 60; return $hours === 1 ? __('dur_hour_one') : __('dur_hours', ['n' => $hours]); } return __('dur_minutes', ['n' => $minutes]); } /** * Optionales Tageslimit pro (Nicht-Admin-)Benutzer (Setting * user_daily_voucher_limit, 0 = aus). Verhindert übermäßige Erstellung. */ function userDailyLimitExceeded($db, $auth, $additional = 1) { if (!$auth->isLoggedIn() || $auth->isAdmin()) { return false; } $limit = (int)$db->getSetting('user_daily_voucher_limit', 0); if ($limit <= 0) { return false; } $uid = $_SESSION['user_id'] ?? 0; $today = (int)($db->fetchOne( "SELECT COUNT(*) c FROM vouchers WHERE user_id=? AND DATE(created_at)=CURDATE()", [$uid] )['c'] ?? 0); return ($today + $additional) > $limit; } $appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); $logoUrl = $db->getSetting('logo_url', ''); $faviconUrl = $db->getSetting('favicon_url', ''); $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}
'); $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)); if (!$publicAccess && !$auth->isLoggedIn()) { header('Location: login.php'); exit; } try { $templates = $db->fetchAll("SELECT * FROM voucher_templates WHERE is_active = 1 ORDER BY name"); } catch (Exception $e) { $templates = []; } $error = ''; $success = ''; $voucherCode = ''; $voucherCreated = false; $voucherData = []; $bulkVouchers = []; $bulkCreated = false; if ($auth->isLoggedIn()) { if ($auth->isAdmin()) { $sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 ORDER BY name"); } else { $sites = $db->fetchAll( "SELECT s.* FROM sites s INNER JOIN user_site_access usa ON s.id = usa.site_id WHERE s.is_active = 1 AND usa.user_id = ? ORDER BY s.name", [$_SESSION['user_id']] ); } } else { $sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 AND public_access = 1 ORDER BY name"); } $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0; // Helper: create one voucher and save to DB function doCreateVoucher($db, $site, $voucherName, $maxUses, $expireMinutes, $userId, $qos = []) { $datum = date('Y-m-d'); $fullName = $datum . '_' . $voucherName; $controller = new UniFiController( $site['unifi_controller_url'], $site['unifi_username'], Crypto::decrypt($site['unifi_password']), $site['site_id'], $site['ssl_verify'] ?? 0 ); $voucher = $controller->createVoucher($fullName, $maxUses, $expireMinutes, $qos); if (!is_array($voucher) || empty($voucher['formatted_code'])) { throw new Exception(__('error_voucher_invalid')); } $db->execute( "INSERT INTO vouchers (site_id, user_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id) VALUES (?, ?, ?, ?, ?, ?, ?)", [$site['id'], $userId, $voucher['code'], $fullName, $maxUses, $expireMinutes, $voucher['unifi_id'] ?? null] ); $expiryTs = time() + ($expireMinutes * 60); return [ 'code' => $voucher['formatted_code'], 'site_name' => $site['name'], 'max_uses' => $maxUses, 'expire_min' => $expireMinutes, 'expiry_date' => date('d.m.Y', $expiryTs), 'expiry_time' => date('H:i', $expiryTs), ]; } // Single voucher if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) { if (!$publicAccess && !$auth->isLoggedIn()) { $error = __('error_login_req'); } elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { // CSRF fuer ALLE (auch anonyme oeffentliche Erstellung) $error = __('error_csrf'); } elseif (!$auth->isLoggedIn() && isVoucherRateLimited($db)) { $error = __('error_rate_limited'); } elseif (!$auth->isLoggedIn() && !Captcha::verify($db)) { $error = 'Captcha-Prüfung fehlgeschlagen. Bitte erneut versuchen.'; } else { try { $siteId = (int)($_POST['site_id'] ?? 0); $voucherName = trim((string)($_POST['voucher_name'] ?? '')); $maxUses = (int)($_POST['max_uses'] ?? $defaultMaxUses); $expireMinutes = sanitizeExpireMinutes($_POST['expire_minutes'] ?? $defaultExpire, $auth->isLoggedIn(), $templates, $defaultExpire); $sendEmail = isset($_POST['send_email']) && !empty($_POST['recipient_email']); $recipientEmail= trim((string)($_POST['recipient_email'] ?? '')); if (empty($voucherName)) throw new Exception(__('error_name_req')); if ($maxUses < 1 || $maxUses > $maxUsesLimit) throw new Exception(__('error_devices_range', ['max' => $maxUsesLimit])); if ($siteId <= 0) throw new Exception(__('error_site_req')); if ($sendEmail && !filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) throw new Exception(__('error_email_invalid')); if ($auth->isLoggedIn() && !$auth->hasAccessToSite($siteId)) throw new Exception(__('error_site_no_perm')); if (userDailyLimitExceeded($db, $auth, 1)) throw new Exception('Tageslimit für Voucher erreicht.'); $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]); if (!$site) throw new Exception(__('error_site_not_found')); $userId = $auth->isLoggedIn() ? ($_SESSION['user_id'] ?? null) : null; $qos = [ 'down' => max(0, (int)($_POST['qos_down'] ?? 0)), 'up' => max(0, (int)($_POST['qos_up'] ?? 0)), 'quota_mb' => max(0, (int)($_POST['qos_quota'] ?? 0)), ]; $voucherData = doCreateVoucher($db, $site, $voucherName, $maxUses, $expireMinutes, $userId, $qos); $voucherCode = $voucherData['code']; $voucherCreated = true; Notifier::voucherCreated(1, $site['name'], $_SESSION['user_name'] ?? null); if ($sendEmail && !empty($recipientEmail)) { $mailer->sendVoucherEmail($recipientEmail, $voucherCode, $site['name'], $maxUses); $success = __('voucher_created_mail'); } else { $success = __('voucher_created_ok'); } // Optional: Code per SMS (Twilio) $recipientPhone = trim((string)($_POST['recipient_phone'] ?? '')); if (isset($_POST['send_sms']) && $recipientPhone !== '' && Sms::enabled($db)) { $smsText = ($appTitle ? $appTitle . ': ' : '') . 'WLAN-Code ' . $voucherCode; $success .= Sms::send($db, $recipientPhone, $smsText) ? ' SMS versendet.' : ' (SMS fehlgeschlagen)'; } $auth->writeAuditLog($userId, 'voucher_create', 'voucher', null, "Voucher '{$voucherName}' für {$site['name']}" . ($userId === null ? ' (öffentlich)' : '')); // PRG-Pattern: Redirect nach erfolgreichem POST, damit ein Reload // (F5) keinen Duplikat-Voucher erzeugt. Ergebnis via Session-Flash. $_SESSION['voucher_flash'] = ['type' => 'single', 'data' => $voucherData, 'success' => $success]; header('Location: index.php?created=1'); exit; } catch (Exception $e) { $error = 'Fehler: ' . $e->getMessage(); } } } // Bulk voucher creation – nur fuer eingeloggte Nutzer. Das Formular wird // Anonymen zwar nicht angezeigt, der POST-Endpunkt muss es aber ebenfalls // serverseitig erzwingen (sonst 20 Voucher pro Request im Public-Modus). if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_bulk'])) { if (!$auth->isLoggedIn()) { $error = __('error_login_req'); } elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { $error = __('error_csrf'); } else { try { $siteId = (int)($_POST['site_id'] ?? 0); $voucherName = trim((string)($_POST['voucher_name'] ?? '')); $maxUses = (int)($_POST['max_uses'] ?? $defaultMaxUses); $expireMinutes = sanitizeExpireMinutes($_POST['expire_minutes'] ?? $defaultExpire, true, $templates, $defaultExpire); $bulkCount = max(1, min(20, (int)($_POST['bulk_count'] ?? 1))); if (empty($voucherName)) throw new Exception(__('error_name_req')); if ($maxUses < 1 || $maxUses > $maxUsesLimit) throw new Exception(__('error_devices_range', ['max' => $maxUsesLimit])); if ($siteId <= 0) throw new Exception(__('error_site_req')); if (!$auth->hasAccessToSite($siteId)) throw new Exception(__('error_site_no_perm')); if (userDailyLimitExceeded($db, $auth, $bulkCount)) throw new Exception('Tageslimit für Voucher erreicht.'); $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]); if (!$site) throw new Exception(__('error_site_not_found')); $userId = $_SESSION['user_id'] ?? null; $qos = [ 'down' => max(0, (int)($_POST['qos_down'] ?? 0)), 'up' => max(0, (int)($_POST['qos_up'] ?? 0)), 'quota_mb' => max(0, (int)($_POST['qos_quota'] ?? 0)), ]; // Alle Voucher in EINEM UniFi-API-Call erstellen ('n'-Parameter) // statt pro Voucher Login + Voucherliste abzurufen. $fullName = date('Y-m-d') . '_' . $voucherName; $controller = new UniFiController( $site['unifi_controller_url'], $site['unifi_username'], Crypto::decrypt($site['unifi_password']), $site['site_id'], $site['ssl_verify'] ?? 0 ); $created = $controller->createVouchers($fullName, $maxUses, $expireMinutes, $bulkCount, $qos); $expiryTs = time() + ($expireMinutes * 60); foreach ($created as $i => $voucher) { $db->execute( "INSERT INTO vouchers (site_id, user_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id) VALUES (?, ?, ?, ?, ?, ?, ?)", [$site['id'], $userId, $voucher['code'], $fullName . '_' . ($i + 1), $maxUses, $expireMinutes, $voucher['unifi_id'] ?? null] ); $bulkVouchers[] = [ 'code' => $voucher['formatted_code'], 'site_name' => $site['name'], 'max_uses' => $maxUses, 'expire_min' => $expireMinutes, 'expiry_date' => date('d.m.Y', $expiryTs), 'expiry_time' => date('H:i', $expiryTs), ]; } Notifier::voucherCreated(count($created), $site['name'], $_SESSION['user_name'] ?? null); $auth->writeAuditLog($userId, 'voucher_bulk', 'voucher', null, count($created) . " Vouchers '{$voucherName}' für {$site['name']}"); $success = str_replace('{count}', count($created), __('bulk_success')); // PRG-Pattern: Reload darf die Bulk-Erstellung nicht wiederholen. $_SESSION['voucher_flash'] = ['type' => 'bulk', 'data' => $bulkVouchers, 'success' => $success]; header('Location: index.php?created=1'); exit; } catch (Exception $e) { $error = 'Fehler: ' . $e->getMessage(); } } } // PRG: Ergebnis nach Redirect aus dem Session-Flash wiederherstellen. // Der Flash bleibt fuer Reloads der Ergebnisseite erhalten und wird beim // Zurueckkehren zum Formular (GET ohne ?created) verworfen. if (isset($_GET['created']) && !empty($_SESSION['voucher_flash'])) { $flash = $_SESSION['voucher_flash']; if (($flash['type'] ?? '') === 'bulk') { $bulkVouchers = $flash['data']; $bulkCreated = true; } else { $voucherData = $flash['data']; $voucherCode = $voucherData['code']; $voucherCreated = true; } $success = $flash['success'] ?? ''; } elseif ($_SERVER['REQUEST_METHOD'] !== 'POST') { unset($_SESSION['voucher_flash']); } $currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null; // Bei Validierungsfehlern: eingegebene Werte und aktiven Tab erhalten $activeMode = ($error && isset($_POST['create_bulk'])) ? 'bulk' : 'single'; $stickyName = $error ? trim((string)($_POST['voucher_name'] ?? '')) : ''; $stickyMaxUses = $error ? (int)($_POST['max_uses'] ?? $defaultMaxUses) : $defaultMaxUses; $stickyBulkCount = $error ? max(1, min(20, (int)($_POST['bulk_count'] ?? 5))) : 5; $stickySiteId = $error ? (int)($_POST['site_id'] ?? 0) : 0; if ($stickyMaxUses < 1 || $stickyMaxUses > $maxUsesLimit) $stickyMaxUses = $defaultMaxUses; // Anonyme Gaeste wissen oft nicht, was sie als Namen eintragen sollen -> Default if ($stickyName === '' && !$auth->isLoggedIn()) $stickyName = __('voucher_name_default'); // Captcha nur für anonyme öffentliche Erstellung $captchaMode = !$auth->isLoggedIn() ? Captcha::mode($db) : 'off'; $captchaQuestion = $captchaMode === 'math' ? Captcha::newMathChallenge() : ''; $hcaptchaSiteKey = $captchaMode === 'hcaptcha' ? $db->getSetting('captcha_site_key', '') : ''; $smsEnabled = Sms::enabled($db); $captchaHtml = ''; if ($captchaMode === 'math') { $captchaHtml = '
' . '
'; } elseif ($captchaMode === 'hcaptcha' && $hcaptchaSiteKey !== '') { $captchaHtml = '
'; } // Build print HTML for each voucher function buildPrintCard($template, $data, $instructionHeader, $instructionText, $appTitle) { $instructions = $instructionHeader || $instructionText ? htmlspecialchars($instructionHeader) . "\n" . $instructionText : ''; 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']], $template ); } ?> <?= htmlspecialchars($appTitle) ?>
$label): ?>
👋 htmlspecialchars($currentUser['name'])]) ?>
isAdmin()): ?> ⚙️
🔐

$bv): ?>
$bv): ?>
#

📶


isAdmin()): ?>

isLoggedIn()): ?>
isLoggedIn()): ?>