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 '
' - . '' - . '' - . '' - . '' . t('Captcha') . '' - . '
'; - } - - 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 = '
' + . '' + . '' + . '' + . 'Captcha
'; + } 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}}