nodebb-plugin-ezoic-infinite 1.8.70 → 1.8.72

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 CHANGED
@@ -7,17 +7,18 @@ const db = require.main.require('./src/database');
7
7
  const SETTINGS_KEY = 'ezoic-infinite';
8
8
  const plugin = {};
9
9
 
10
- // ── Helpers ────────────────────────────────────────────────────────────────
10
+ // ── Helpers ──────────────────────────────────────────────────────────────────
11
11
 
12
12
  function normalizeExcludedGroups(value) {
13
13
  if (!value) return [];
14
14
  if (Array.isArray(value)) return value;
15
- // NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
16
15
  const s = String(value).trim();
17
16
  if (s.startsWith('[')) {
18
- try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
17
+ try {
18
+ const parsed = JSON.parse(s);
19
+ if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
20
+ } catch (_) {}
19
21
  }
20
- // Fallback : séparation par virgule
21
22
  return s.split(',').map(v => v.trim()).filter(Boolean);
22
23
  }
23
24
 
@@ -30,25 +31,29 @@ function parseBool(v, def = false) {
30
31
 
31
32
  async function getAllGroups() {
32
33
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
33
- if (!names || !names.length) {
34
+ if (!names?.length) {
34
35
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
35
36
  }
36
- const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
37
- const data = await groups.getGroupsData(filtered);
38
- const valid = data.filter(g => g && g.name);
39
- valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
40
- return valid;
37
+ return (await groups.getGroupsData(
38
+ (names || []).filter(name => !groups.isPrivilegeGroup(name))
39
+ ))
40
+ .filter(g => g?.name)
41
+ .sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
41
42
  }
42
43
 
43
- // ── Settings cache (30s TTL) ────────────────────────────────────────────────
44
+ // ── Settings cache ───────────────────────────────────────────────────────────
44
45
 
45
46
  let _settingsCache = null;
46
47
  let _settingsCacheAt = 0;
47
48
  const SETTINGS_TTL = 30_000;
48
49
 
50
+ const _excludeCache = new Map();
51
+ const EXCLUDE_TTL = 60_000;
52
+
49
53
  async function getSettings() {
50
- const now = Date.now();
51
- if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
54
+ const t = Date.now();
55
+ if (_settingsCache && (t - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
56
+
52
57
  const s = await meta.settings.get(SETTINGS_KEY);
53
58
  _settingsCacheAt = Date.now();
54
59
  _settingsCache = {
@@ -71,52 +76,121 @@ async function getSettings() {
71
76
 
72
77
  async function isUserExcluded(uid, excludedGroups) {
73
78
  if (!uid || !excludedGroups.length) return false;
79
+
80
+ const key = `${uid}|${excludedGroups.join('|')}`;
81
+ const t = Date.now();
82
+ const hit = _excludeCache.get(key);
83
+ if (hit && (t - hit.at) < EXCLUDE_TTL) return hit.value;
84
+
85
+ const excludedSet = new Set(excludedGroups);
74
86
  const userGroups = await groups.getUserGroups([uid]);
75
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
87
+ const value = (userGroups[0] || []).some(g => excludedSet.has(g.name));
88
+
89
+ _excludeCache.set(key, { value, at: Date.now() });
90
+ if (_excludeCache.size > 1000) {
91
+ _excludeCache.delete(_excludeCache.keys().next().value);
92
+ }
93
+
94
+ return value;
95
+ }
96
+
97
+ function clearCaches() {
98
+ _settingsCache = null;
99
+ _settingsCacheAt = 0;
100
+ _excludeCache.clear();
76
101
  }
77
102
 
78
- // ── Scripts Ezoic ──────────────────────────────────────────────────────────
103
+ // ── Client config ────────────────────────────────────────────────────────────
104
+
105
+ function buildClientConfig(settings, excluded) {
106
+ return {
107
+ excluded,
108
+ enableBetweenAds: settings.enableBetweenAds,
109
+ showFirstTopicAd: settings.showFirstTopicAd,
110
+ placeholderIds: settings.placeholderIds,
111
+ intervalPosts: settings.intervalPosts,
112
+ enableCategoryAds: settings.enableCategoryAds,
113
+ showFirstCategoryAd: settings.showFirstCategoryAd,
114
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
115
+ intervalCategories: settings.intervalCategories,
116
+ enableMessageAds: settings.enableMessageAds,
117
+ showFirstMessageAd: settings.showFirstMessageAd,
118
+ messagePlaceholderIds: settings.messagePlaceholderIds,
119
+ messageIntervalPosts: settings.messageIntervalPosts,
120
+ };
121
+ }
79
122
 
80
- const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
81
- <script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
82
- <script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
83
- <script>
84
- window.ezstandalone = window.ezstandalone || {};
85
- ezstandalone.cmd = ezstandalone.cmd || [];
86
- </script>`;
123
+ function serializeInlineConfig(cfg) {
124
+ return `<script data-cfasync="false">window.__nbbEzoicCfg=${JSON.stringify(cfg).replace(/</g, '\\u003c')};</script>`;
125
+ }
87
126
 
88
- // ── Hooks ──────────────────────────────────────────────────────────────────
127
+ // ── Head injection ───────────────────────────────────────────────────────────
128
+
129
+ const HEAD_PRECONNECTS = [
130
+ '<link rel="preconnect" href="https://g.ezoic.net" crossorigin>',
131
+ '<link rel="preconnect" href="https://go.ezoic.net" crossorigin>',
132
+ '<link rel="preconnect" href="https://securepubads.g.doubleclick.net" crossorigin>',
133
+ '<link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>',
134
+ '<link rel="dns-prefetch" href="https://g.ezoic.net">',
135
+ '<link rel="dns-prefetch" href="https://securepubads.g.doubleclick.net">',
136
+ ].join('\n');
137
+
138
+ // Match v50 exactly: CMP → sa.min.js, no stubs.
139
+ // sa.min.js defines _ezaq and ezstandalone itself.
140
+ // Placing stubs before or alongside sa.min.js changes Ezoic's init path
141
+ // and causes "Timed out waiting for loadingStatus" errors.
142
+ const EZOIC_SCRIPTS = [
143
+ '<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>',
144
+ '<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>',
145
+ '<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
146
+ ].join('\n');
147
+
148
+ // ── Hooks ────────────────────────────────────────────────────────────────────
89
149
 
90
150
  plugin.onSettingsSet = function (data) {
91
- if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
151
+ if (data?.hash === SETTINGS_KEY) clearCaches();
92
152
  };
93
153
 
94
154
  plugin.addAdminNavigation = async (header) => {
95
155
  header.plugins = header.plugins || [];
96
- header.plugins.push({ route: '/plugins/ezoic-infinite', icon: 'fa-ad', name: 'Ezoic Infinite Ads' });
156
+ header.plugins.push({
157
+ route: '/plugins/ezoic-infinite',
158
+ icon: 'fa-ad',
159
+ name: 'Ezoic Infinite Ads',
160
+ });
97
161
  return header;
98
162
  };
99
163
 
100
- /**
101
- * Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
102
- *
103
- * NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
104
- * (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
105
- * Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
106
- * et est rendu via req.app.renderAsync('header', hookReturn.templateData).
107
- * On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
108
- * tout en préservant ce dernier.
109
- */
110
164
  plugin.injectEzoicHead = async (data) => {
111
165
  try {
112
166
  const settings = await getSettings();
113
167
  const uid = data.req?.uid ?? 0;
114
168
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
115
- if (!excluded) {
116
- // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
117
- data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
169
+
170
+ if (excluded) {
171
+ const cfg = buildClientConfig(settings, true);
172
+ // Minimal stub for excluded users — prevents ReferenceError if other
173
+ // scripts reference _ezaq or ezstandalone
174
+ const stub = '<script data-cfasync="false">'
175
+ + 'window._ezaq=window._ezaq||{};'
176
+ + 'window.ezstandalone=window.ezstandalone||{};'
177
+ + 'window.ezstandalone.cmd=window.ezstandalone.cmd||[];'
178
+ + '</script>';
179
+ data.templateData.customHTML =
180
+ stub + '\n' +
181
+ serializeInlineConfig(cfg) +
182
+ (data.templateData.customHTML || '');
183
+ } else {
184
+ const cfg = buildClientConfig(settings, false);
185
+ data.templateData.customHTML =
186
+ HEAD_PRECONNECTS + '\n' +
187
+ EZOIC_SCRIPTS + '\n' +
188
+ serializeInlineConfig(cfg) +
189
+ (data.templateData.customHTML || '');
118
190
  }
119
- } catch (_) {}
191
+ } catch (err) {
192
+ console.error('[ezoic-infinite] injectEzoicHead error:', err.message);
193
+ }
120
194
  return data;
121
195
  };
122
196
 
@@ -127,7 +201,8 @@ plugin.init = async ({ router, middleware }) => {
127
201
  res.render('admin/plugins/ezoic-infinite', {
128
202
  title: 'Ezoic Infinite Ads',
129
203
  ...settings,
130
- enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
204
+ enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
205
+ enableCategoryAds_checked: settings.enableCategoryAds ? 'checked' : '',
131
206
  enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
132
207
  allGroups,
133
208
  });
@@ -137,23 +212,14 @@ plugin.init = async ({ router, middleware }) => {
137
212
  router.get('/api/admin/plugins/ezoic-infinite', render);
138
213
 
139
214
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
140
- const settings = await getSettings();
141
- const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
142
- res.json({
143
- excluded,
144
- enableBetweenAds: settings.enableBetweenAds,
145
- showFirstTopicAd: settings.showFirstTopicAd,
146
- placeholderIds: settings.placeholderIds,
147
- intervalPosts: settings.intervalPosts,
148
- enableCategoryAds: settings.enableCategoryAds,
149
- showFirstCategoryAd: settings.showFirstCategoryAd,
150
- categoryPlaceholderIds: settings.categoryPlaceholderIds,
151
- intervalCategories: settings.intervalCategories,
152
- enableMessageAds: settings.enableMessageAds,
153
- showFirstMessageAd: settings.showFirstMessageAd,
154
- messagePlaceholderIds: settings.messagePlaceholderIds,
155
- messageIntervalPosts: settings.messageIntervalPosts,
156
- });
215
+ try {
216
+ const settings = await getSettings();
217
+ const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
218
+ res.json(buildClientConfig(settings, excluded));
219
+ } catch (err) {
220
+ console.error('[ezoic-infinite] config API error:', err.message);
221
+ res.status(500).json({ error: 'internal' });
222
+ }
157
223
  });
158
224
  };
159
225
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.70",
3
+ "version": "1.8.72",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -18,4 +18,4 @@
18
18
  "compatibility": "^4.0.0"
19
19
  },
20
20
  "private": false
21
- }
21
+ }