1/** 2 * IP-access check for cached pages. 3 * 4 * WPE full-page cache serves the same cached HTML to all logged-out users 5 * regardless of cookies, so PHP never runs for these requests: the paywall 6 * modal is always present in the HTML, digitalData is always baked with 7 * anonymous user data, and the attribution banner and "Become a Member" nav 8 * item reflect whichever visitor's request generated the cached HTML. This 9 * script corrects all of it client-side for IP-matched institutional 10 * visitors â the paywall/banner/nav fixups (COP-80) and the data layer 11 * (COP-159) â on every page, not just paywalled ones. 12 * 13 * Division of labour with the inline bridge script (printed at wp_head by 14 * Ip_Team_Association::print_datalayer_bridge()): 15 * 16 * - Bridge (synchronous, before GTM): reads the wordpress_cop_ip cookie, 17 * merges the payload into digitalData.user, pushes the cop_user_ready 18 * dataLayer event, and records the outcome in window.copIpState (with the 19 * decoded payload in window.copIpUser). It runs in the head, so it cannot 20 * touch the paywall or banner DOM. 21 * - This script (deferred): applies the DOM fixups for confirmed matches â 22 * paywall removal, attribution banner, nav item â and owns the no-cookie 23 * path: fetch /ip-access (the server sets the result cookie for future 24 * pageviews), then apply the result via window.copIpApply so merge/event 25 * logic exists in one place. 26 * 27 * Between them, cop_user_ready fires exactly once per pageview in every 28 * outcome (match, no match, error, timeout) â the contract GTM relies on if 29 * the GA4 pageview tag is ever repointed to the event (COP-159 "option B"). 30 */ 31(async function copCheckIp() { 32 function removePaywall() { 33 const overlay = document.getElementById('paywall-overlay'); 34 if (overlay) { 35 overlay.remove(); 36 } 37 document.body.style.overflow = ''; 38 } 39 40 // The organization name shown in the attribution banner rides in the 41 // digitalData.user payload â teams[0].teamName is the matched team's raw 42 // title, the same value the server renders for cache-bypassed requests. 43 function orgFromUser(user) { 44 return (user && user.teams && user.teams[0] && user.teams[0].teamName) || ''; 45 } 46 47 // Populate and reveal the site-license attribution banner (COP-80). The 48 // container and its static copy are baked into the cached HTML; we only set 49 // the organization name and unhide it. Suppressed when no name is available. 50 function showAttribution(orgName) { 51 if (!orgName) { 52 return; 53 } 54 const banner = document.getElementById('cop-site-license-attribution'); 55 if (!banner) { 56 return; 57 } 58 // A visible banner was populated server-side (cache-bypassed render) â 59 // PHP's value is fresher than the cookie's, so never overwrite it. 60 if (!banner.hidden) { 61 return; 62 } 63 const org = banner.querySelector('.cop-sla-org'); 64 if (org) { 65 org.textContent = orgName; 66 } 67 banner.hidden = false; 68 } 69 70 // Remove the "Become a Member" utility-nav item for IP-matched visitors on 71 // a cached page (COP-80). Logged-in users and freshly-rendered IP-matched 72 // requests have it stripped server-side in hide_become_a_member() instead. 73 function hideBecomeAMember() { 74 document.querySelectorAll('#utility-nav a[href*="/memberships"]').forEach((link) => { 75 const item = link.closest('li'); 76 if (item) { 77 item.remove(); 78 } 79 }); 80 } 81 82 // Everything a confirmed match changes in the cached DOM (COP-80). 83 function applyDomFixups(user) { 84 removePaywall(); 85 showAttribution(orgFromUser(user)); 86 hideBecomeAMember(); 87 } 88 89 // Merge + event via the inline bridge. The fallback only runs on HTML 90 // cached before the bridge shipped (self-heals as the page cache turns 91 // over) â it still upholds the one-event-per-pageview contract. 92 function apply(user, match) { 93 if (typeof window.copIpApply === 'function') { 94 window.copIpApply(user, match); 95 return; 96 } 97 window.dataLayer = window.dataLayer || []; 98 window.dataLayer.push({ event: 'cop_user_ready', copIpMatch: !!match, copUser: user || null }); 99 } 100 101 // The bridge already resolved this pageview from the cookie â the data 102 // layer is settled and the event has fired; only the DOM fixups are left. 103 if (window.copIpState === 'match') { 104 applyDomFixups(window.copIpUser || null); 105 return; 106 } 107 if (window.copIpState === 'nomatch') { 108 return; 109 } 110 111 const endpoint = (window.copIpCheck && window.copIpCheck.endpoint) || ''; 112 if (!endpoint) { 113 apply(null, false); 114 return; 115 } 116 117 try { 118 const response = await fetch(endpoint, { 119 credentials: 'same-origin', 120 cache: 'no-store', 121 signal: AbortSignal.timeout(5000), 122 }); 123 const data = await response.json(); 124 if (data && data.access === true) { 125 apply(data.user || null, true); 126 applyDomFixups(data.user || null); 127 } else { 128 apply(null, false); 129 } 130 } catch { 131 // Silent failure (including timeout AbortError) â visitor sees normal 132 // paywall behaviour; the event still fires with the anonymous state so 133 // analytics is never blocked on a failed check. 134 apply(null, false); 135 } 136}());
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.