PageSourceSearch

https://www.philanthropy.com/wp-content/plugins/philanthropy-2026/assets/js/ip-check.js?ver=1787155485

js philanthropy.com collected 2026-10-01 08:46:04 UTC 5,226 bytes, 136 lines download raw bytes

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.