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 plugins = { 33 coblocks: { 34 actions: ["coblocks-form-submit"],
35 formIdField: "form-submit", 36 }, 37 "contact-form-7": { 38 formIdField: "_wpcf7", 39 paths: ["/contact-form-7/", "#wpcf7-"], 40 }, 41 "elementor-pro": { 42 actions: ["elementor_pro_forms_send_form"], 43 }, 44 eventprime: { 45 actions: ["ep_save_event_booking"], 46 }, 47 formidable: { 48 field: "frm_action", 49 formIdField: "form_id", 50 }, 51 forminator: { 52 field: "forminator_nonce", 53 formIdField: "form_id", 54 }, 55 fluentform: { 56 actions: ["fluentform_submit"], 57 field: "__fluent_form_embded_post_id", 58 formIdField: "form_id", 59 }, 60 gravityforms: { 61 field: "gform_submission_method", 62 formIdField: "gform_submit", 63 }, 64 "html-forms": { 65 field: "_hf_form_id", 66 formIdField: "_hf_form_id", 67 }, 68 metform: { 69 paths: ["/metform/"], 70 formIdField: (request) => request?.url?.pathname.match(/(\d+)$/)?.[1], 71 }, 72 ninjaforms: { 73 actions: ["nf_ajax_submit"], 74 formIdField: (request) => 75 request?.body?.formData.match(/"id":"(\d+)"/)?.[1], 76 }, 77 woocommerce: { 78 paths: ["/wc/store/v1/", "/wc-api/"], 79 }, 80 "wp-comments": { 81 paths: ["/wp-comments-post.php"], 82 }, 83 "wp-login": { 84 paths: ["/wp-login.php"], 85 }, 86 wpdiscuz: { 87 field: "wpdiscuz_unique_id", 88 }, 89 wpforms: { 90 actions: ["wpforms_submit"], 91 field: "wpforms[id]", 92 formIdField: "wpforms[id]", 93 }, 94 }; 95 let interceptor; 96 let refreshUnderAttackPromise = null; 97 98 function createRegExp(pattern) { 99 return new RegExp( 100 "^" + 101 pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + 102 "$", 103 "i" 104 ); 105 } 106 107 function detectPlugin(request, action) { 108 for (const plugin in plugins) { 109 const opts = plugins[plugin]; 110 if (opts.field && request.body?.[opts.field] !== undefined) { 111 return plugin; 112 } 113 if (opts.actions?.length && opts.actions.some((a) => a === action)) { 114 return plugin; 115 } 116 if ( 117 opts.paths?.length && 118 opts.paths.some((path) => request.url.href.includes(path)) 119 ) { 120 return plugin; 121 } 122 } 123 return null; 124 } 125 126 function getTimeZone() { 127 try { 128 return Intl.DateTimeFormat().resolvedOptions().timeZone; 129 } catch { 130 // noop 131 } 132 } 133 134 function getCookie(name) { 135 const nameEQ = name + "="; 136 const cookies = document.cookie.split(";"); 137 for (let i = 0; i < cookies.length; i++) { 138 let c = cookies[i]; 139 while (c.charAt(0) === " ") { 140 c = c.substring(1, c.length); 141 } 142 if (c.indexOf(nameEQ) === 0) { 143 return decodeURIComponent(c.substring(nameEQ.length, c.length)); 144 } 145 } 146 return null; 147 } 148 149 function setCookie(name, value, props = "") { 150 document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent( 151 value 152 )}; SameSite=strict; ${props}`.trim(); 153 } 154 155 function matchActions(request, patterns) { 156 let matched = false; 157 let rx = null; 158 for (let pattern of patterns) { 159 const isNegation = pattern.startsWith("!"); 160 const cleanPattern = isNegation ? pattern.slice(1) : pattern; 161 let name = defaultActionName; 162 let value = null; 163 if (cleanPattern.includes("=")) { 164 const parts = cleanPattern.split("="); 165 name = parts[0]; 166 rx = createRegExp(parts[1]); 167 } else if (cleanPattern === "*") { 168 matched = String( 169 request.body?.[name] || request.url.searchParams.get(name) || "" 170 ); 171 continue; 172 } else { 173 rx = createRegExp(cleanPattern); 174 } 175 value = String( 176 request.body?.[name] || request.url.searchParams.get(name) || "" 177 ); 178 if (rx) { 179 if ((name === defaultActionName || !!value) && rx.test(value)) { 180 if (isNegation) { 181 return false; 182 } 183 return name === defaultActionName ? value : `${name}=${value}`; 184 } 185 } 186 } 187 return matched; 188 } 189 190 function matchPattern(str, patterns) { 191 let matched = false; 192 for (let pattern of patterns) { 193 const isNegation = pattern.startsWith("!"); 194 const cleanPattern = isNegation ? pattern.slice(1) : pattern; 195 if (cleanPattern === "*") { 196 matched = true; 197 } else if (createRegExp(cleanPattern).test(str)) { 198 if (isNegation) { 199 return false;
200 } 201 return true; 202 } 203 } 204 return matched; 205 } 206 207 function normalizePath(path) { 208 if (sitePath !== "/" && path.startsWith(sitePath)) { 209 path = "/" + path.slice(sitePath.length).replace(/^\/+/, ""); 210 } 211 return path.replace(/^\/index.php\//, "/"); 212 } 213 214 async function refreshUnderAttackCookie() { 215 const underAttackCookie = getCookie("altcha_under_attack"); 216 const expires = parseInt( 217 getCookie("altcha_under_attack_expires") || "0", 218 10 219 ); 220 const ttl = parseInt(getCookie("altcha_under_attack_ttl") || "0", 10); 221 if ( 222 !refreshUnderAttackPromise && 223 (!underAttackCookie || expires < Date.now() + 15_000) 224 ) { 225 refreshUnderAttackPromise = interceptor.requestAltchaVerification( 226 { 227 "params.under_attack": "1", 228 }, 229 config.underAttackChallengeUrl || config.widget.challengeurl 230 ); 231 try { 232 const payload = await refreshUnderAttackPromise; 233 if (payload) { 234 setCookie( 235 "altcha_under_attack", 236 payload, 237 "path=" + 238 cookiePath + 239 "; expires=" + 240 new Date(Date.now() + (ttl || 30_000)).toUTCString() 241 ); 242 } 243 } finally { 244 refreshUnderAttackPromise = null; 245 } 246 } 247 } 248 249 interceptor = altchaInterceptor({ 250 filter: async (request) => { 251 const isLogin = request.method === "POST" && !!request.body?.log && !!request.body?.pwd; 252 if (request.url.origin !== location.origin) { 253 return false; 254 } 255 if (getCookie("altcha")) { 256 return false; 257 } 258 if (request.body?.altcha) { 259 return false; 260 } 261 if ( 262 request.method !== "POST" && 263 !request.url.pathname.endsWith("admin-ajax.php") 264 ) { 265 return false; 266 } 267 if (!matchPattern(normalizePath(request.url.pathname), paths) && (!protectLogin || !isLogin)) { 268 return false; 269 } 270 const action = matchActions(request, actions); 271 if (action === false) { 272 return false; 273 } 274 const plugin = detectPlugin(request, action); 275 if (config.underAttack) { 276 await refreshUnderAttackCookie(); 277 } 278 const formIdField = plugins[plugin]?.formIdField; 279 return { 280 action: typeof action === "string" ? action : null, 281 form_id: 282 (formIdField && typeof formIdField === "function" 283 ? formIdField(request) 284 : request.body?.[formIdField]) || null, 285 interceptor: "1", 286 plugin, 287 tz: getTimeZone(), 288 }; 289 }, 290 onVerified: (payload) => { 291 setCookie("altcha", payload, "path=" + cookiePath); 292 }, 293 debug, 294 ...config, 295 widget: { 296 debug, 297 ...config.widget, 298 }, 299 }); 300 301 if (!window.isSecureContext) { 302 console.warn( 303 "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." 304 ); 305 } 306})();
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.