1"use strict"; 2/* DokuWiki BotMon Captcha JavaScript */ 3/* 23.10.2025 - 0.1.2 - pre-release */ 4/* Author: Sascha Leib <ad@hominem.info> */ 5 6const $BMCaptcha = { 7 8 init: function() { 9 10 // hide the NoJS warning: 11 document.getElementById('BM__NoJSWarning').close(); 12 13 // install the captcha: 14 document.getElementsByTagName('body')[0].classList.add('botmon_captcha'); 15 $BMCaptcha._cbDly = 1.5; 16 $BMCaptcha.install() 17 }, 18 19 install: function() { 20 21 // localisation helper function: 22 let _loc = function(id, alt) { 23 if ($BMLocales && $BMLocales[id]) return $BMLocales[id]; 24 return alt; 25 } 26 27 // find the parent element: 28 let bm_parent = document.getElementsByTagName('body')[0]; 29 30 // create the dialog: 31 const dlg = document.createElement('dialog'); 32 dlg.setAttribute('closedby', 'none'); 33 dlg.setAttribute('open', 'open'); 34 dlg.setAttribute('role', 'alertdialog'); 35 dlg.setAttribute('aria-labelledby', 'botmon_captcha_title'); 36 dlg.classList.add('checking'); 37 dlg.id = 'botmon_captcha_box'; 38 dlg.innerHTML = '<h2 id="botmon_captcha_title">' + _loc('dlgTitle', 'Title') + '</h2><p>' + _loc('dlgSubtitle', 'Subtitle') + '</p>'; 39 40 // Checkbox: 41 const lbl = document.createElement('label'); 42 lbl.setAttribute('aria-live', 'assertive'); 43 lbl.innerHTML = '<span class="confirm">' + _loc('dlgConfirm', "Confirm.") + '</span>' + 44 '<span class="busy"></span><span class="checking">' + _loc('dlgChecking', "Checking") + '</span>' + 45 '<span class="loading">' + _loc('dlgLoading', "Loading") + '</span>' + 46 '<span class="erricon">�</span><span class="error">' + _loc('dlgError', "Error") + '</span>'; 47 const cb = document.createElement('input'); 48 cb.setAttribute('type', 'checkbox'); 49 cb.setAttribute('disabled', 'disabled'); 50 cb.addEventListener('click', $BMCaptcha._cbCallback); 51 lbl.prepend(cb); 52 53 dlg.appendChild(lbl); 54 55 bm_parent.appendChild(dlg); 56 57 // call the delayed callback in a couple of seconds: 58 $BMCaptcha._st = performance.now(); 59 setTimeout($BMCaptcha._delayedCallback, $BMCaptcha._cbDly * 1000); 60 }, 61 62 /* creates a digest hash */ 63 digest: { 64 65 /* simple SHA hash function - adapted from https://geraintluff.github.io/sha256/ */ 66 hash: function(ascii) { 67 68 // shortcut: 69 const sha256 = $BMCaptcha.digest.hash; 70 71 // helper function 72 const rightRotate = function(v, a) { 73 return (v>>>a) | (v<<(32 - a)); 74 }; 75 76 var mathPow = Math.pow; 77 var maxWord = mathPow(2, 32); 78 var lengthProperty = 'length' 79 var i, j; 80 var result = '' 81 82 var words = []; 83 var asciiBitLength = ascii[lengthProperty]*8; 84 85 //* caching results is optional - remove/add slash from front of this line to toggle 86 // Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes 87 // (we actually calculate the first 64, but extra values are just ignored) 88 var hash = sha256.h = sha256.h || []; 89 // Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes 90 var k = sha256.k = sha256.k || []; 91 var primeCounter = k[lengthProperty]; 92 /*/ 93 var hash = [], k = []; 94 var primeCounter = 0; 95 //*/ 96 97 var isComposite = {}; 98 for (var candidate = 2; primeCounter < 64; candidate++) { 99 if (!isComposite[candidate]) { 100 for (i = 0; i < 313; i += candidate) { 101 isComposite[i] = candidate; 102 } 103 hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0; 104 k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0; 105 } 106 } 107 108 ascii += '\x80' // Append Ƈ' bit (plus zero padding) 109 while (ascii[lengthProperty]%64 - 56) ascii += '\x00' // More zero padding 110 for (i = 0; i < ascii[lengthProperty]; i++) { 111 j = ascii.charCodeAt(i); 112 if (j>>8) return; // ASCII check: only accept characters in range 0-255 113 words[i>>2] |= j << ((3 - i)%4)*8; 114 } 115 words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0); 116 words[words[lengthProperty]] = (asciiBitLength) 117 118 // process each chunk 119 for (j = 0; j < words[lengthProperty];) { 120 var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration 121 var oldHash = hash; 122 // This is now the undefinedworking hash", often labelled as variables a...g 123 // (we have to truncate as well, otherwise extra entries at the end accumulate 124 hash = hash.slice(0, 8); 125 126 for (i = 0; i < 64; i++) { 127 var i2 = i + j; 128 // Expand the message into 64 words 129 // Used below if 130 var w15 = w[i - 15], w2 = w[i - 2]; 131 132 // Iterate 133 var a = hash[0], e = hash[4]; 134 var temp1 = hash[7] 135 + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1 136 + ((e&hash[5])^((~e)&hash[6])) // ch 137 + k[i] 138 // Expand the message schedule if needed 139 + (w[i] = (i < 16) ? w[i] : ( 140 w[i - 16] 141 + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) // s0 142 + w[i - 7] 143 + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)) // s1 144 )|0 145 ); 146 // This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble 147 var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0 148 + ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2])); // maj 149 150 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() 151 hash[4] = (hash[4] + temp1)|0; 152 } 153 154 for (i = 0; i < 8; i++) { 155 hash[i] = (hash[i] + oldHash[i])|0; 156 } 157 } 158 159 for (i = 0; i < 8; i++) { 160 for (j = 3; j + 1; j--) { 161 var b = (hash[i]>>(j*8))&255; 162 result += ((b < 16) ? 0 : '') + b.toString(16); 163 } 164 } 165 return result; 166 } 167 }, 168 169 _cbCallback: function(e) { 170 if (e.target.checked) { 171 //document.getElementById('botmon_captcha_box').close(); 172 173 try { 174 var $status = 'loading'; 175 176 // generate the hash: 177 const dat = [ // the data to encode 178 document._botmon.seed || '', 179 location.hostname, 180 document._botmon.ip || '0.0.0.0', 181 (new Date()).toISOString().substring(0, 10) 182 ]; 183 if (performance.now() - $BMCaptcha._st <= 1500) dat.push(performance.now() - $BMCaptcha._st); 184 185 // set the cookie: 186 document.cookie = "DWConfirm=" + encodeURIComponent($BMCaptcha.digest.hash(dat.join(';'))) + '; path=/; session;'; 187 // + (document.location.protocol === 'https:' ? ' secure;' : ''); 188 189 } catch (err) { 190 console.error(err); 191 $status = 'error'; 192 } 193 194 // change the interface: 195 const dlg = document.getElementById('botmon_captcha_box'); 196 if (dlg) { 197 dlg.classList.add( $status ); 198 dlg.classList.remove('ready'); 199 } 200 201 // reload the page: 202 if ($status !== 'error')window.location.reload(true); 203 } 204 }, 205 206 _delayedCallback: function() { 207 const dlg = document.getElementById('botmon_captcha_box'); 208 if (dlg) { 209 dlg.classList.add('ready'); 210 dlg.classList.remove('checking'); 211 212 const input = dlg.getElementsByTagName('input')[0]; 213 if (input) { 214 input.removeAttribute('disabled'); 215 input.focus(); 216 setTimeout($BMCaptcha._autoCheck, 200, input); 217 } 218 } 219 }, 220 _cbDly: null, 221 _st: null, 222 223 _autoCheck: function(e) { 224 225 const bypass = ($BMConfig['captchaBypass'] || '').split(','); 226 var action = false; 227 228 if (bypass.indexOf('langmatch') >= 0) { // Languages matching 229 const cntLangs = navigator.languages.map(lang => lang.split('-')[0]); 230 if (cntLangs.indexOf(document.documentElement.lang || 'en') >= 0) action = true; 231 } 232 233 if (action) e.click(); // action! 234 } 235} 236// initialise the captcha module: 237$BMCaptcha.init();