diff --git a/anoncaptcha/README.md b/anoncaptcha/README.md
index 322d66a..d567135 100644
--- a/anoncaptcha/README.md
+++ b/anoncaptcha/README.md
@@ -3,7 +3,7 @@ AnonCaptcha Addon
Adds a math-based captcha for anonymous commenters to prevent spam.
-When enabled, anonymous visitors who wish to leave a comment must solve a simple addition problem (e.g. "What is 12 + 37?"). The captcha is generated server-side and signed with an HMAC token, so answers are never exposed in URLs or client-side code.
+When enabled, anonymous visitors who wish to leave a comment must solve a simple addition problem (e.g. "What is 12 + 37?").
Based on Zotlabs hook system.
diff --git a/anoncaptcha/anoncaptcha.php b/anoncaptcha/anoncaptcha.php
index 9e4d8b5..79aab34 100644
--- a/anoncaptcha/anoncaptcha.php
+++ b/anoncaptcha/anoncaptcha.php
@@ -2,95 +2,32 @@
/**
* Name: AnonCaptcha
* Description: Adds a math captcha for anonymous commenters to prevent spam
- * Version: 0.9
+ * Version: 0.91
* Author: ivan zlax <@zlax@ussr.win>
* Maintainer: ivan zlax <@zlax@ussr.win>
*/
-define('ANONCAPTCHA_SECRET', 'heresecretsalt');
-define('ANONCAPTCHA_LIFE', 900);
+define('ANONCAPTCHA_SECRET', 'an0nc@ptch4hubzilla2026!@#$');
+define('ANONCAPTCHA_LIFE', 300);
+/**
+ * Register hooks and config on addon load
+ */
function anoncaptcha_load() {
register_hook('post_local_start', 'addon/anoncaptcha/anoncaptcha.php', 'anoncaptcha_post_local_start');
register_hook('page_end', 'addon/anoncaptcha/anoncaptcha.php', 'anoncaptcha_page_end');
set_config('anoncaptcha', 'enabled', 1);
}
+/**
+ * Unregister hooks and config on addon unload
+ */
function anoncaptcha_unload() {
unregister_hook('post_local_start', 'addon/anoncaptcha/anoncaptcha.php', 'anoncaptcha_post_local_start');
unregister_hook('page_end', 'addon/anoncaptcha/anoncaptcha.php', 'anoncaptcha_page_end');
del_config('anoncaptcha', 'enabled');
}
-/**
- * Generate a PNG image with a number and return as base64 data URI.
- * Uses random background colors and noise to deter bot recognition.
- */
-function anoncaptcha_generate_image($answer) {
- $width = 64;
- $height = 32;
- $img = imagecreatetruecolor($width, $height);
-
- // Random background
- $bgR = rand(180, 230);
- $bgG = rand(180, 230);
- $bgB = rand(180, 230);
- $bg = imagecolorallocate($img, $bgR, $bgG, $bgB);
- imagefill($img, 0, 0, $bg);
-
- // Noise lines
- for ($i = 0; $i < 3; $i++) {
- $color = imagecolorallocate($img, rand(100, 200), rand(100, 200), rand(100, 200));
- imageline($img, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $color);
- }
- // Noise dots
- for ($i = 0; $i < 10; $i++) {
- $color = imagecolorallocate($img, rand(100, 200), rand(100, 200), rand(100, 200));
- imagesetpixel($img, rand(0, $width), rand(0, $height), $color);
- }
-
- // High contrast text (complementary to background)
- imagestring($img, 5, ($width - 40) / 2, 4, (string)$answer,
- imagecolorallocate($img, 255 - $bgR, 255 - $bgG, 255 - $bgB));
-
- // Encode as base64 data URI
- ob_start();
- imagepng($img);
- $data = ob_get_contents();
- ob_end_clean();
- imagedestroy($img);
-
- return 'data:image/png;base64,' . base64_encode($data);
-}
-
-/**
- * Generate a captcha token containing a random sum and an HMAC-signed payload.
- * If use_image is enabled, embeds base64 images for each operand.
- */
-function anoncaptcha_generate_token() {
- $use_image = get_config('anoncaptcha', 'use_image', false);
- $a = rand(5, 49);
- $b = rand(5, 49);
- $answer = $a + $b;
- $expires = time() + ANONCAPTCHA_LIFE;
- $data = $answer . '|' . $expires;
- $hmac = hash_hmac('sha256', $data, ANONCAPTCHA_SECRET);
-
- $token = [
- 'token' => base64_encode($hmac . '|' . $data),
- 'a' => $a,
- 'b' => $b,
- 'use_image' => $use_image,
- ];
-
- if ($use_image) {
- $token['img_a'] = anoncaptcha_generate_image($a);
- $token['img_b'] = anoncaptcha_generate_image($b);
- }
-
- return $token;
-}
-
/**
* Validate the captcha answer submitted via POST.
* Checks that both token and answer are present, the answer is numeric,
@@ -148,54 +85,25 @@ function anoncaptcha_validate_token($token) {
return (int)$answer;
}
-/**
- * Render the captcha form as HTML.
- * Shows image-based operands when use_image is enabled, otherwise text.
- */
-function anoncaptcha_render_captcha() {
- $gen = anoncaptcha_generate_token();
- $use_image = $gen['use_image'];
-
- $question = sprintf(t('What is %d + %d?'), $gen['a'], $gen['b']);
-
- if ($use_image) {
- return '
';
- }
-
- return ''
- . ''
- . ''
- . ''
- . '' . t('Captcha') . ''
- . '
';
-}
-
/**
* Hook: post_local_start — validate captcha before a comment is persisted.
* Skips preview submissions and logged-in users (non-anonymous).
*/
function anoncaptcha_post_local_start(&$data) {
+ // Skip preview submissions
if (!empty($_POST['preview'])) {
return;
}
+ // Skip non-anonymous comments (logged-in users)
if (empty($_POST['anonname']) || empty($_POST['anonmail'])) {
return;
}
+ // Validate captcha for anonymous submissions
if (!anoncaptcha_validate()) {
- $_SESSION['_NOTICES_'] ??= [];
+ if (!isset($_SESSION['_NOTICES_'])) {
+ $_SESSION['_NOTICES_'] = [];
+ }
$_SESSION['_NOTICES_'][] = t('Captcha verification failed. Please try again.');
header('Content-Type: application/json');
@@ -208,7 +116,7 @@ function anoncaptcha_post_local_start(&$data) {
/**
* Hook: page_end — inject JavaScript into the page footer.
* On /moderate pages: adds Approve/Delete buttons (not rendered by default).
- * On other pages: injects the captcha form into anonymous comment editors
+ * On other pages: injects captcha into anonymous comment editors
* and hides moderation buttons for unauthenticated visitors.
*/
function anoncaptcha_page_end($content) {
@@ -217,6 +125,23 @@ function anoncaptcha_page_end($content) {
return;
}
+ // Prevent page caching
+ if (!headers_sent()) {
+ header('Cache-Control: no-cache, no-store, must-revalidate');
+ header('Pragma: no-cache');
+ header('Expires: 0');
+ }
+
+ // Generate captcha token server-side (fresh on every request)
+ $use_image = get_config('anoncaptcha', 'use_image', false);
+ $a = rand(5, 49);
+ $b = rand(5, 49);
+ $answer = $a + $b;
+ $expires = time() + ANONCAPTCHA_LIFE;
+ $data = $answer . '|' . $expires;
+ $hmac = hash_hmac('sha256', $data, ANONCAPTCHA_SECRET);
+ $token = base64_encode($hmac . '|' . $data);
+
$path = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($path, '/moderate') !== false) {
// Inject Approve/Delete buttons on /moderate pages
@@ -258,29 +183,55 @@ JSEOF;
return;
}
- // Encode captcha HTML as base64 to safely embed in JavaScript
- $captcha_html = anoncaptcha_render_captcha();
+ // Build captcha HTML with server-generated token
+ if ($use_image && function_exists('imagecreatetruecolor')) {
+ // Generate images inline
+ $img_a = anoncaptcha_gen_img($a);
+ $img_b = anoncaptcha_gen_img($b);
+ $captcha_html = '';
+ } else {
+ $captcha_html = ''
+ . ''
+ . ''
+ . ''
+ . 'Captcha
';
+ }
+
$b64 = base64_encode($captcha_html);
+ $debug = get_config('anoncaptcha', 'debug', false) ? 'true' : 'false';
$js = <<
(function(){
+var debugEnabled={$debug};
+function dc(msg){if(debugEnabled)console.log('anoncaptcha: '+msg);}
// Show alert when captcha validation fails on item submission
if(typeof jQuery!=='undefined'){
jQuery(document).ajaxComplete(function(event, xhr, settings){
if(settings && settings.url && settings.url.indexOf('item')!==-1){
+ dc('ajaxComplete for item request, url='+settings.url);
try{
var resp = JSON.parse(xhr.responseText);
+ dc('response parsed, captcha_fail='+resp.captcha_fail);
if(resp && resp.captcha_fail){
alert(resp.error_msg || 'Captcha failed');
}
- }catch(e){}
+ }catch(e){
+ dc('error parsing response: '+e.message);
+ }
}
});
}
-// Detect whether the visitor is logged in by checking for
-// elements that only appear for authenticated users.
+// Detect whether the visitor is logged in
function isLoggedIn(){
if(document.querySelector('#my_channels')) return true;
if(document.querySelector('#user-menu')) return true;
@@ -313,39 +264,108 @@ if(typeof MutationObserver!=="undefined"){
// Inject captcha form into anonymous comment editors.
var h=atob('{$b64}');
+dc('captcha HTML loaded, length='+h.length);
function addC(){
var els=document.querySelectorAll('[id^="comment-edit-anon"]');
+ dc('found '+els.length+' comment-edit-anon elements');
for(var i=0;i=25 || document.querySelectorAll('[id^="comment-edit-anon"]').length>0){
+ clearInterval(retry);
+ }
+ },400);
+}
+// Observe for dynamically added comment forms
if(typeof MutationObserver!=="undefined"){
- try{var m=new MutationObserver(addC);m.observe(document.body,{childList:true,subtree:true});}catch(e){}
+ try{
+ var mo=new MutationObserver(function(mutations){
+ for(var m=0;m
JSEOF;
+ // Inject into page content
App::$page['content'] .= $js;
}
+/**
+ * Generate a PNG image with a number and return as base64 data URI.
+ */
+function anoncaptcha_gen_img($answer) {
+ $width = 64;
+ $height = 32;
+ $img = imagecreatetruecolor($width, $height);
+ $bgR = rand(180, 230);
+ $bgG = rand(180, 230);
+ $bgB = rand(180, 230);
+ $bg = imagecolorallocate($img, $bgR, $bgG, $bgB);
+ imagefill($img, 0, 0, $bg);
+ for ($i = 0; $i < 3; $i++) {
+ $color = imagecolorallocate($img, rand(100, 200), rand(100, 200), rand(100, 200));
+ imageline($img, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $color);
+ }
+ for ($i = 0; $i < 10; $i++) {
+ $color = imagecolorallocate($img, rand(100, 200), rand(100, 200), rand(100, 200));
+ imagesetpixel($img, rand(0, $width), rand(0, $height), $color);
+ }
+ imagestring($img, 5, ($width - 40) / 2, 4, (string)$answer,
+ imagecolorallocate($img, 255 - $bgR, 255 - $bgG, 255 - $bgB));
+ ob_start();
+ imagepng($img);
+ $data = ob_get_contents();
+ ob_end_clean();
+ imagedestroy($img);
+ return 'data:image/png;base64,' . base64_encode($data);
+}
+
function anoncaptcha_plugin_admin(&$a) {
$use_image = get_config('anoncaptcha', 'use_image', false);
+ $debug = get_config('anoncaptcha', 'debug', false);
$t = get_markup_template('admin.tpl', 'addon/anoncaptcha/');
$a = replace_macros($t, [
'$submit' => t('Submit'),
'$use_image' => ['use_image', t('Draw digital symbols using graphics'), $use_image, t('When enabled, captcha numbers will be drawn as small images instead of text.')],
+ '$debug' => ['debug', t('Enable debug console logging'), $debug, t('When enabled, shows detailed debug messages in browser console for troubleshooting.')],
]);
}
function anoncaptcha_plugin_admin_post() {
$use_image = intval($_POST['use_image'] ?? 0);
+ $debug = intval($_POST['debug'] ?? 0);
set_config('anoncaptcha', 'use_image', $use_image);
+ set_config('anoncaptcha', 'debug', $debug);
info(t('Settings updated.') . EOL);
}
diff --git a/anoncaptcha/lang/de/strings.php b/anoncaptcha/lang/de/strings.php
index 66f5528..2fb6a97 100644
--- a/anoncaptcha/lang/de/strings.php
+++ b/anoncaptcha/lang/de/strings.php
@@ -14,4 +14,6 @@ App::$strings = [
"Settings updated." => "Einstellungen aktualisiert.",
"Captcha" => "Captcha",
"anti-spam" => "Antispam",
+ "Enable debug console logging" => "Debug-Konsolenprotokollierung aktivieren",
+ "When enabled, shows detailed debug messages in browser console for troubleshooting." => "Wenn aktiviert, werden detaillierte Debug-Nachrichten in der Browser-Konsole angezeigt, um Probleme zu beheben.",
];
diff --git a/anoncaptcha/lang/en/strings.php b/anoncaptcha/lang/en/strings.php
index 4cfa4bb..b737b89 100644
--- a/anoncaptcha/lang/en/strings.php
+++ b/anoncaptcha/lang/en/strings.php
@@ -14,4 +14,6 @@ App::$strings = [
"Settings updated." => "Settings updated.",
"Captcha" => "Captcha",
"anti-spam" => "anti-spam",
+ "Enable debug console logging" => "Enable debug console logging",
+ "When enabled, shows detailed debug messages in browser console for troubleshooting." => "When enabled, shows detailed debug messages in browser console for troubleshooting.",
];
diff --git a/anoncaptcha/lang/es/strings.php b/anoncaptcha/lang/es/strings.php
index 9a4a5f5..5afba1f 100644
--- a/anoncaptcha/lang/es/strings.php
+++ b/anoncaptcha/lang/es/strings.php
@@ -14,4 +14,6 @@ App::$strings = [
"Settings updated." => "Configuración actualizada.",
"Captcha" => "Captcha",
"anti-spam" => "antispam",
+ "Enable debug console logging" => "Habilitar registro de depuración en consola",
+ "When enabled, shows detailed debug messages in browser console for troubleshooting." => "Cuando está habilitado, muestra mensajes de depuración detallados en la consola del navegador para solucionar problemas.",
];
diff --git a/anoncaptcha/lang/fr/strings.php b/anoncaptcha/lang/fr/strings.php
index d1b43da..0786301 100644
--- a/anoncaptcha/lang/fr/strings.php
+++ b/anoncaptcha/lang/fr/strings.php
@@ -14,4 +14,6 @@ App::$strings = [
"Settings updated." => "Paramètres mis à jour.",
"Captcha" => "Captcha",
"anti-spam" => "anti-spam",
+ "Enable debug console logging" => "Activer la journalisation de débogage",
+ "When enabled, shows detailed debug messages in browser console for troubleshooting." => "Lorsqu'activé, affiche des messages de débogage détaillés dans la console du navigateur pour le dépannage.",
];
diff --git a/anoncaptcha/lang/ru/strings.php b/anoncaptcha/lang/ru/strings.php
index 1bccdb0..86c5542 100644
--- a/anoncaptcha/lang/ru/strings.php
+++ b/anoncaptcha/lang/ru/strings.php
@@ -14,4 +14,6 @@ App::$strings = [
"Settings updated." => "Настройки обновлены.",
"Captcha" => "Капча",
"anti-spam" => "от спама",
+ "Enable debug console logging" => "Включить отладку в консоли",
+ "When enabled, shows detailed debug messages in browser console for troubleshooting." => "При включении показывает подробные сообщения отладки в консоли браузера для поиска проблем.",
];
diff --git a/anoncaptcha/view/tpl/admin.tpl b/anoncaptcha/view/tpl/admin.tpl
index df4eeed..8d8d3d1 100644
--- a/anoncaptcha/view/tpl/admin.tpl
+++ b/anoncaptcha/view/tpl/admin.tpl
@@ -1,2 +1,3 @@
{{include file="field_checkbox.tpl" field=$use_image}}
+{{include file="field_checkbox.tpl" field=$debug}}