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,
+ = $captchaHtml ?>