Initial: Simple Math CAPTCHA v1.3.4 + plugin-update-checker (auto-updates via Gitea info.json)
This commit is contained in:
@@ -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: 2–99.', '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: 2–999.', '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);
|
||||
Reference in New Issue
Block a user