1/** 2 * Copyright (c) 2025 BAU Software s.r.o., Czechia. All rights reserved. 3 * 4 * This file is part of the Software licensed under the 5 * END-USER LICENSE AGREEMENT (EULA) 6 * 7 * License Summary: 8 * - Source is available for review, testing, debugging, and evaluation. 9 * - Distribution of the Software or source code is prohibited. 10 * - Modifications are allowed only for internal testing/debugging, 11 * not for production or deployment. 12 * 13 * The full license text can be found in the LICENSE file 14 * distributed with this source code. 15 * 16 * Unauthorized distribution, modification, or production use of 17 * this Software is strictly prohibited. 18 */ 19 20(() => { 21 const debug = 22 /^localhost|.*\.localhost|.*\.test$/.test(location.hostname) || 23 /:\d+$/.test(location.host) || 24 localStorage.getItem("altcha_debug") === "1"; 25 const defaultActionName = "action"; 26 const config = pluginData?.altcha || {}; 27 const actions = config.actions || []; 28 const paths = config.paths || []; 29 const cookiePath = config.cookiePath || "/"; 30 const sitePath = config.sitePath || "/"; 31 const protectLogin = !!config.protectLogin; 32 const protectRegistration = !!config.protectRegistration; 33 const protectPasswordReset = !!config.protectPasswordReset; 34 const protectComments = !!config.protectComments; 35 const plugins = { 36 coblocks: { 37 actions: ["coblocks-form-submit"],
38 formIdField: "form-submit", 39 }, 40 "contact-form-7": { 41 formIdField: "_wpcf7", 42 paths: ["/contact-form-7/", "#wpcf7-"], 43 }, 44 divi: { 45 actions: ["et_pb_contact_form_submit"], 46 formIdField: "et_pb_contact_form_id" 47 }, 48 "elementor-pro": { 49 actions: ["elementor_pro_forms_send_form"], 50 formIdField: "form_id" 51 }, 52 eventprime: { 53 actions: ["ep_save_event_booking"], 54 }, 55 "everest-forms": { 56 fields: ["everest_forms[id]"], 57 formIdField: "everest_forms[id]" 58 }, 59 formidable: { 60 fields: ["frm_action"], 61 formIdField: "form_id", 62 }, 63 forminator: { 64 fields: ["forminator_nonce"], 65 formIdField: "form_id", 66 }, 67 fluentform: { 68 actions: ["fluentform_submit"], 69 fields: ["__fluent_form_embded_post_id"], 70 formIdField: "form_id", 71 }, 72 gravityforms: { 73 fields: ["gform_submission_method"], 74 formIdField: "gform_submit", 75 }, 76 "kali-forms": { 77 actions: ["kaliforms_form_process"], 78 formIdField: "data[formId]", 79 }, 80 "html-forms": { 81 fields: ["_hf_form_id"], 82 formIdField: "_hf_form_id", 83 }, 84 mailpoet: { 85 actions: ["mailpoet"], 86 formIdField: "data[form_id]" 87 }, 88 metform: { 89 paths: ["/metform/"], 90 formIdField: (request) => request?.url?.pathname.match(/(\d+)$/)?.[1], 91 }, 92 newsletter: { 93 actions: ["tnp"], 94 }, 95 noptin: { 96 paths: ["/noptin/"], 97 formIdField: "noptin_form_id", 98 }, 99 ninjaforms: { 100 actions: ["nf_ajax_submit"], 101 formIdField: (request) => 102 request?.body?.formData.match(/"id":"(\d+)"/)?.[1], 103 }, 104 sureforms: { 105 paths: ["/sureforms/v1/submit-form"], 106 formIdField: "form-id", 107 }, 108 "user-registration": { 109 fields: [ 110 "user-registration-login-nonce", 111 "ur_reset_password", 112 "ur_frontend_form_nonce" 113 ], 114 }, 115 woocommerce: { 116 fields: [ 117 "wc_reset_password", 118 "woocommerce-login-nonce" 119 ], 120 paths: ["/wc/store/v1/", "/wc-api/"], 121 }, 122 "wp-members": { 123 fields: ["_wpmem_login_nonce", "_wpmem_register_nonce"], 124 }, 125 wpdiscuz: { 126 fields: ["wpdiscuz_unique_id"], 127 }, 128 wpforms: { 129 actions: ["wpforms_submit"], 130 fields: ["wpforms[id]"], 131 formIdField: "wpforms[id]", 132 }, 133 "wp": { 134 match: (request) => { 135 if (request.url.pathname.endsWith("/wp-login.php")) { 136 const action = matchActions(request, [ 137 "lostpassword", 138 "register", 139 ]); 140 return action || "login"; 141 } else if (request.url.pathname.endsWith("/wp-register.php")) { 142 return "register"; 143 } else if (request.url.pathname.endsWith("/wp-comments-post.php")) { 144 return "comments"; 145 } 146 }, 147 }, 148 }; 149 let interceptor; 150 let refreshUnderAttackPromise = null; 151 152 function createRegExp(pattern) { 153 return new RegExp( 154 "^" + 155 pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + 156 "$", 157 "i" 158 ); 159 } 160 161 function detectPlugin(request, action) { 162 for (const plugin in plugins) { 163 const opts = plugins[plugin]; 164 if (opts.match) { 165 const pluginAction = opts.match(request, action); 166 if (pluginAction) { 167 return { 168 pluginAction, 169 plugin, 170 }; 171 } 172 } 173 if (opts.fields && opts.fields.some((field) => request.body?.[field] !== undefined)) { 174 return { 175 pluginAction: null, 176 plugin, 177 }; 178 } 179 if (opts.actions?.length) { 180 const pluginAction = matchActions(request, opts.actions); 181 if (pluginAction) { 182 return { 183 pluginAction, 184 plugin, 185 }; 186 } 187 } 188 if ( 189 opts.paths?.length && 190 opts.paths.some((path) => request.url.href.includes(path)) 191 ) { 192 return { 193 pluginAction: null, 194 plugin, 195 }; 196 } 197 } 198 return { 199 pluginAction: null, 200 plugin: null, 201 }; 202 } 203 204 function getTimeZone() { 205 try { 206 return Intl.DateTimeFormat().resolvedOptions().timeZone; 207 } catch { 208 // noop 209 } 210 } 211 212 function getCookie(name) { 213 const nameEQ = name + "="; 214 const cookies = document.cookie.split(";"); 215 for (let i = 0; i < cookies.length; i++) { 216 let c = cookies[i]; 217 while (c.charAt(0) === " ") { 218 c = c.substring(1, c.length); 219 } 220 if (c.indexOf(nameEQ) === 0) { 221 return decodeURIComponent(c.substring(nameEQ.length, c.length)); 222 } 223 } 224 return null; 225 } 226 227 function setCookie(name, value, props = "") { 228 document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent( 229 value 230 )}; SameSite=strict; ${props}`.trim(); 231 } 232 233 function matchActions(request, patterns) { 234 let matched = false;
235 let rx = null; 236 for (let pattern of patterns) { 237 const isNegation = pattern.startsWith("!"); 238 const cleanPattern = isNegation ? pattern.slice(1) : pattern; 239 let name = defaultActionName; 240 let value = null; 241 if (cleanPattern.includes("=")) { 242 const parts = cleanPattern.split("="); 243 name = parts[0]; 244 rx = createRegExp(parts[1]); 245 } else if (cleanPattern === "*") { 246 matched = String( 247 request.body?.[name] || request.url.searchParams.get(name) || "" 248 ); 249 continue; 250 } else { 251 rx = createRegExp(cleanPattern); 252 } 253 value = String( 254 request.body?.[name] || request.url.searchParams.get(name) || "" 255 ); 256 if (rx) { 257 if ((name === defaultActionName || !!value) && rx.test(value)) { 258 if (isNegation) { 259 return false; 260 } 261 if (cleanPattern.includes('=')) { 262 return `${name}=`; 263 } 264 return name === defaultActionName ? value : `${name}=`; 265 } 266 } 267 } 268 return matched; 269 } 270 271 function matchPattern(str, patterns) { 272 let matched = false; 273 for (let pattern of patterns) { 274 const isNegation = pattern.startsWith("!"); 275 const cleanPattern = isNegation ? pattern.slice(1) : pattern; 276 if (cleanPattern === "*") { 277 matched = true; 278 } else if (createRegExp(cleanPattern).test(str)) { 279 if (isNegation) { 280 return false; 281 } 282 return true; 283 } 284 } 285 return matched; 286 } 287 288 function normalizePath(path) { 289 if (sitePath !== "/" && path.startsWith(sitePath)) { 290 path = "/" + path.slice(sitePath.length).replace(/^\/+/, ""); 291 } 292 return path.replace(/^\/index.php\//, "/"); 293 } 294 295 async function refreshUnderAttackCookie() { 296 const underAttackCookie = getCookie("altcha_under_attack"); 297 const expires = parseInt( 298 getCookie("altcha_under_attack_expires") || "0", 299 10 300 ); 301 const ttl = parseInt(getCookie("altcha_under_attack_ttl") || "0", 10); 302 if ( 303 !refreshUnderAttackPromise && 304 (!underAttackCookie || expires < Date.now() + 15_000) 305 ) { 306 refreshUnderAttackPromise = interceptor.requestAltchaVerification( 307 { 308 "params.under_attack": "1", 309 }, 310 config.underAttackChallengeUrl || config.widget.challenge 311 );
312 try { 313 const payload = await refreshUnderAttackPromise; 314 if (payload) { 315 setCookie( 316 "altcha_under_attack", 317 payload, 318 "path=" + 319 cookiePath + 320 "; expires=" + 321 new Date(Date.now() + (ttl || 30_000)).toUTCString() 322 ); 323 } 324 } finally { 325 refreshUnderAttackPromise = null; 326 } 327 } 328 } 329 330 interceptor = altchaInterceptor({ 331 filter: async (request) => { 332 const isLogin = request.method === "POST" && !!request.body?.log && !!request.body?.pwd; 333 const isRegistration = request.method === "POST" && (request.url.pathname.endsWith("/wp-register.php") || request.url.searchParams.get("action") === "register"); 334 const isPasswordReset = request.method === "POST" && request.url.searchParams.get("action") === "lostpassword"; 335 const isComment = request.method === "POST" && !!request.url.pathname.endsWith("/wp-comments-post.php"); 336 if (request.url.origin !== location.origin) { 337 return false; 338 } 339 if (getCookie("altcha")) { 340 return false; 341 } 342 if (request.body?.altcha) { 343 return false; 344 } 345 if ( 346 request.method !== "POST" && 347 !request.url.pathname.endsWith("admin-ajax.php") 348 ) { 349 return false; 350 } 351 if (isLogin && !protectLogin) { 352 return false; 353 } else if (isRegistration && !protectRegistration) { 354 return false; 355 } else if (isPasswordReset && !protectPasswordReset) { 356 return false; 357 } else if (isComment && !protectComments) { 358 return false; 359 } 360 const pathMatched = matchPattern(normalizePath(request.url.pathname), paths); 361 const action = matchActions(request, actions); 362 if (!isLogin && !isRegistration && !isPasswordReset && !isComment && action === false && !pathMatched) { 363 return false; 364 } 365 const { plugin, pluginAction } = detectPlugin(request, action); 366 if (config.underAttack) { 367 await refreshUnderAttackCookie(); 368 } 369 const formIdField = plugins[plugin]?.formIdField; 370 return { 371 action: typeof action === "string" ? action : (pluginAction || null), 372 form_id: 373 (formIdField && typeof formIdField === "function" 374 ? formIdField(request) 375 : request.body?.[formIdField]) || null, 376 interceptor: "1", 377 plugin, 378 tz: getTimeZone(), 379 }; 380 }, 381 onVerified: (payload) => { 382 setCookie("altcha", payload, "path=" + cookiePath); 383 }, 384 debug, 385 ...config, 386 widget: { 387 debug, 388 ...config.widget, 389 }, 390 }); 391 392 if (config.widget.powAlgorithm === "ARGON2ID") { 393 try { 394 $altcha.algorithms.set("ARGON2ID", () => new Worker(config.workerArgon2idPath)); 395 } catch (err) { 396 console.error(err); 397 console.error('ALTCHA failed to register ARGON2ID worker.'); 398 } 399 } 400 401 if (!window.isSecureContext) { 402 console.warn( 403 "Secure Context (HTTPS) Required! ALTCHA requires a secure connection (HTTPS), also known as a Secure Context [https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts]. Please access this page over HTTPS or use whitelisted localhost domains for development." 404 ); 405 } 406})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.