diff --git a/admin/integrations.php b/admin/integrations.php index e237ad2..f7872aa 100644 --- a/admin/integrations.php +++ b/admin/integrations.php @@ -24,6 +24,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) { $error = __('error_csrf'); } else { $db->setSetting('enforce_2fa_admins', isset($_POST['enforce_2fa_admins']) ? '1' : '0'); + $cm = in_array($_POST['captcha_mode'] ?? 'off', ['off','math','hcaptcha'], true) ? $_POST['captcha_mode'] : 'off'; + $db->setSetting('captcha_mode', $cm); + $db->setSetting('captcha_site_key', trim($_POST['captcha_site_key'] ?? '')); + if (!empty($_POST['captcha_secret'])) { $db->setSetting('captcha_secret', trim($_POST['captcha_secret'])); } $db->setSetting('user_daily_voucher_limit', max(0, (int)($_POST['user_daily_voucher_limit'] ?? 0))); $db->setSetting('trusted_proxy', trim($_POST['trusted_proxy'] ?? '')); $db->setSetting('webhook_enabled', isset($_POST['webhook_enabled']) ? '1' : '0'); @@ -42,6 +46,9 @@ if (isset($_GET['test_webhook']) && isset($_GET['token']) && $auth->validateCsrf } $enforce2fa = $db->getSetting('enforce_2fa_admins', '0') === '1'; +$captchaMode = $db->getSetting('captcha_mode', 'off'); +$captchaSiteKey = $db->getSetting('captcha_site_key', ''); +$captchaSecretSet = $db->getSetting('captcha_secret', '') !== ''; $dailyLimit = (int)$db->getSetting('user_daily_voucher_limit', 0); $trustedProxy = $db->getSetting('trusted_proxy', ''); $webhookEnabled = $db->getSetting('webhook_enabled', '0') === '1'; @@ -90,6 +97,16 @@ label { display:block; font-size:14px; color:var(--text-secondary); margin:14px + + +
+
+
+
diff --git a/includes/Captcha.php b/includes/Captcha.php new file mode 100644 index 0000000..a923c5c --- /dev/null +++ b/includes/Captcha.php @@ -0,0 +1,56 @@ +getSetting('captcha_mode', 'off'); + return in_array($m, ['off', 'math', 'hcaptcha'], true) ? $m : 'off'; + } + + /** Frage für das Math-Captcha erzeugen und Antwort in Session hinterlegen. */ + public static function newMathChallenge() { + $a = random_int(1, 9); + $b = random_int(1, 9); + $_SESSION['captcha_answer'] = (string)($a + $b); + return "$a + $b"; + } + + /** Prüft die Captcha-Antwort des aktuellen Requests. */ + public static function verify($db) { + $mode = self::mode($db); + if ($mode === 'off') { + return true; + } + if ($mode === 'math') { + $expected = $_SESSION['captcha_answer'] ?? null; + unset($_SESSION['captcha_answer']); // einmalig + $given = trim((string)($_POST['captcha'] ?? '')); + return $expected !== null && hash_equals((string)$expected, $given); + } + if ($mode === 'hcaptcha') { + $resp = $_POST['h-captcha-response'] ?? ''; + if ($resp === '') return false; + $secret = (string)$db->getSetting('captcha_secret', ''); + $ch = curl_init('https://hcaptcha.com/siteverify'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query(['secret' => $secret, 'response' => $resp]), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 8, + ]); + $out = curl_exec($ch); + curl_close($ch); + $data = json_decode((string)$out, true); + return is_array($data) && !empty($data['success']); + } + return true; + } +} diff --git a/index.php b/index.php index e90b7fc..58d3036 100644 --- a/index.php +++ b/index.php @@ -17,6 +17,7 @@ require_once __DIR__ . '/includes/Auth.php'; require_once __DIR__ . '/includes/UniFiController.php'; require_once __DIR__ . '/includes/Mailer.php'; require_once __DIR__ . '/includes/Notifier.php'; +require_once __DIR__ . '/includes/Captcha.php'; require_once __DIR__ . '/includes/I18n.php'; $auth = new Auth(); @@ -151,6 +152,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) { $error = __('error_csrf'); } elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) { $error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.'; + } elseif (!$auth->isLoggedIn() && !Captcha::verify($db)) { + $error = 'Captcha-Prüfung fehlgeschlagen. Bitte erneut versuchen.'; } else { try { $siteId = (int)($_POST['site_id'] ?? 0); @@ -203,6 +206,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_bulk'])) { $error = __('error_csrf'); } elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) { $error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.'; + } elseif (!$auth->isLoggedIn() && !Captcha::verify($db)) { + $error = 'Captcha-Prüfung fehlgeschlagen. Bitte erneut versuchen.'; } else { try { $siteId = (int)($_POST['site_id'] ?? 0); @@ -242,6 +247,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_bulk'])) { $currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null; +// 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', '') : ''; +$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 @@ -261,6 +278,9 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText, <?= htmlspecialchars($appTitle) ?> + + + @@ -510,6 +530,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText, +
@@ -570,6 +591,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText, +