Captcha implementation
Also Dada generator is OK
This commit is contained in:
205
action.php
205
action.php
@@ -62,7 +62,7 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
|
||||
$username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) ? $INFO['userinfo']['name'] : '');
|
||||
|
||||
// build the tracker code:
|
||||
$code = "document._botmon = {'t0': Date.now(), 'session': '" . json_encode($this->sessionId) . "'};" . NL;
|
||||
$code = "document._botmon = {t0: Date.now(), session: " . json_encode($this->sessionId) . ", seed: " . json_encode($this->getConf('captchaSeed')) . ", ip: " . json_encode($_SERVER['REMOTE_ADDR']) . "};" . NL;
|
||||
if ($username) {
|
||||
$code .= DOKU_TAB . DOKU_TAB . 'document._botmon.user = "' . $username . '";'. NL;
|
||||
}
|
||||
@@ -74,7 +74,6 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
|
||||
$code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "e.src='".DOKU_BASE."lib/plugins/botmon/client.js';" . NL;
|
||||
$code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(e);" . NL;
|
||||
$code .= DOKU_TAB . DOKU_TAB . "});";
|
||||
|
||||
$event->data['script'][] = ['_data' => $code];
|
||||
}
|
||||
|
||||
@@ -91,7 +90,6 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
|
||||
$event->data['script'][] = ['src' => DOKU_BASE.'lib/plugins/botmon/admin.js', 'defer' => 'defer', '_data' => ''];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Writes data to the server log.
|
||||
*
|
||||
@@ -191,39 +189,216 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
|
||||
|
||||
public function showCaptcha(Event $event) {
|
||||
|
||||
if ($this->getConf('useCaptcha') && $this->checkCaptchaCookie()) {
|
||||
$useCaptcha = $this->getConf('useCaptcha');
|
||||
|
||||
if ($useCaptcha !== 'disabled' && $this->checkCaptchaCookie()) {
|
||||
echo '<h1 class="sectionedit1">'; tpl_pagetitle(); echo "</h1>\n"; // always show the original page title
|
||||
$event->preventDefault(); // don't show normal content
|
||||
$this->insertDadaFiller(); // show dada filler instead!
|
||||
switch ($useCaptcha) {
|
||||
case 'blank':
|
||||
$this->insertBlankBox(); // show dada filler instead of text
|
||||
break;
|
||||
case 'dada':
|
||||
$this->insertDadaFiller(); // show dada filler instead of text
|
||||
break;
|
||||
}
|
||||
$this->insertCaptchaLoader(); // and load the captcha
|
||||
|
||||
} else {
|
||||
echo '<p>Normal page.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
private function checkCaptchaCookie() {
|
||||
|
||||
$cookieVal = isset($_COOKIE['_c_']) ? $_COOKIE['_c_'] : '';
|
||||
$seed = $this->getConf('captchaSeed');
|
||||
$cookieVal = isset($_COOKIE['captcha']) ? $_COOKIE['captcha'] : null;
|
||||
|
||||
return ($cookieVal == $seed ? 0 : 1); // #TODO: encrypt with other data
|
||||
$today = new DateTime();
|
||||
$isodate = substr((new DateTime())->format('c'), 0, 10);
|
||||
|
||||
$raw = $this->getConf('captchaSeed') . '|' . $_SERVER['SERVER_NAME'] . '|' . $_SERVER['REMOTE_ADDR'] . '|' . $isodate;
|
||||
|
||||
return $cookieVal !== hash('sha256', $raw);
|
||||
}
|
||||
|
||||
private function insertCaptchaLoader() {
|
||||
echo '<script>' . NL;
|
||||
|
||||
// add the deferred script loader::
|
||||
echo DOKU_TAB . "addEventListener('DOMContentLoaded', function(){" . NL;
|
||||
echo DOKU_TAB . DOKU_TAB . "const cj=document.createElement('script');" . NL;
|
||||
echo DOKU_TAB . DOKU_TAB . "cj.async=true;cj.defer=true;cj.type='text/javascript';" . NL;
|
||||
echo DOKU_TAB . DOKU_TAB . "cj.src='".DOKU_BASE."lib/plugins/botmon/captcha.js';" . NL;
|
||||
echo DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(cj);" . NL;
|
||||
echo DOKU_TAB . "});";
|
||||
echo '</script>' . NL;
|
||||
|
||||
}
|
||||
|
||||
// inserts a blank box to ensure there is enough space for the captcha:
|
||||
private function insertBlankBox() {
|
||||
|
||||
echo '<p style="min-height: 100px;"> </p>';
|
||||
}
|
||||
|
||||
/* Generates a few paragraphs of Dada text to show instead of the article content */
|
||||
private function insertDadaFiller() {
|
||||
// #TODO: make a dada filler
|
||||
|
||||
echo '<h1>'; tpl_pagetitle(); echo "</h1>\n";
|
||||
global $conf;
|
||||
global $TOC;
|
||||
global $ID;
|
||||
|
||||
echo '<script> alert("Hello world!"); </script>';
|
||||
// list of languages to search for the wordlist
|
||||
$langs = array_unique([$conf['lang'], 'la']);
|
||||
|
||||
echo "<p>Placeholder text while the captcha is being displayed.</p>\n";
|
||||
// find path to the first available wordlist:
|
||||
foreach ($langs as $lang) {
|
||||
$filename = __DIR__ .'/lang/' . $lang . '/wordlist.txt'; /* language-specific wordlist */
|
||||
if (file_exists($filename)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// load the wordlist file:
|
||||
if (file_exists($filename)) {
|
||||
$words = array();
|
||||
$totalWeight = 0;
|
||||
$lines = file($filename, FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$arr = explode("\t", $line);
|
||||
$arr[1] = ( count($arr) > 1 ? (int) trim($arr[1]) : 1 );
|
||||
$totalWeight += (int) $arr[1];
|
||||
array_push($words, $arr);
|
||||
}
|
||||
} else {
|
||||
echo '<script> console.log("Can’t generate filler text: wordlist file not found!"); </script>';
|
||||
return;
|
||||
}
|
||||
|
||||
// If a TOC exists, use it for the headlines:
|
||||
if(is_array($TOC)) {
|
||||
$toc = $TOC;
|
||||
} else {
|
||||
$meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE);
|
||||
//$tocok = (isset($meta['internal']['toc']) ? $meta['internal']['toc'] : $tocok = true);
|
||||
$toc = isset($meta['description']['tableofcontents']) ? $meta['description']['tableofcontents'] : null;
|
||||
}
|
||||
if (!$toc) { // no TOC, generate my own:
|
||||
$hlCount = mt_rand(0, (int) $conf['tocminheads']);
|
||||
$toc = array();
|
||||
for ($i=0; $i<$hlCount; $i++) {
|
||||
array_push($toc, $this->dadaMakeHeadline($words, $totalWeight)); // $toc
|
||||
}
|
||||
}
|
||||
|
||||
// if H1 heading is not in the TOC, add a chappeau section:
|
||||
$chapeauCount = mt_rand(1, 3);
|
||||
if ((int) $conf['toptoclevel'] > 1) {
|
||||
echo "<div class=\"level1\">\n";
|
||||
for ($i=0; $i<$chapeauCount; $i++) {
|
||||
echo $this->dadaMakeParagraph($words, $totalWeight);
|
||||
}
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
// text sections for each sub-headline:
|
||||
foreach ($toc as $hl) {
|
||||
echo $this->dadaMakeSection($words, $totalWeight, $hl);
|
||||
}
|
||||
}
|
||||
|
||||
private function dadaMakeSection($words, $totalWeight, $hl) {
|
||||
|
||||
global $conf;
|
||||
|
||||
// how many paragraphs?
|
||||
$paragraphCount = mt_rand(1, 4);
|
||||
|
||||
// section level
|
||||
$topTocLevel = (int) $conf['toptoclevel'];
|
||||
$secLevel = $hl['level'] + 1;;
|
||||
|
||||
// return value:
|
||||
$sec = "";
|
||||
|
||||
// make a headline:
|
||||
if ($topTocLevel > 1 || $secLevel > 1) {
|
||||
$sec .= "<h{$secLevel} id=\"{$hl['hid']}\">{$hl['title']}</h{$secLevel}>\n";
|
||||
}
|
||||
|
||||
// add the paragraphs:
|
||||
$sec .= "<div class=\"level{$secLevel}\">\n";
|
||||
for ($i=0; $i<$paragraphCount; $i++) {
|
||||
$sec .= $this->dadaMakeParagraph($words, $totalWeight);
|
||||
}
|
||||
$sec .= "</div>\n";
|
||||
|
||||
return $sec;
|
||||
}
|
||||
|
||||
private function dadaMakeHeadline($words, $totalWeight) {
|
||||
|
||||
// how many words to generate?
|
||||
$wordCount = mt_rand(2, 5);
|
||||
|
||||
// function returns an array:
|
||||
$r = Array();
|
||||
|
||||
// generate the headline:
|
||||
$hlArr = array();
|
||||
for ($i=0; $i<$wordCount; $i++) {
|
||||
array_push($hlArr, $this->dadaSelectRandomWord($words, $totalWeight));
|
||||
}
|
||||
|
||||
$r['title'] = ucfirst(implode(' ', $hlArr));
|
||||
|
||||
$r['hid'] = preg_replace('/[^\w\d\-]+/i', '_', strtolower($r['title']));
|
||||
$r['type'] = 'ul'; // always ul!
|
||||
$r['level'] = 1; // always level 1 for now
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
private function dadaMakeParagraph($words, $totalWeight) {
|
||||
|
||||
// how many words to generate?
|
||||
$sentenceCount = mt_rand(2, 5);
|
||||
|
||||
$paragraph = array();
|
||||
for ($i=0; $i<$sentenceCount; $i++) {
|
||||
array_push($paragraph, $this->dadaMakeSentence($words, $totalWeight));
|
||||
}
|
||||
|
||||
return "<p>\n" . implode(' ', $paragraph) . "\n</p>\n";
|
||||
|
||||
}
|
||||
|
||||
private function dadaMakeSentence($words, $totalWeight) {
|
||||
|
||||
// how many words to generate?
|
||||
$wordCount = mt_rand(4, 20);
|
||||
|
||||
// generate the sentence:
|
||||
$sentence = array();
|
||||
for ($i=0; $i<$wordCount; $i++) {
|
||||
array_push($sentence, $this->dadaSelectRandomWord($words, $totalWeight));
|
||||
}
|
||||
|
||||
return ucfirst(implode(' ', $sentence)) . '.';
|
||||
|
||||
}
|
||||
|
||||
private function dadaSelectRandomWord($list, $totalWeight) {
|
||||
|
||||
// get a random selection:
|
||||
$rand = mt_rand(0, $totalWeight);
|
||||
|
||||
// match the selection to the weighted list:
|
||||
$cumulativeWeight = 0;
|
||||
for ($i=0; $i<count($list); $i++) {
|
||||
$cumulativeWeight += $list[$i][1];
|
||||
if ($cumulativeWeight >= $rand) {
|
||||
return $list[$i][0];
|
||||
}
|
||||
}
|
||||
return '***';
|
||||
}
|
||||
|
||||
}
|
||||
166
captcha.js
Normal file
166
captcha.js
Normal file
@@ -0,0 +1,166 @@
|
||||
"use strict";
|
||||
/* DokuWiki BotMon Captcha JavaScript */
|
||||
/* 22.10.2025 - 0.1.0 - pre-release */
|
||||
/* Author: Sascha Leib <ad@hominem.info> */
|
||||
|
||||
const $BMCaptcha = {
|
||||
|
||||
init: function() {
|
||||
/* mark the page to contain the captcha styles */
|
||||
document.getElementsByTagName('body')[0].classList.add('botmon_captcha');
|
||||
|
||||
$BMCaptcha.install()
|
||||
},
|
||||
|
||||
install: function() {
|
||||
// find the parent element:
|
||||
let bm_parent = document.getElementsByTagName('body')[0];
|
||||
|
||||
// create the dialog:
|
||||
const dlg = document.createElement('dialog');
|
||||
dlg.setAttribute('closedby', 'none');
|
||||
dlg.setAttribute('open', 'open');
|
||||
dlg.id = 'botmon_captcha_box';
|
||||
dlg.innerHTML = '<h2>Captcha box</h2><p>Checking if you are a human …</p><p></p>';
|
||||
|
||||
// Checkbox:
|
||||
const lbl = document.createElement('label');
|
||||
const cb = document.createElement('input');
|
||||
cb.setAttribute('type', 'checkbox');
|
||||
cb.addEventListener('click', $BMCaptcha._cbCallback);
|
||||
lbl.appendChild(cb);
|
||||
lbl.appendChild(document.createTextNode('I am a human.'));
|
||||
|
||||
dlg.appendChild(lbl);
|
||||
|
||||
bm_parent.appendChild(dlg);
|
||||
},
|
||||
|
||||
/* creates a digest hash for the cookie function */
|
||||
digest: {
|
||||
|
||||
/* simple SHA hash function - adapted from https://geraintluff.github.io/sha256/ */
|
||||
hash: function(ascii) {
|
||||
|
||||
// shortcut:
|
||||
const sha256 = $BMCaptcha.digest.hash;
|
||||
|
||||
// helper function
|
||||
const rightRotate = function(v, a) {
|
||||
return (v>>>a) | (v<<(32 - a));
|
||||
};
|
||||
|
||||
var mathPow = Math.pow;
|
||||
var maxWord = mathPow(2, 32);
|
||||
var lengthProperty = 'length'
|
||||
var i, j;
|
||||
var result = ''
|
||||
|
||||
var words = [];
|
||||
var asciiBitLength = ascii[lengthProperty]*8;
|
||||
|
||||
//* caching results is optional - remove/add slash from front of this line to toggle
|
||||
// Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes
|
||||
// (we actually calculate the first 64, but extra values are just ignored)
|
||||
var hash = sha256.h = sha256.h || [];
|
||||
// Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes
|
||||
var k = sha256.k = sha256.k || [];
|
||||
var primeCounter = k[lengthProperty];
|
||||
/*/
|
||||
var hash = [], k = [];
|
||||
var primeCounter = 0;
|
||||
//*/
|
||||
|
||||
var isComposite = {};
|
||||
for (var candidate = 2; primeCounter < 64; candidate++) {
|
||||
if (!isComposite[candidate]) {
|
||||
for (i = 0; i < 313; i += candidate) {
|
||||
isComposite[i] = candidate;
|
||||
}
|
||||
hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0;
|
||||
k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0;
|
||||
}
|
||||
}
|
||||
|
||||
ascii += '\x80' // Append Ƈ' bit (plus zero padding)
|
||||
while (ascii[lengthProperty]%64 - 56) ascii += '\x00' // More zero padding
|
||||
for (i = 0; i < ascii[lengthProperty]; i++) {
|
||||
j = ascii.charCodeAt(i);
|
||||
if (j>>8) return; // ASCII check: only accept characters in range 0-255
|
||||
words[i>>2] |= j << ((3 - i)%4)*8;
|
||||
}
|
||||
words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0);
|
||||
words[words[lengthProperty]] = (asciiBitLength)
|
||||
|
||||
// process each chunk
|
||||
for (j = 0; j < words[lengthProperty];) {
|
||||
var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration
|
||||
var oldHash = hash;
|
||||
// This is now the undefinedworking hash", often labelled as variables a...g
|
||||
// (we have to truncate as well, otherwise extra entries at the end accumulate
|
||||
hash = hash.slice(0, 8);
|
||||
|
||||
for (i = 0; i < 64; i++) {
|
||||
var i2 = i + j;
|
||||
// Expand the message into 64 words
|
||||
// Used below if
|
||||
var w15 = w[i - 15], w2 = w[i - 2];
|
||||
|
||||
// Iterate
|
||||
var a = hash[0], e = hash[4];
|
||||
var temp1 = hash[7]
|
||||
+ (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1
|
||||
+ ((e&hash[5])^((~e)&hash[6])) // ch
|
||||
+ k[i]
|
||||
// Expand the message schedule if needed
|
||||
+ (w[i] = (i < 16) ? w[i] : (
|
||||
w[i - 16]
|
||||
+ (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) // s0
|
||||
+ w[i - 7]
|
||||
+ (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)) // s1
|
||||
)|0
|
||||
);
|
||||
// This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble
|
||||
var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0
|
||||
+ ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2])); // maj
|
||||
|
||||
hash = [(temp1 + temp2)|0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice()
|
||||
hash[4] = (hash[4] + temp1)|0;
|
||||
}
|
||||
|
||||
for (i = 0; i < 8; i++) {
|
||||
hash[i] = (hash[i] + oldHash[i])|0;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < 8; i++) {
|
||||
for (j = 3; j + 1; j--) {
|
||||
var b = (hash[i]>>(j*8))&255;
|
||||
result += ((b < 16) ? 0 : '') + b.toString(16);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
|
||||
_cbCallback: function(e) {
|
||||
if (e.target.checked) {
|
||||
//document.getElementById('botmon_captcha_box').close();
|
||||
|
||||
// make a hash for the cookie:
|
||||
const seed = document._botmon.seed || '';
|
||||
const extIp = document._botmon.ip || '0.0.0.0';
|
||||
const d = new Date(document._botmon.t0);
|
||||
const raw = seed + '|' + location.hostname + '|' + extIp + '|' + d.toISOString().substring(0, 10);
|
||||
|
||||
const hash = $BMCaptcha.digest.hash(raw);
|
||||
console.log('Setting cookie to:', raw, ' --> ', hash);
|
||||
document.cookie = "captcha=" + hash + ';';
|
||||
|
||||
window.location.reload(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// initialise the captcha module:
|
||||
$BMCaptcha.init();
|
||||
@@ -8,5 +8,7 @@
|
||||
$meta['geoiplib'] = array('multichoice',
|
||||
'_choices' => array ('disabled', 'phpgeoip'));
|
||||
|
||||
$meta['useCaptcha'] = array('onoff');
|
||||
$meta['captchaSeed'] = array('string', '_pattern' => '/[\da-fA-F]{16,32}/');
|
||||
//$meta['useCaptcha'] = array('onoff');
|
||||
$meta['useCaptcha'] = array('multichoice',
|
||||
'_choices' => array ('disabled', 'blank', 'dada'));
|
||||
$meta['captchaSeed'] = array('string');
|
||||
|
||||
4733
lang/de/wordlist.txt
4733
lang/de/wordlist.txt
File diff suppressed because it is too large
Load Diff
5052
lang/en/wordlist.txt
5052
lang/en/wordlist.txt
File diff suppressed because it is too large
Load Diff
5305
lang/fr/wordlist.txt
5305
lang/fr/wordlist.txt
File diff suppressed because it is too large
Load Diff
1577
lang/la/wordlist.txt
Normal file
1577
lang/la/wordlist.txt
Normal file
File diff suppressed because it is too large
Load Diff
38
style.less
38
style.less
@@ -1 +1,37 @@
|
||||
/* This file is no longer in use */
|
||||
body.botmon_captcha {
|
||||
main {
|
||||
h1 {
|
||||
color: transparent !important;
|
||||
text-shadow: 0 0 .25em rgba(0,.0,0,.8);
|
||||
}
|
||||
p, h2, h3, h4, h5, h6 {
|
||||
color: transparent !important;
|
||||
text-shadow: 0 0 .35em rgba(0,0,0,.5);
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
#botmon_captcha_box {
|
||||
border: red dotted 2pt;
|
||||
position: fixed;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: ~"calc(50vh - 200px)";
|
||||
left: ~"calc(50vw - 200px)";
|
||||
border-radius: .5rem;
|
||||
margin: 0; padding: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* dark mode overrides */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.darkmode.botmon_captcha {
|
||||
main {
|
||||
h1 {
|
||||
text-shadow: 0 0 .25em rgba(170,170,170,.75);
|
||||
}
|
||||
p, h2, h3, h4, h5, h6 {
|
||||
text-shadow: 0 0 .35em rgba(170,170,170,.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user