Math CAPTCHA". * Version: 1.3.5 * Author: Ingo Höttges * License: GPL-2.0-or-later */ if (!defined('ABSPATH')) { exit; } class Simple_Math_Captcha { const PREFIX = 'smc_'; const TTL = 600; // 10 Minuten const OPT_MAX = 'smc_max'; const OPT_MAX_SUM = 'smc_max_sum'; const OPT_POSITION = 'smc_position'; const POSITIONS = ['first', 'second', 'sum', 'random']; const DEFAULT_MAX = 10; const DEFAULT_MAX_SUM = 100; const DEFAULT_POSITION = 'random'; public function __construct() { // Frontend-Hooks add_action('login_form', [$this, 'render']); add_action('lostpassword_form', [$this, 'render']); if ((int) get_option('users_can_register', 0) === 1) { add_action('register_form', [$this, 'render']); add_filter('registration_errors', [$this, 'verify_register'], 10, 3); } add_filter('authenticate', [$this, 'verify_login'], 30, 3); add_action('lostpassword_post', [$this, 'verify_lostpassword']); // Admin-Einstellungen add_action('admin_init', [$this, 'register_settings']); add_action('admin_menu', [$this, 'register_menu']); } // ---- Rendering ---------------------------------------------------------- public function render(): void { $max_gap = $this->get_max(); // max an der Luecke $max_sum = $this->get_max_sum(); // max fuer die Gesamtsumme $pos = $this->resolve_position(); switch ($pos) { case 'first': $a_cap = max(1, min($max_gap, $max_sum - 1)); $a = random_int(1, $a_cap); $b = random_int(1, max(1, $max_sum - $a)); $sum = $a + $b; $expected = $a; break; case 'second': $b_cap = max(1, min($max_gap, $max_sum - 1)); $b = random_int(1, $b_cap); $a = random_int(1, max(1, $max_sum - $b)); $sum = $a + $b; $expected = $b; break; case 'sum': default: $s_cap = max(2, min($max_gap, $max_sum)); $sum = random_int(2, $s_cap); $a = random_int(1, $sum - 1); $b = $sum - $a; $expected = $sum; break; } $id = wp_generate_uuid4(); set_transient(self::PREFIX . $id, (int) $expected, self::TTL); // Inline-Eingabefeld an der Luecke: max 3 Ziffern, schmal, zentriert. // Theme/WP-Login erzwingen sonst 100% Breite via eigenem CSS -> hier per !important ueberschreiben. $input_html = ''; switch ($pos) { case 'first': $equation = $input_html . ' + ' . $b . ' = ' . $sum . ''; break; case 'second': $equation = '' . $a . ' + ' . $input_html . ' = ' . $sum . ''; break; case 'sum': default: $equation = '' . $a . ' + ' . $b . ' = ' . $input_html; break; } // CSS-Override mit hoher Spezifitaet: Theme-Styles schlagen sonst die inline-Breite. ?> '; printf( '', esc_html__('Sicherheitsfrage', 'simple-math-captcha'), $equation ); echo ''; echo '
'; } // ---- Validation --------------------------------------------------------- private function check(): bool { $id = isset($_POST['smc_id']) ? sanitize_text_field(wp_unslash($_POST['smc_id'])) : ''; $ans = isset($_POST['smc_answer']) ? (int) $_POST['smc_answer'] : null; if ($id === '' || !preg_match('/^[0-9a-f-]{36}$/', $id)) { return false; } $expected = get_transient(self::PREFIX . $id); delete_transient(self::PREFIX . $id); if ($expected === false || $ans === null) { return false; } return (int) $ans === (int) $expected; } public function verify_login($user, $username, $password) { if (empty($_POST) || empty($_POST['log']) || empty($_POST['pwd'])) { return $user; } // Wenn WordPress bereits einen Fehler hat (z. B. falsches Passwort), // durchreichen — sonst wuerde unsere Captcha-Fehlermeldung den // originalen Fehler ueberdecken und die native rote WP-Meldung // fuer falsche Anmeldedaten ginge verloren. if (is_wp_error($user)) { return $user; } if (!$this->check()) { return new WP_Error( 'captcha_failed', '' . esc_html__('Fehler', 'simple-math-captcha') . ': ' . esc_html__('Sicherheitsfrage falsch beantwortet.', 'simple-math-captcha') ); } return $user; } public function verify_register($errors, $username = '', $email = '') { if (!$this->check()) { $errors->add( 'captcha_failed', '' . esc_html__('Fehler', 'simple-math-captcha') . ': ' . esc_html__('Sicherheitsfrage falsch beantwortet.', 'simple-math-captcha') ); } return $errors; } public function verify_lostpassword(): void { if (!$this->check()) { wp_die( esc_html__('Sicherheitsfrage falsch beantwortet.', 'simple-math-captcha'), esc_html__('Fehler', 'simple-math-captcha'), ['back_link' => true, 'response' => 403] ); } } // ---- Settings-Helpers --------------------------------------------------- private function get_max(): int { $v = (int) get_option(self::OPT_MAX, self::DEFAULT_MAX); return max(2, min(99, $v)); } private function get_max_sum(): int { $v = (int) get_option(self::OPT_MAX_SUM, self::DEFAULT_MAX_SUM); return max(2, min(999, $v)); } private function resolve_position(): string { $p = (string) get_option(self::OPT_POSITION, self::DEFAULT_POSITION); if ($p === 'random') { $pool = ['first', 'second', 'sum']; return $pool[random_int(0, count($pool) - 1)]; } return in_array($p, self::POSITIONS, true) ? $p : self::DEFAULT_POSITION; } // ---- Admin: Settings API ------------------------------------------------ public function register_settings(): void { register_setting('smc_settings', self::OPT_MAX, [ 'type' => 'integer', 'default' => self::DEFAULT_MAX, 'sanitize_callback' => function ($v) { return max(2, min(99, (int) $v)); }, ]); register_setting('smc_settings', self::OPT_MAX_SUM, [ 'type' => 'integer', 'default' => self::DEFAULT_MAX_SUM, 'sanitize_callback' => function ($v) { return max(2, min(999, (int) $v)); }, ]); register_setting('smc_settings', self::OPT_POSITION, [ 'type' => 'string', 'default' => self::DEFAULT_POSITION, 'sanitize_callback' => function ($v) { return in_array((string) $v, self::POSITIONS, true) ? (string) $v : self::DEFAULT_POSITION; }, ]); add_settings_section( 'smc_main', __('Konfiguration', 'simple-math-captcha'), function () { echo '' . esc_html__( 'Einstellungen der Mathe-Sicherheitsfrage auf Login- und Passwort-Reset-Formularen.', 'simple-math-captcha' ) . '
'; }, 'smc_settings' ); add_settings_field( self::OPT_MAX, __('Maximaler Wert an der Lücke', 'simple-math-captcha'), function () { $v = $this->get_max(); echo ' '; echo '' . esc_html__('Der gesuchte Wert an der mit ? markierten Stelle wird maximal so groß. Erlaubt: 2–99.', 'simple-math-captcha') . '
'; }, 'smc_settings', 'smc_main' ); add_settings_field( self::OPT_MAX_SUM, __('Maximale Summe', 'simple-math-captcha'), function () { $v = $this->get_max_sum(); echo ' '; echo '' . esc_html__('Obergrenze für die Gesamtsumme der Aufgabe (A + B). Begrenzt die sichtbaren Zahlen bei Lücke am Summanden. Erlaubt: 2–999.', 'simple-math-captcha') . '
'; }, 'smc_settings', 'smc_main' ); add_settings_field( self::OPT_POSITION, __('Position der Lücke', 'simple-math-captcha'), function () { $v = (string) get_option(self::OPT_POSITION, self::DEFAULT_POSITION); $opts = [ 'first' => __('Erster Summand (? + B = S)', 'simple-math-captcha'), 'second' => __('Zweiter Summand (A + ? = S)', 'simple-math-captcha'), 'sum' => __('Summe (A + B = ?)', 'simple-math-captcha'), 'random' => __('Zufall (eine der drei Positionen)', 'simple-math-captcha'), ]; echo ''; }, 'smc_settings', 'smc_main' ); } public function register_menu(): void { add_options_page( __('Simple Math CAPTCHA', 'simple-math-captcha'), __('Math CAPTCHA', 'simple-math-captcha'), 'manage_options', 'smc_settings', [$this, 'render_settings_page'] ); } public function render_settings_page(): void { if (!current_user_can('manage_options')) { return; } echo '