1/** 2 * @file 3 * Global utilities. 4 */ 5 6/* ========================================================================== 7 * Turnstile configuration 8 * ========================================================================== */ 9var TURNSTILE_SITE_KEY = '0x4AAAAAAAFOCViB_9IxPWEY'; 10var TURNSTILE_SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; 11var turnstileScriptPromise = null; 12 13(function ($, Drupal) { 14 15 'use strict'; 16 17 /* ========================================================================== 18 * Generic helpers 19 * ======================================================================== */ 20 21 /** 22 * Provides a resolved promise regardless of browser support. 23 */ 24 function resolvedPromise(value) { 25 if (typeof window.Promise !== 'undefined') { 26 return window.Promise.resolve(value); 27 } 28 if ($.Deferred) { 29 var deferred = $.Deferred(); 30 deferred.resolve(value); 31 return deferred.promise(); 32 } 33 return null; 34 } 35 36 /** 37 * Fetches a CSRF token from Drupal. 38 */ 39 function requestCsrfToken() { 40 return axios.get('/session/token') 41 .then(function (response) { 42 return response.data; 43 }); 44 } 45 46 /** 47 * Sends the subscription payload to the REST endpoint. 48 */ 49 function postSubscription(form, payload, axiosConfig) { 50 var endpoint = !form.action ? 'api/subscribe' : form.action; 51 return axios.post(endpoint, payload, axiosConfig); 52 } 53 54 /* ========================================================================== 55 * Turnstile lifecycle helpers 56 * ======================================================================== */ 57 58 /** 59 * Builds initial Turnstile state and attaches DOM scaffolding. 60 */ 61 function createTurnstileState($form) { 62 var state = { 63 widgetId: null, 64 token: '', 65 wrapper: null, 66 container: null, 67 tokenInput: null, 68 mirrorInput: null, 69 }; 70 71 if (!TURNSTILE_SITE_KEY) { 72 return state; 73 } 74 75 state.wrapper = $('<div class="turnstile-wrapper" hidden></div>');
76 state.container = $('<div class="cf-turnstile js-turnstile-container"></div>'); 77 state.wrapper.append(state.container); 78 79 var $submitButton = $form.find('.form-submit').first(); 80 if ($submitButton.length) { 81 state.wrapper.insertBefore($submitButton); 82 } 83 else { 84 $form.append(state.wrapper); 85 } 86 87 state.tokenInput = $('<input type="hidden" name="cf-turnstile-response" value="">'); 88 state.mirrorInput = $('<input type="hidden" name="turnstile_token" value="">'); 89 $form.append(state.tokenInput, state.mirrorInput); 90 91 return state; 92 } 93 94 /** 95 * Resets the current Turnstile token and UI state. 96 */ 97 function resetTurnstile(state) { 98 state.token = ''; 99 if (state.tokenInput && state.tokenInput.length) { 100 state.tokenInput.val(''); 101 } 102 if (state.mirrorInput && state.mirrorInput.length) { 103 state.mirrorInput.val(''); 104 } 105 if (state.container && state.container.length) { 106 state.container.removeClass('error'); 107 } 108 } 109 110 /** 111 * Loads the Turnstile script once and shares the promise across forms. 112 */ 113 function loadTurnstileScript() { 114 if (!TURNSTILE_SITE_KEY) { 115 return resolvedPromise(); 116 } 117 if (window.turnstile) { 118 return resolvedPromise(); 119 } 120 if (turnstileScriptPromise) { 121 return turnstileScriptPromise; 122 } 123 124 if (typeof window.Promise === 'undefined' && !$.Deferred) { 125 console.error('Turnstile requires Promise or jQuery Deferred support in this browser.'); 126 return null; 127 } 128 129 var clearPromise = function () { 130 turnstileScriptPromise = null; 131 }; 132 133 if (typeof window.Promise === 'undefined') { 134 var deferred = $.Deferred(); 135 turnstileScriptPromise = deferred.promise(); 136 137 var existingScript = document.querySelector('script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]'); 138 var onLoad = function () { 139 clearPromise(); 140 deferred.resolve(); 141 }; 142 var onError = function (event) { 143 clearPromise(); 144 deferred.reject(event); 145 }; 146 147 if (existingScript) { 148 existingScript.addEventListener('load', onLoad, { once: true }); 149 existingScript.addEventListener('error', onError, { once: true }); 150 return turnstileScriptPromise; 151 } 152 153 var legacyScript = document.createElement('script'); 154 legacyScript.src = TURNSTILE_SCRIPT_URL; 155 legacyScript.async = true; 156 legacyScript.defer = true; 157 legacyScript.addEventListener('load', onLoad, { once: true }); 158 legacyScript.addEventListener('error', onError, { once: true }); 159 document.head.appendChild(legacyScript); 160 161 return turnstileScriptPromise; 162 } 163 164 turnstileScriptPromise = new window.Promise(function (resolve, reject) { 165 var existing = document.querySelector('script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]'); 166 167 var handleLoad = function () { 168 clearPromise(); 169 resolve(); 170 }; 171 var handleError = function (event) { 172 clearPromise(); 173 reject(event); 174 }; 175 176 if (existing) { 177 if (existing.getAttribute('data-loaded') === 'true' || existing.readyState === 'complete') { 178 clearPromise(); 179 resolve(); 180 return; 181 } 182 existing.addEventListener('load', handleLoad, { once: true }); 183 existing.addEventListener('error', handleError, { once: true }); 184 return; 185 } 186 187 var script = document.createElement('script'); 188 script.src = TURNSTILE_SCRIPT_URL; 189 script.async = true; 190 script.defer = true; 191 script.addEventListener('load', function () { 192 script.setAttribute('data-loaded', 'true'); 193 handleLoad(); 194 }, { once: true }); 195 script.addEventListener('error', handleError, { once: true }); 196 document.head.appendChild(script); 197 }); 198 199 return turnstileScriptPromise; 200 } 201 202 /** 203 * Renders the Turnstile widget if it has not already been rendered. 204 */ 205 function renderTurnstile(state) { 206 if (!TURNSTILE_SITE_KEY || !state.container || !state.container.length || !window.turnstile) { 207 return; 208 } 209 if (state.widgetId !== null) { 210 return; 211 } 212 try { 213 state.widgetId = window.turnstile.render(state.container[0], { 214 sitekey: TURNSTILE_SITE_KEY, 215 callback: function (token) { 216 state.token = token; 217 if (state.tokenInput && state.tokenInput.length) { 218 state.tokenInput.val(token); 219 } 220 if (state.mirrorInput && state.mirrorInput.length) { 221 state.mirrorInput.val(token); 222 } 223 if (state.container && state.container.length) { 224 state.container.removeClass('error'); 225 } 226 }, 227 'expired-callback': function () { 228 resetTurnstile(state); 229 }, 230 'error-callback': function () { 231 resetTurnstile(state); 232 if (state.container && state.container.length) { 233 state.container.addClass('error'); 234 } 235 } 236 }); 237 } 238 catch (err) { 239 console.error('Turnstile render failed', err); 240 } 241 } 242 243 /** 244 * Makes the Turnstile widget visible and ensures the script is ready. 245 */ 246 function showTurnstile(state) { 247 if (!TURNSTILE_SITE_KEY || !state.wrapper) { 248 return; 249 } 250 state.wrapper.removeAttr('hidden'); 251 var scriptLoader = loadTurnstileScript(); 252 if (scriptLoader && typeof scriptLoader.then === 'function') { 253 scriptLoader.then(function () { 254 renderTurnstile(state); 255 }).catch(function (error) { 256 console.error('Turnstile script failed to load', error); 257 }); 258 } 259 else { 260 renderTurnstile(state); 261 } 262 } 263 264 /* ========================================================================== 265 * Form validation helpers 266 * ======================================================================== */ 267 268 /** 269 * Evaluates a single element for the serialized payload. 270 */ 271 function evaluateField(element, pattern, context) { 272 var $element = $(element); 273 var value = (element.type === 'checkbox') ? element.checked : element.value; 274 var passes = false;
275 276 if (element.name === 'redirect') { 277 context.redirect = $element; 278 } 279 if (element.name === 'message') { 280 context.message = $element; 281 } 282 283 if (!element.hidden) { 284 if (!element.required) { 285 passes = true; 286 } 287 else if ($element.attr('data-pattern') && typeof pattern[$element.data('pattern')] !== 'undefined') { 288 if (element.type === 'checkbox') { 289 passes = pattern[$element.attr('data-pattern')] === value; 290 } 291 else if (pattern[$element.attr('data-pattern')].test(String(value).toLowerCase())) { 292 passes = true; 293 if (element.name === 'email') { 294 context.email = value; 295 } 296 } 297 } 298 else { 299 passes = value !== ''; 300 if (element.name === 'fname') { 301 context.fname = value; 302 } 303 if (element.name === 'lname') { 304 context.lname = value; 305 } 306 } 307 } 308 else { 309 passes = true; 310 } 311 312 context.values[element.name] = value; 313 314 if (passes) { 315 $element.removeClass('error'); 316 } 317 else { 318 $element.addClass('error'); 319 context.err = true; 320 } 321 } 322 323 /** 324 * Serializes and validates all fields for the outgoing payload. 325 */ 326 function validateAndSerializeFields($fields, pattern) { 327 var context = { 328 values: {}, 329 err: false, 330 redirect: null, 331 message: null, 332 email: '', 333 fname: '', 334 lname: '' 335 }; 336 337 $fields.each(function () { 338 evaluateField(this, pattern, context); 339 }); 340 341 return context; 342 } 343 344 /* ========================================================================== 345 * Behavior entrypoint helpers 346 * ======================================================================== */ 347 348 /** 349 * Hooks the Turnstile trigger to the first email field. 350 */ 351 function attachEmailTrigger($form, turnstileState) { 352 if (!TURNSTILE_SITE_KEY || !turnstileState.wrapper || !turnstileState.container) { 353 return; 354 } 355 var $emailField = $form.find('input[type="email"]').first(); 356 if ($emailField.length) { 357 $emailField.one('focus', function () { 358 showTurnstile(turnstileState); 359 }); 360 } 361 } 362 363 /** 364 * Builds the submit handler with Turnstile and REST integration. 365 */ 366 function createSubmitHandler($form, formState, pattern, $fields) { 367 return function () { 368 var callbacks = { 369 fok: $.noop, 370 ferr: $.noop 371 }; 372 if ($(this).data('ai_subscribe') && $.isFunction($(this).data('ai_subscribe').fok)) { 373 callbacks.fok = $(this).data('ai_subscribe').fok; 374 } 375 if ($(this).data('ai_subscribe') && $.isFunction($(this).data('ai_subscribe').ferr)) { 376 callbacks.ferr = $(this).data('ai_subscribe').ferr; 377 } 378 379 var validation = validateAndSerializeFields($fields, pattern); 380 var hasErrors = validation.err; 381 382 if (TURNSTILE_SITE_KEY && !formState.token) { 383 hasErrors = true; 384 if (formState.container && formState.container.length) { 385 formState.container.addClass('error'); 386 } 387 showTurnstile(formState); 388 } 389 390 var contactData = Object.assign({}, validation.values); 391 if (TURNSTILE_SITE_KEY) { 392 delete contactData['cf-turnstile-response']; 393 delete contactData['turnstile_token']; 394 } 395 396 var payload = { 397 contact: contactData 398 }; 399 400 var currentPageUri = ''; 401 if (typeof window !== 'undefined' && window.location) { 402 currentPageUri = (window.location.pathname || '') + 403 (window.location.search || '') + 404 (window.location.hash || ''); 405 } 406 if (payload.contact && typeof payload.contact === 'object') { 407 var existingPageUri = payload.contact.page_uri; 408 if (typeof payload.contact['page_uri[]'] !== 'undefined') { 409 existingPageUri = payload.contact['page_uri[]']; 410 } 411 var pageUriList = []; 412 if (Array.isArray(existingPageUri)) { 413 pageUriList = existingPageUri.filter(function (value) { 414 return value; 415 }); 416 } 417 else if (typeof existingPageUri === 'string' && existingPageUri.length) { 418 pageUriList = [existingPageUri]; 419 } 420 if (currentPageUri) { 421 if (pageUriList.indexOf(currentPageUri) === -1) { 422 pageUriList.push(currentPageUri); 423 } 424 } 425 if (pageUriList.length) { 426 payload.contact.page_uri = pageUriList; 427 if (typeof payload.contact['page_uri[]'] !== 'undefined') { 428 payload.contact['page_uri[]'] = pageUriList; 429 } 430 } 431 else if (currentPageUri) { 432 payload.contact.page_uri = [currentPageUri]; 433 if (typeof payload.contact['page_uri[]'] !== 'undefined') { 434 payload.contact['page_uri[]'] = [currentPageUri]; 435 } 436 } 437 } 438 439 if (TURNSTILE_SITE_KEY) { 440 payload.turnstile = { 441 token: formState.token || (formState.tokenInput ? formState.tokenInput.val() : ''), 442 site_key: TURNSTILE_SITE_KEY 443 }; 444 } 445 446 if (!hasErrors) { 447 requestCsrfToken() 448 .then(function (token) { 449 var axiosConfig = { 450 headers: { 451 'content-type': 'application/json', 452 'accept': 'application/json', 453 'X-CSRF-Token': token 454 } 455 }; 456 return postSubscription($form[0], payload, axiosConfig); 457 }) 458 .then(function (res) { 459 if (validation.redirect && validation.redirect[0].value && res.data.result.status === 'Subscribed' && res.data.result.redirect) { 460 window.location.href = res.data.result.redirect; 461 } 462 if (!validation.redirect && validation.message && validation.message[0].value && res.data.result === 'Subscribed') { 463 var subscriptionMessage = validation.message[0].value; 464 var $footerSubscribe = $('.b-footer-subscribe'); 465 if ($footerSubscribe.length) { 466 $footerSubscribe.find('h2').attr('style', 'opacity:0; -moz-opacity:0; filter:alpha(opacity=0)'); 467 $footerSubscribe.find('form').empty().append($('<h2>').append($('<strong>').text(subscriptionMessage))); 468 } 469 } 470 if (!validation.redirect && validation.message && validation.message[0].value && res.data.result !== 'Subscribed') { 471 Swal.fire(res.data.result); 472 } 473 if (TURNSTILE_SITE_KEY) { 474 resetTurnstile(formState); 475 if (window.turnstile && typeof window.turnstile.reset === 'function' && formState.widgetId !== null) { 476 try { 477 window.turnstile.reset(formState.widgetId); 478 } 479 catch (turnstileError) { 480 console.error('Turnstile reset failed', turnstileError); 481 } 482 } 483 } 484 if ($.isFunction(callbacks.fok)) { 485 callbacks.fok.call(this); 486 } 487 console.log(res); 488 }.bind(this)) 489 .catch(function (error) { 490 console.log(error.response || error); 491 }); 492 } 493 else if ($.isFunction(callbacks.ferr)) { 494 callbacks.ferr.call(this); 495 } 496 }; 497 } 498 499 /* ========================================================================== 500 * Drupal behavior 501 * ======================================================================== */ 502 503 Drupal.behaviors.ai_subscribe_rest = { 504 attach: function (context) { 505 $('.subscribe-rest', context).each(function () { 506 var $form = $(this); 507 508 if ($form.data('aiSubscribeInitialized')) { 509 return; 510 } 511 $form.data('aiSubscribeInitialized', true); 512 513 var form = $form[0]; 514 var submitButton = $form.find('.form-submit')[0]; 515 if (!form || !submitButton) { 516 return; 517 } 518 519 var pattern = { 520 'false': false, 521 'true': true, 522 'email': /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 523 }; 524 var $fields = $form.find('input, textarea, select'); 525 var turnstileState = createTurnstileState($form); 526 527 attachEmailTrigger($form, turnstileState); 528 submitButton.onclick = createSubmitHandler($form, turnstileState, pattern, $fields); 529 }); 530 } 531 }; 532 533})(jQuery, Drupal);
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.