1/** 2 * main.js - Main site functionality 3 * Handles form submissions, page interactions, and UI effects 4 */ 5 6let pageLoadTime = Date.now(); 7let challengeCompleted = false; 8 9$(document).ready(function () { 10 // External link handler - opens external links in new window, except campaignpartner.net 11 const links = document.querySelectorAll('a[href^="http://"], a[href^="https://"]'); 12 links.forEach(link => { 13 if (link.hostname && 14 link.hostname !== location.hostname && 15 !link.hostname.endsWith('campaignpartner.net')) { 16 link.target = '_blank'; 17 } 18 }); 19 20 // Initialize YouTube Content Fixer 21 YouTubeContentFixer.init(); 22 23 // Form submission handler 24 $("#subscription").submit(function (j) { 25 j.preventDefault(); 26 27 var currentTime = Date.now(); 28 var elapsedTime = currentTime - pageLoadTime; 29 var obfuscatedTime = btoa(elapsedTime.toString()); 30 var encodedTimestamp = btoa(currentTime.toString()); 31 32 var c = $(this), 33 e = c.find('input[name="e"]').val(), 34 i = c.find('input[name="z"]').val(), 35 b = c.find('input[name="f"]').val(), 36 k = c.find('input[name="l"]').val(); 37 38 $.post("/EmailSignup.ashx", { 39 e: e, 40 z: i, 41 f: b, 42 l: k, 43 t: obfuscatedTime, 44 ts: encodedTimestamp 45 }, function (h) { 46 var g = $(h).find("#formResponse"); 47 $("#result").html(g); 48 }).fail(function (xhr, status, error) { 49 console.log("Error: " + error); 50 }); 51 }); 52 53 // Scroll to top functionality 54 var $backToTop = $('#backToTop'); 55 var backToTopShown = false; 56 57 $(window).on('scroll', function () { 58 var shouldShow = $(this).scrollTop() > 0; // > 0 ignores iOS's negative bounce offsets 59 if (shouldShow !== backToTopShown) { 60 backToTopShown = shouldShow; 61 $backToTop.stop(true)[shouldShow ? 'fadeIn' : 'fadeOut'](400); 62 } 63 }); 64 65 $backToTop.on('click', function (e) { 66 e.preventDefault(); 67 // Let the browser animate the scroll itself. On iOS, a jQuery scrollTop 68 // tween competes with Safari's native scrolling and can bounce back. 69 if ('scrollBehavior' in document.documentElement.style) { 70 window.scrollTo({ top: 0, behavior: 'smooth' }); 71 } else { 72 $('html, body').stop().animate({ scrollTop: 0 }, 800); // older browsers 73 } 74 }); 75}); 76 77// Also run when window fully loads (in case of late-loading content) 78window.addEventListener('load', function () { 79 setTimeout(() => { 80 YouTubeContentFixer.fixYouTubeContent(); 81 }, 1000); 82}); 83 84 85// Challenge completion tracker 86function completeChallenge() { 87 challengeCompleted = true; 88} 89 90setTimeout(completeChallenge, 3000); 91 92// Utility functions 93function encodeValue(value) { 94 return btoa(value.toString()); 95} 96 97function isValidEmail(email) { 98 const re = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; 99 return re.test(email); 100} 101 102// Form submission function 103function subForm() { 104 const subEElement = document.querySelector('#subE'); 105 const subZElement = document.querySelector('#subZ'); 106 const subFElement = document.querySelector('#subF'); 107 const subLElement = document.querySelector('#subL'); 108 const subPElement = document.querySelector('#subP'); 109 const messageElement = document.querySelector('#subH'); 110 const disclaimerCheckbox = document.querySelector('#disclaimerCheckbox'); 111 let isFormValid = true; 112 113 const params = { 114 email: subEElement && subEElement.value.trim() ? subEElement.value.trim() : '', 115 zip: subZElement && subZElement.value.trim() ? subZElement.value.trim() : '', 116 first: subFElement && subFElement.value.trim() ? subFElement.value.trim() : '', 117 last: subLElement && subLElement.value.trim() ? subLElement.value.trim() : '', 118 phone: subPElement && subPElement.value.trim() ? subPElement.value.trim() : '', 119 message: messageElement && messageElement.value.trim() ? messageElement.value.trim() : '' 120 }; 121 122 // SMS opt-in: if the disclaimer checkbox carries a data-list-id,
123 // it's the centralized SMS opt-in (text was overwritten by the server). 124 // Send the checked state and list ID so the handler can tag the contact. 125 const disclaimerCb = document.querySelector('#disclaimerCheckbox'); 126 if (disclaimerCb && disclaimerCb.getAttribute('data-list-id')) { 127 params.smsOptIn = disclaimerCb.checked; 128 params.smsListId = parseInt(disclaimerCb.getAttribute('data-list-id'), 10) || 0; 129 } 130 131 [subEElement, subZElement, subFElement, subLElement].forEach(field => { 132 if (field && field.classList.contains('required') && !field.value.trim()) { 133 field.classList.add("error"); 134 isFormValid = false; 135 } else if (field) { 136 field.classList.remove("error"); 137 } 138 }); 139 140 if (subEElement && subEElement.value.trim() && !isValidEmail(subEElement.value)) { 141 subEElement.classList.add("error"); 142 isFormValid = false; 143 } 144 145 if (disclaimerCheckbox && !disclaimerCheckbox.checked) { 146 isFormValid = false; 147 disclaimerCheckbox.classList.add("error"); 148 } else if (disclaimerCheckbox) { 149 disclaimerCheckbox.classList.remove("error"); 150 } 151 152 const currentTime = Date.now(); 153 const elapsedTime = currentTime - pageLoadTime; 154 const obfuscatedTime = btoa(elapsedTime.toString()); 155 const encodedTimestamp = btoa(currentTime.toString()); 156 params.t = obfuscatedTime; 157 params.ts = encodedTimestamp; 158 159 if (isFormValid) { 160 fetch('/EmailSignup', { 161 method: 'POST', 162 headers: { 163 'Content-Type': 'application/json' 164 }, 165 body: JSON.stringify(params) 166 }) 167 .then(response => { 168 // Non-2xx responses (e.g. a 405 from the routing middleware on 169 // suspended/unpublished sites) may have an empty or non-JSON 170 // body; parse only when the server says it sent JSON so we fail 171 // with a clear error instead of a JSON.parse exception. 172 const contentType = response.headers.get('content-type') || ''; 173 if (contentType.includes('application/json')) { 174 return response.json(); 175 } 176 if (!response.ok) { 177 throw new Error('Signup request failed: HTTP ' + response.status); 178 } 179 throw new Error('Signup request returned an unexpected response.'); 180 }) 181 .then(data => { 182 if (data.success) { 183 $("#emailSignup").hide(); 184 $("#signupThanks").show(); 185 } else { 186 console.log("Error: " + (data.error || "Unknown error occurred")); 187 // Tell the widget's button wrapper the attempt is over so it 188 // can restore the button immediately instead of waiting for 189 // its 10-second backstop. 190 document.dispatchEvent(new CustomEvent('signup:error')); 191 alert(data.error || "An error occurred. Please try again."); 192 } 193 }) 194 .catch(error => { 195 console.error('Fetch error:', error); 196 document.dispatchEvent(new CustomEvent('signup:error')); 197 alert("An error occurred. Please try again."); 198 }); 199 200 // A request is in flight: the button wrapper may enter its 201 // "Submittingâ¦" state. 202 return true; 203 } else { 204 if (disclaimerCheckbox && !disclaimerCheckbox.checked) { 205 alert('Please agree to the disclaimer before submitting.'); 206 } else { 207 alert('Please correct the errors in the form.'); 208 } 209 210 // Validation failed, nothing was sent: the button wrapper must NOT 211 // enter its "Submittingâ¦" state (a disabled button here left the 212 // form dead for 10 seconds after an unchecked required checkbox). 213 return false; 214 } 215} 216 217// Sidebar email signup function 218function sidebarSubForm() { 219 const sidebarSubEElement = document.querySelector('#sidebarSubE'); 220 const sidebarSubButton = document.querySelector('#sidebarSubButton'); 221 let isFormValid = true; 222 223 const params = { 224 email: sidebarSubEElement && sidebarSubEElement.value.trim() ? sidebarSubEElement.value.trim() : '', 225 zip: '', 226 first: '', 227 last: '', 228 phone: '', 229 message: '' 230 }; 231 232 if (!params.email) { 233 sidebarSubEElement.classList.add("error"); 234 isFormValid = false; 235 } else if (!isValidEmail(params.email)) { 236 sidebarSubEElement.classList.add("error"); 237 isFormValid = false; 238 } else { 239 sidebarSubEElement.classList.remove("error"); 240 } 241 242 const currentTime = Date.now(); 243 const elapsedTime = currentTime - pageLoadTime; 244 params.t = btoa(elapsedTime.toString()); 245 params.ts = btoa(currentTime.toString()); 246 247 if (isFormValid) { 248 sidebarSubButton.disabled = true; 249 sidebarSubButton.value = "..."; 250 251 fetch('/EmailSignup', { 252 method: 'POST', 253 headers: { 254 'Content-Type': 'application/json' 255 }, 256 body: JSON.stringify(params) 257 }) 258 .then(response => { 259 // Same guard as subForm(): only parse JSON when the server 260 // actually sent JSON; surface everything else as an error. 261 const contentType = response.headers.get('content-type') || ''; 262 if (contentType.includes('application/json')) { 263 return response.json(); 264 } 265 if (!response.ok) { 266 throw new Error('Signup request failed: HTTP ' + response.status); 267 } 268 throw new Error('Signup request returned an unexpected response.'); 269 }) 270 .then(data => { 271 if (data.success) { 272 $("#sidebarEmailSignup").hide(); 273 $("#sidebarSignupThanks").show(); 274 } else { 275 sidebarSubButton.disabled = false; 276 sidebarSubButton.value = "GO"; 277 alert(data.error || "An error occurred. Please try again."); 278 } 279 }) 280 .catch(error => { 281 sidebarSubButton.disabled = false; 282 sidebarSubButton.value = "GO"; 283 console.error('Fetch error:', error); 284 alert("An error occurred. Please try again."); 285 }); 286 } else { 287 alert('Please enter a valid email address.'); 288 } 289} 290 291/** 292* YouTube Content Fixer - Automatically converts YouTube embeds to privacy-enhanced mode
293* and adds appropriate referrer policies 294*/ 295const YouTubeContentFixer = { 296 /** 297 * Main function to fix all YouTube content on the page 298 */ 299 fixYouTubeContent: function () { 300 const iframes = document.querySelectorAll('iframe'); 301 302 iframes.forEach(iframe => { 303 const src = iframe.getAttribute('src'); 304 if (src && (src.includes('youtube.com') || src.includes('youtu.be'))) { 305 // Create a completely new iframe 306 const newIframe = document.createElement('iframe'); 307 308 // Copy all attributes EXCEPT src 309 for (let attr of iframe.attributes) { 310 if (attr.name !== 'src') { 311 newIframe.setAttribute(attr.name, attr.value); 312 } 313 } 314 315 // Set referrer policy FIRST (before src) 316 newIframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 317 318 // Then set the corrected src 319 const fixedUrl = this.convertToNoCookie(src); 320 newIframe.setAttribute('src', fixedUrl); 321 322 // Replace old iframe with new one 323 iframe.parentNode.replaceChild(newIframe, iframe); 324 } 325 }); 326 }, 327 328 /** 329 * Fixes YouTube URL in an iframe element 330 */ 331 fixYouTubeUrl: function (iframe) { 332 const src = iframe.getAttribute('src'); 333 if (!src) return false; 334 335 const fixedUrl = this.convertToNoCookie(src); 336 if (fixedUrl !== src) { 337 iframe.setAttribute('src', fixedUrl); 338 return true; 339 } 340 return false; 341 }, 342 343 /** 344 * Converts a YouTube URL to youtube-nocookie.com 345 */ 346 convertToNoCookie: function (url) { 347 let fixedUrl = url; 348 349 // Handle youtu.be shortened URLs 350 if (url.includes('youtu.be/')) { 351 const videoIdMatch = url.match(/youtu\.be\/([a-zA-Z0-9_-]+)(\?.*)?$/); 352 if (videoIdMatch) { 353 const videoId = videoIdMatch[1]; 354 const params = videoIdMatch[2] || ''; 355 fixedUrl = `https://www.youtube-nocookie.com/embed/${videoId}${params}`; 356 } 357 } 358 359 // Replace various YouTube URL patterns 360 const replacements = [ 361 { from: 'https://www.youtube.com/embed/', to: 'https://www.youtube-nocookie.com/embed/' }, 362 { from: 'https://youtube.com/embed/', to: 'https://www.youtube-nocookie.com/embed/' }, 363 { from: 'http://www.youtube.com/embed/', to: 'https://www.youtube-nocookie.com/embed/' }, 364 { from: 'http://youtube.com/embed/', to: 'https://www.youtube-nocookie.com/embed/' }, 365 { from: '//www.youtube.com/embed/', to: '//www.youtube-nocookie.com/embed/' }, 366 { from: '//youtube.com/embed/', to: '//www.youtube-nocookie.com/embed/' },
367 { from: 'youtube.com/v/', to: 'youtube-nocookie.com/embed/' } 368 ]; 369 370 replacements.forEach(replacement => { 371 if (fixedUrl.includes(replacement.from)) { 372 fixedUrl = fixedUrl.replace(replacement.from, replacement.to); 373 } 374 }); 375 376 // Handle src="youtube.com/embed/" without protocol 377 if (fixedUrl.match(/^(?!https?:\/\/)(?!\/\/)(www\.)?youtube\.com\/embed\//)) { 378 fixedUrl = fixedUrl.replace(/^(?:www\.)?youtube\.com\/embed\//, 'https://www.youtube-nocookie.com/embed/'); 379 } 380 381 return fixedUrl; 382 }, 383 384 /** 385 * Adds referrer policy to iframe if it contains youtube-nocookie and doesn't have one 386 */ 387 addReferrerPolicy: function (iframe) { 388 const src = iframe.getAttribute('src'); 389 const dataSrc = iframe.getAttribute('data-src'); 390 391 // Check if iframe has youtube-nocookie.com in src or data-src 392 const hasYouTubeNoCookie = (src && src.includes('youtube-nocookie.com')) || 393 (dataSrc && dataSrc.includes('youtube-nocookie.com')); 394 395 if (hasYouTubeNoCookie && !iframe.hasAttribute('referrerpolicy')) { 396 iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 397 return true; 398 } 399 return false; 400 }, 401 402 /** 403 * Analyze content (for debugging) 404 */ 405 analyzeContent: function () { 406 const analysis = { 407 totalIframes: 0, 408 needingUrlFix: 0, 409 needingReferrerPolicy: 0, 410 iframes: [] 411 }; 412 413 const iframes = document.querySelectorAll('iframe'); 414 415 iframes.forEach((iframe, index) => { 416 const src = iframe.getAttribute('src') || ''; 417 const dataSrc = iframe.getAttribute('data-src') || ''; 418 const combinedSrc = src || dataSrc; 419 420 const iframeInfo = { 421 index: index, 422 src: combinedSrc, 423 domain: '', 424 videoId: '', 425 hasReferrerPolicy: iframe.hasAttribute('referrerpolicy'), 426 needsUrlFix: false, 427 needsReferrerPolicy: false 428 }; 429 430 // Determine domain and if fixes are needed 431 if (combinedSrc.includes('youtube-nocookie.com')) { 432 iframeInfo.domain = 'youtube-nocookie.com'; 433 iframeInfo.needsReferrerPolicy = !iframeInfo.hasReferrerPolicy; 434 } else if (combinedSrc.includes('youtube.com')) { 435 iframeInfo.domain = 'youtube.com'; 436 iframeInfo.needsUrlFix = true; 437 iframeInfo.needsReferrerPolicy = true; 438 } else if (combinedSrc.includes('youtu.be')) { 439 iframeInfo.domain = 'youtu.be'; 440 iframeInfo.needsUrlFix = true; 441 iframeInfo.needsReferrerPolicy = true; 442 } 443 444 // Extract video ID 445 const videoIdMatch = combinedSrc.match(/(?:embed|v)\/([a-zA-Z0-9_-]+)|youtu\.be\/([a-zA-Z0-9_-]+)/); 446 if (videoIdMatch) { 447 iframeInfo.videoId = videoIdMatch[1] || videoIdMatch[2]; 448 } 449 450 analysis.totalIframes++; 451 if (iframeInfo.needsUrlFix) analysis.needingUrlFix++; 452 if (iframeInfo.needsReferrerPolicy) analysis.needingReferrerPolicy++; 453 analysis.iframes.push(iframeInfo); 454 }); 455 456 return analysis; 457 }, 458 459 /** 460 * Initialize and set up observers for dynamic content 461 */ 462 init: function () { 463 // Fix existing content 464 this.fixYouTubeContent(); 465 466 // Set up MutationObserver to handle dynamically added content 467 const observer = new MutationObserver((mutations) => { 468 let shouldFix = false; 469 470 mutations.forEach(mutation => { 471 if (mutation.type === 'childList') { 472 mutation.addedNodes.forEach(node => { 473 if (node.nodeType === 1) { // Element node 474 if (node.tagName === 'IFRAME' || 475 node.querySelector?.('iframe')) { 476 shouldFix = true; 477 } 478 } 479 }); 480 } 481 }); 482 483 if (shouldFix) { 484 // Debounce to avoid multiple rapid fixes 485 clearTimeout(this.fixTimeout); 486 this.fixTimeout = setTimeout(() => { 487 this.fixYouTubeContent(); 488 }, 100); 489 } 490 }); 491 492 // Start observing 493 observer.observe(document.body, { 494 childList: true, 495 subtree: true 496 }); 497 498 //console.log('YouTube Content Fixer initialized'); 499 } 500};
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.