nodebb-plugin-ezoic-infinite 1.8.34 → 1.8.35

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +108 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.34",
3
+ "version": "1.8.35",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
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
- TCF_DEBOUNCE_MS: 500,
41
+
42
42
  };
43
43
 
44
44
  // Limits
@@ -923,50 +923,122 @@
923
923
  } catch (_) {}
924
924
  }
925
925
 
926
- // ── TCF Locator ────────────────────────────────────────────────────────────
926
+ // ── TCF / CMP Protection ─────────────────────────────────────────────────
927
927
  //
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.
928
+ // Root cause of the CMP errors:
929
+ // "Cannot read properties of null (reading 'postMessage')"
930
+ // "Cannot set properties of null (setting 'addtlConsent')"
930
931
  //
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')".
932
+ // The CMP (Gatekeeper Consent) communicates via postMessage on the
933
+ // __tcfapiLocator iframe's contentWindow. During NodeBB ajaxify navigation,
934
+ // jQuery's html() or empty() on the content area can cascade and remove
935
+ // iframes from <body>. The CMP then calls getTCData on a stale reference
936
+ // where contentWindow is null.
934
937
  //
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;
938
+ // Strategy (3 layers):
939
+ //
940
+ // 1. PROTECT: Move the locator iframe into <head> where ajaxify never
941
+ // touches it. The TCF spec only requires the iframe to exist in the
942
+ // document with name="__tcfapiLocator" — it works from <head>.
943
+ //
944
+ // 2. GUARD: Wrap __tcfapi and __cmp with a safety layer that catches
945
+ // errors in the CMP's internal getTCData, preventing the uncaught
946
+ // TypeError from propagating.
947
+ //
948
+ // 3. RESTORE: MutationObserver on <body> childList (not subtree) to
949
+ // immediately re-create the locator if something still removes it.
940
950
 
941
951
  function ensureTcfLocator() {
942
- // Only needed if CMP APIs are present
943
952
  if (!window.__tcfapi && !window.__cmp) return;
944
953
 
945
- const inject = () => {
946
- if (document.getElementById('__tcfapiLocator')) return;
954
+ const LOCATOR_ID = '__tcfapiLocator';
955
+
956
+ // Create or relocate the locator iframe into <head> for protection
957
+ const ensureInHead = () => {
958
+ let existing = document.getElementById(LOCATOR_ID);
959
+ if (existing) {
960
+ // If it's in <body>, move it to <head> where ajaxify can't reach it
961
+ if (existing.parentElement !== document.head) {
962
+ try { document.head.appendChild(existing); } catch (_) {}
963
+ }
964
+ return existing;
965
+ }
966
+ // Create fresh
947
967
  const f = document.createElement('iframe');
948
968
  f.style.display = 'none';
949
- f.id = f.name = '__tcfapiLocator';
950
- (document.body || document.documentElement).appendChild(f);
969
+ f.id = f.name = LOCATOR_ID;
970
+ try { document.head.appendChild(f); } catch (_) {
971
+ // Fallback to body if head insertion fails
972
+ (document.body || document.documentElement).appendChild(f);
973
+ }
974
+ return f;
951
975
  };
952
976
 
953
- inject();
977
+ ensureInHead();
978
+
979
+ // Layer 2: Guard the CMP API calls against null contentWindow
980
+ if (!window.__nbbCmpGuarded) {
981
+ window.__nbbCmpGuarded = true;
982
+
983
+ // Wrap __tcfapi
984
+ if (typeof window.__tcfapi === 'function') {
985
+ const origTcf = window.__tcfapi;
986
+ window.__tcfapi = function (cmd, version, cb, param) {
987
+ try {
988
+ return origTcf.call(this, cmd, version, function (...args) {
989
+ try { cb?.(...args); } catch (_) {}
990
+ }, param);
991
+ } catch (e) {
992
+ // If the error is the null postMessage/addtlConsent, swallow it
993
+ if (e?.message?.includes('null')) {
994
+ // Re-ensure locator exists, then retry once
995
+ ensureInHead();
996
+ try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
997
+ }
998
+ }
999
+ };
1000
+ }
954
1001
 
955
- // Set up a debounced observer instead of per-mutation injection
1002
+ // Wrap __cmp (legacy CMP v1 API)
1003
+ if (typeof window.__cmp === 'function') {
1004
+ const origCmp = window.__cmp;
1005
+ window.__cmp = function (...args) {
1006
+ try {
1007
+ return origCmp.apply(this, args);
1008
+ } catch (e) {
1009
+ if (e?.message?.includes('null')) {
1010
+ ensureInHead();
1011
+ try { return origCmp.apply(this, args); } catch (_) {}
1012
+ }
1013
+ }
1014
+ };
1015
+ }
1016
+ }
1017
+
1018
+ // Layer 3: MutationObserver to immediately restore if removed
956
1019
  if (!window.__nbbTcfObs) {
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,
1020
+ window.__nbbTcfObs = new MutationObserver(muts => {
1021
+ // Fast check: still in document?
1022
+ if (document.getElementById(LOCATOR_ID)) return;
1023
+ // Something removed it restore immediately (no debounce)
1024
+ ensureInHead();
969
1025
  });
1026
+ // Observe body direct children only (the most likely removal point)
1027
+ try {
1028
+ window.__nbbTcfObs.observe(document.body || document.documentElement, {
1029
+ childList: true,
1030
+ subtree: false,
1031
+ });
1032
+ } catch (_) {}
1033
+ // Also observe <head> in case something cleans it
1034
+ try {
1035
+ if (document.head) {
1036
+ window.__nbbTcfObs.observe(document.head, {
1037
+ childList: true,
1038
+ subtree: false,
1039
+ });
1040
+ }
1041
+ } catch (_) {}
970
1042
  }
971
1043
  }
972
1044
 
@@ -985,6 +1057,8 @@
985
1057
  'cannot call refresh on the same page',
986
1058
  'no placeholders are currently defined in Refresh',
987
1059
  'Debugger iframe already exists',
1060
+ '[CMP] Error in custom getTCData',
1061
+ 'vignette: no interstitial API',
988
1062
  ];
989
1063
  const PH_PATTERN = `with id ${PH_PREFIX}`;
990
1064
 
@@ -1050,6 +1124,9 @@
1050
1124
  state.kind = null;
1051
1125
  state.blockedUntil = 0;
1052
1126
 
1127
+ // Fix: CMP modal can leave aria-hidden="true" on <body> after navigation
1128
+ try { document.body.removeAttribute('aria-hidden'); } catch (_) {}
1129
+
1053
1130
  muteConsole();
1054
1131
  ensureTcfLocator();
1055
1132
  warmNetwork();