Initial: Simple Math CAPTCHA v1.3.4 + plugin-update-checker (auto-updates via Gitea info.json)

This commit is contained in:
2026-04-23 13:39:59 +02:00
commit 2c0fdc68f5
122 changed files with 11697 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
<?php
/**
* Plugin Name: Simple Math CAPTCHA
* Description: Minimalistische Mathe-CAPTCHA fuer Login, Registrierung und Passwort-Reset.
* Keine externen Requests, kein JavaScript, kein Tracking, keine Tabellen.
* Loesung wird serverseitig als einmal-verwendbarer Transient gespeichert (10 min TTL).
* Einstellungen unter "Einstellungen > Math CAPTCHA".
* Version: 1.3.4
* 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 = '<input type="text" name="smc_answer" id="smc_answer" '
. 'required autocomplete="off" inputmode="numeric" '
. 'pattern="[0-9]{1,3}" maxlength="3">';
switch ($pos) {
case 'first':
$equation = $input_html . ' + <strong>' . $b . '</strong> = <strong>' . $sum . '</strong>';
break;
case 'second':
$equation = '<strong>' . $a . '</strong> + ' . $input_html . ' = <strong>' . $sum . '</strong>';
break;
case 'sum':
default:
$equation = '<strong>' . $a . '</strong> + <strong>' . $b . '</strong> = ' . $input_html;
break;
}
// CSS-Override mit hoher Spezifitaet: Theme-Styles schlagen sonst die inline-Breite.
?>
<style>
input#smc_answer[type=text] {
width: 2.8em !important;
max-width: 2.8em !important;
min-width: 0 !important;
height: 1.5em !important;
display: inline-block !important;
padding: 0 4px !important;
text-align: center !important;
font-weight: bold !important;
font-size: 1em !important;
line-height: 1 !important;
margin: 0 4px !important;
box-sizing: content-box !important;
vertical-align: middle !important;
}
</style>
<?php
echo '<p>';
printf(
'<label>%s: %s</label>',
esc_html__('Sicherheitsfrage', 'simple-math-captcha'),
$equation
);
echo '<input type="hidden" name="smc_id" value="' . esc_attr($id) . '">';
echo '</p>';
}
// ---- 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;
}
if (!$this->check()) {
return new WP_Error(
'captcha_failed',
'<strong>' . esc_html__('Fehler', 'simple-math-captcha') . ':</strong> '
. esc_html__('Sicherheitsfrage falsch beantwortet.', 'simple-math-captcha')
);
}
return $user;
}
public function verify_register($errors, $username = '', $email = '') {
if (!$this->check()) {
$errors->add(
'captcha_failed',
'<strong>' . esc_html__('Fehler', 'simple-math-captcha') . ':</strong> '
. 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 '<p>' . esc_html__(
'Einstellungen der Mathe-Sicherheitsfrage auf Login- und Passwort-Reset-Formularen.',
'simple-math-captcha'
) . '</p>';
},
'smc_settings'
);
add_settings_field(
self::OPT_MAX,
__('Maximaler Wert an der Lücke', 'simple-math-captcha'),
function () {
$v = $this->get_max();
echo '<input type="number" name="' . esc_attr(self::OPT_MAX) . '" '
. 'value="' . esc_attr((string) $v) . '" min="2" max="99" class="small-text"> ';
echo '<p class="description">'
. esc_html__('Der gesuchte Wert an der mit ? markierten Stelle wird maximal so groß. Erlaubt: 299.', 'simple-math-captcha')
. '</p>';
},
'smc_settings',
'smc_main'
);
add_settings_field(
self::OPT_MAX_SUM,
__('Maximale Summe', 'simple-math-captcha'),
function () {
$v = $this->get_max_sum();
echo '<input type="number" name="' . esc_attr(self::OPT_MAX_SUM) . '" '
. 'value="' . esc_attr((string) $v) . '" min="2" max="999" class="small-text"> ';
echo '<p class="description">'
. esc_html__('Obergrenze für die Gesamtsumme der Aufgabe (A + B). Begrenzt die sichtbaren Zahlen bei Lücke am Summanden. Erlaubt: 2999.', 'simple-math-captcha')
. '</p>';
},
'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 '<select name="' . esc_attr(self::OPT_POSITION) . '">';
foreach ($opts as $key => $label) {
echo '<option value="' . esc_attr($key) . '"' . selected($v, $key, false) . '>'
. esc_html($label) . '</option>';
}
echo '</select>';
},
'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 '<div class="wrap"><h1>' . esc_html__('Simple Math CAPTCHA', 'simple-math-captcha') . '</h1>';
echo '<form method="post" action="options.php">';
settings_fields('smc_settings');
do_settings_sections('smc_settings');
submit_button();
echo '</form></div>';
}
}
new Simple_Math_Captcha();
// ----------------------------------------------------------------------------
// Auto-Updates von eigener Gitea-Instanz (plugin-update-checker, MIT).
// Pruef-URL: info.json auf main-Branch. Bei neuer Version laedt WP das ZIP
// aus der download_url im info.json (Gitea-Archive eines Tags).
// ----------------------------------------------------------------------------
$smc_puc = __DIR__ . '/plugin-update-checker/plugin-update-checker.php';
if (is_readable($smc_puc)) {
require_once $smc_puc;
if (class_exists('\\YahnisElsts\\PluginUpdateChecker\\v5\\PucFactory')) {
\YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
'https://git.rosinenkot.de/ingo/simple-math-captcha/raw/branch/main/info.json',
__FILE__,
'simple-math-captcha'
);
}
}
unset($smc_puc);