nodebb-plugin-ezoic-infinite 1.8.41 → 1.8.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/library.js +1 -2
- package/package.json +1 -1
- package/public/client.js +31 -176
package/library.js
CHANGED
|
@@ -144,8 +144,7 @@ const EZOIC_SCRIPTS = [
|
|
|
144
144
|
'<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
|
|
145
145
|
'<script>',
|
|
146
146
|
'window.ezstandalone = window.ezstandalone || {};',
|
|
147
|
-
|
|
148
|
-
'window.ezstandalone.cmd = window.ezstandalone.cmd || [];',
|
|
147
|
+
'ezstandalone.cmd = ezstandalone.cmd || [];',
|
|
149
148
|
'</script>',
|
|
150
149
|
].join('\n');
|
|
151
150
|
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
SHOW_RELEASE_MS: 700,
|
|
39
39
|
BATCH_FLUSH_MS: 80,
|
|
40
40
|
RECYCLE_DELAY_MS: 450,
|
|
41
|
-
|
|
41
|
+
TCF_DEBOUNCE_MS: 500,
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
// Limits
|
|
@@ -257,54 +257,6 @@
|
|
|
257
257
|
return set;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
// ── Garbage collection (NodeBB post virtualization safe) ─────────────────
|
|
261
|
-
//
|
|
262
|
-
// NodeBB can remove large portions of the DOM during infinite scrolling
|
|
263
|
-
// (especially posts in topics). If a wrap is removed outside of our own
|
|
264
|
-
// dropWrap(), we must free its placeholder id; otherwise the pool appears
|
|
265
|
-
// exhausted and ads won't re-insert when the user scrolls back up.
|
|
266
|
-
|
|
267
|
-
function gcDisconnectedWraps() {
|
|
268
|
-
// 1) Clean wrapByKey
|
|
269
|
-
for (const [key, w] of Array.from(state.wrapByKey.entries())) {
|
|
270
|
-
if (!w?.isConnected) state.wrapByKey.delete(key);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// 2) Clean wrapsByClass sets and free ids
|
|
274
|
-
for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
|
|
275
|
-
for (const w of Array.from(set)) {
|
|
276
|
-
if (w?.isConnected) continue;
|
|
277
|
-
set.delete(w);
|
|
278
|
-
try {
|
|
279
|
-
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
280
|
-
if (Number.isFinite(id)) {
|
|
281
|
-
state.mountedIds.delete(id);
|
|
282
|
-
state.phState.delete(id);
|
|
283
|
-
state.lastShow.delete(id);
|
|
284
|
-
}
|
|
285
|
-
} catch (_) {}
|
|
286
|
-
}
|
|
287
|
-
if (!set.size) state.wrapsByClass.delete(klass);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// 3) Authoritative rebuild of mountedIds from the live DOM
|
|
291
|
-
try {
|
|
292
|
-
const live = new Set();
|
|
293
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
294
|
-
const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
295
|
-
if (id > 0) live.add(id);
|
|
296
|
-
}
|
|
297
|
-
// Drop any ids that are no longer live
|
|
298
|
-
for (const id of Array.from(state.mountedIds)) {
|
|
299
|
-
if (!live.has(id)) {
|
|
300
|
-
state.mountedIds.delete(id);
|
|
301
|
-
state.phState.delete(id);
|
|
302
|
-
state.lastShow.delete(id);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
} catch (_) {}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
260
|
// ── Wrap lifecycle detection ───────────────────────────────────────────────
|
|
309
261
|
|
|
310
262
|
/**
|
|
@@ -813,10 +765,6 @@
|
|
|
813
765
|
async function runCore() {
|
|
814
766
|
if (isBlocked()) return 0;
|
|
815
767
|
|
|
816
|
-
// Keep internal pools in sync with NodeBB DOM virtualization/removals
|
|
817
|
-
// so ads can re-insert correctly when users scroll back up.
|
|
818
|
-
try { gcDisconnectedWraps(); } catch (_) {}
|
|
819
|
-
|
|
820
768
|
const cfg = await fetchConfig();
|
|
821
769
|
if (!cfg || cfg.excluded) return 0;
|
|
822
770
|
initPools(cfg);
|
|
@@ -940,22 +888,6 @@
|
|
|
940
888
|
|
|
941
889
|
for (const m of muts) {
|
|
942
890
|
if (m.type !== 'childList') continue;
|
|
943
|
-
|
|
944
|
-
// If NodeBB removed wraps as part of virtualization, free ids immediately
|
|
945
|
-
for (const node of m.removedNodes) {
|
|
946
|
-
if (!(node instanceof Element)) continue;
|
|
947
|
-
try {
|
|
948
|
-
if (node.classList?.contains(WRAP_CLASS)) {
|
|
949
|
-
dropWrap(node);
|
|
950
|
-
} else {
|
|
951
|
-
const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
|
|
952
|
-
if (wraps?.length) {
|
|
953
|
-
for (const w of wraps) dropWrap(w);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
} catch (_) {}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
891
|
for (const node of m.addedNodes) {
|
|
960
892
|
if (!(node instanceof Element)) continue;
|
|
961
893
|
|
|
@@ -991,122 +923,50 @@
|
|
|
991
923
|
} catch (_) {}
|
|
992
924
|
}
|
|
993
925
|
|
|
994
|
-
// ── TCF
|
|
995
|
-
//
|
|
996
|
-
// Root cause of the CMP errors:
|
|
997
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
998
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
999
|
-
//
|
|
1000
|
-
// The CMP (Gatekeeper Consent) communicates via postMessage on the
|
|
1001
|
-
// __tcfapiLocator iframe's contentWindow. During NodeBB ajaxify navigation,
|
|
1002
|
-
// jQuery's html() or empty() on the content area can cascade and remove
|
|
1003
|
-
// iframes from <body>. The CMP then calls getTCData on a stale reference
|
|
1004
|
-
// where contentWindow is null.
|
|
1005
|
-
//
|
|
1006
|
-
// Strategy (3 layers):
|
|
926
|
+
// ── TCF Locator ────────────────────────────────────────────────────────────
|
|
1007
927
|
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
// document with name="__tcfapiLocator" — it works from <head>.
|
|
928
|
+
// FIX: The CMP iframe locator (__tcfapiLocator) must exist for the CMP to
|
|
929
|
+
// communicate via postMessage. NodeBB's ajaxify can remove it during navigation.
|
|
1011
930
|
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
931
|
+
// Previous approach: MutationObserver that re-injects on EVERY mutation → race
|
|
932
|
+
// conditions with CMP, causing "Cannot read properties of null (reading 'postMessage')"
|
|
933
|
+
// and "Cannot set properties of null (setting 'addtlConsent')".
|
|
1015
934
|
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
935
|
+
// New approach: Debounced check — only re-inject after DOM stabilizes (500ms).
|
|
936
|
+
// The CMP scripts handle their own initialization; we just ensure the locator
|
|
937
|
+
// iframe exists when needed.
|
|
938
|
+
|
|
939
|
+
let _tcfDebounce = null;
|
|
1018
940
|
|
|
1019
941
|
function ensureTcfLocator() {
|
|
942
|
+
// Only needed if CMP APIs are present
|
|
1020
943
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
1021
944
|
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
// Create or relocate the locator iframe into <head> for protection
|
|
1025
|
-
const ensureInHead = () => {
|
|
1026
|
-
let existing = document.getElementById(LOCATOR_ID);
|
|
1027
|
-
if (existing) {
|
|
1028
|
-
// If it's in <body>, move it to <head> where ajaxify can't reach it
|
|
1029
|
-
if (existing.parentElement !== document.head) {
|
|
1030
|
-
try { document.head.appendChild(existing); } catch (_) {}
|
|
1031
|
-
}
|
|
1032
|
-
return existing;
|
|
1033
|
-
}
|
|
1034
|
-
// Create fresh
|
|
945
|
+
const inject = () => {
|
|
946
|
+
if (document.getElementById('__tcfapiLocator')) return;
|
|
1035
947
|
const f = document.createElement('iframe');
|
|
1036
948
|
f.style.display = 'none';
|
|
1037
|
-
f.id = f.name =
|
|
1038
|
-
|
|
1039
|
-
// Fallback to body if head insertion fails
|
|
1040
|
-
(document.body || document.documentElement).appendChild(f);
|
|
1041
|
-
}
|
|
1042
|
-
return f;
|
|
949
|
+
f.id = f.name = '__tcfapiLocator';
|
|
950
|
+
(document.body || document.documentElement).appendChild(f);
|
|
1043
951
|
};
|
|
1044
952
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
// Layer 2: Guard the CMP API calls against null contentWindow
|
|
1048
|
-
if (!window.__nbbCmpGuarded) {
|
|
1049
|
-
window.__nbbCmpGuarded = true;
|
|
1050
|
-
|
|
1051
|
-
// Wrap __tcfapi
|
|
1052
|
-
if (typeof window.__tcfapi === 'function') {
|
|
1053
|
-
const origTcf = window.__tcfapi;
|
|
1054
|
-
window.__tcfapi = function (cmd, version, cb, param) {
|
|
1055
|
-
try {
|
|
1056
|
-
return origTcf.call(this, cmd, version, function (...args) {
|
|
1057
|
-
try { cb?.(...args); } catch (_) {}
|
|
1058
|
-
}, param);
|
|
1059
|
-
} catch (e) {
|
|
1060
|
-
// If the error is the null postMessage/addtlConsent, swallow it
|
|
1061
|
-
if (e?.message?.includes('null')) {
|
|
1062
|
-
// Re-ensure locator exists, then retry once
|
|
1063
|
-
ensureInHead();
|
|
1064
|
-
try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
953
|
+
inject();
|
|
1069
954
|
|
|
1070
|
-
|
|
1071
|
-
if (typeof window.__cmp === 'function') {
|
|
1072
|
-
const origCmp = window.__cmp;
|
|
1073
|
-
window.__cmp = function (...args) {
|
|
1074
|
-
try {
|
|
1075
|
-
return origCmp.apply(this, args);
|
|
1076
|
-
} catch (e) {
|
|
1077
|
-
if (e?.message?.includes('null')) {
|
|
1078
|
-
ensureInHead();
|
|
1079
|
-
try { return origCmp.apply(this, args); } catch (_) {}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// Layer 3: MutationObserver to immediately restore if removed
|
|
955
|
+
// Set up a debounced observer instead of per-mutation injection
|
|
1087
956
|
if (!window.__nbbTcfObs) {
|
|
1088
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
1089
|
-
//
|
|
1090
|
-
if (document.getElementById(
|
|
1091
|
-
//
|
|
1092
|
-
|
|
957
|
+
window.__nbbTcfObs = new MutationObserver(() => {
|
|
958
|
+
// Quick check: if locator still exists, skip
|
|
959
|
+
if (document.getElementById('__tcfapiLocator')) return;
|
|
960
|
+
// Debounce: wait for DOM to stabilize before re-injecting
|
|
961
|
+
clearTimeout(_tcfDebounce);
|
|
962
|
+
_tcfDebounce = setTimeout(inject, TIMING.TCF_DEBOUNCE_MS);
|
|
963
|
+
});
|
|
964
|
+
// Observe only direct children of body (not deep subtree)
|
|
965
|
+
// since the locator iframe is a direct child
|
|
966
|
+
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
967
|
+
childList: true,
|
|
968
|
+
subtree: false,
|
|
1093
969
|
});
|
|
1094
|
-
// Observe body direct children only (the most likely removal point)
|
|
1095
|
-
try {
|
|
1096
|
-
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
1097
|
-
childList: true,
|
|
1098
|
-
subtree: false,
|
|
1099
|
-
});
|
|
1100
|
-
} catch (_) {}
|
|
1101
|
-
// Also observe <head> in case something cleans it
|
|
1102
|
-
try {
|
|
1103
|
-
if (document.head) {
|
|
1104
|
-
window.__nbbTcfObs.observe(document.head, {
|
|
1105
|
-
childList: true,
|
|
1106
|
-
subtree: false,
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
} catch (_) {}
|
|
1110
970
|
}
|
|
1111
971
|
}
|
|
1112
972
|
|
|
@@ -1125,8 +985,6 @@
|
|
|
1125
985
|
'cannot call refresh on the same page',
|
|
1126
986
|
'no placeholders are currently defined in Refresh',
|
|
1127
987
|
'Debugger iframe already exists',
|
|
1128
|
-
'[CMP] Error in custom getTCData',
|
|
1129
|
-
'vignette: no interstitial API',
|
|
1130
988
|
];
|
|
1131
989
|
const PH_PATTERN = `with id ${PH_PREFIX}`;
|
|
1132
990
|
|
|
@@ -1192,9 +1050,6 @@
|
|
|
1192
1050
|
state.kind = null;
|
|
1193
1051
|
state.blockedUntil = 0;
|
|
1194
1052
|
|
|
1195
|
-
// Fix: CMP modal can leave aria-hidden="true" on <body> after navigation
|
|
1196
|
-
try { document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
1197
|
-
|
|
1198
1053
|
muteConsole();
|
|
1199
1054
|
ensureTcfLocator();
|
|
1200
1055
|
warmNetwork();
|