nodebb-plugin-ezoic-infinite 1.8.23 → 1.8.25

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
@@ -1,205 +1,197 @@
1
1
  'use strict';
2
2
 
3
- const meta = require.main.require('./src/meta');
3
+ const meta = require.main.require('./src/meta');
4
4
  const groups = require.main.require('./src/groups');
5
- const db = require.main.require('./src/database');
5
+ const db = require.main.require('./src/database');
6
6
 
7
7
  const SETTINGS_KEY = 'ezoic-infinite';
8
- const SETTINGS_TTL_MS = 30_000;
8
+ const plugin = {};
9
9
 
10
- const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
11
- <script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
12
- <script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
13
- <script>
14
- window.ezstandalone = window.ezstandalone || {};
15
- ezstandalone.cmd = ezstandalone.cmd || [];
16
- </script>`;
10
+ // ── Helpers ────────────────────────────────────────────────────────────────
17
11
 
18
- const DEFAULTS = Object.freeze({
19
- enableBetweenAds: true,
20
- showFirstTopicAd: false,
21
- placeholderIds: '',
22
- intervalPosts: 6,
23
- enableCategoryAds: false,
24
- showFirstCategoryAd: false,
25
- categoryPlaceholderIds: '',
26
- intervalCategories: 4,
27
- enableMessageAds: false,
28
- showFirstMessageAd: false,
29
- messagePlaceholderIds: '',
30
- messageIntervalPosts: 3,
31
- excludedGroups: [],
32
- });
33
-
34
- const CONFIG_FIELDS = Object.freeze([
35
- 'enableBetweenAds', 'showFirstTopicAd', 'placeholderIds', 'intervalPosts',
36
- 'enableCategoryAds', 'showFirstCategoryAd', 'categoryPlaceholderIds', 'intervalCategories',
37
- 'enableMessageAds', 'showFirstMessageAd', 'messagePlaceholderIds', 'messageIntervalPosts',
38
- ]);
39
-
40
- const plugin = {
41
- _settingsCache: null,
42
- _settingsCacheAt: 0,
43
- };
44
-
45
- function toBool(value, fallback = false) {
46
- if (value === undefined || value === null || value === '') return fallback;
47
- if (typeof value === 'boolean') return value;
48
- switch (String(value).trim().toLowerCase()) {
49
- case '1':
50
- case 'true':
51
- case 'on':
52
- case 'yes':
53
- return true;
54
- default:
55
- return false;
12
+ function normalizeExcludedGroups(value) {
13
+ if (!value) return [];
14
+ if (Array.isArray(value)) return value;
15
+ // NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
16
+ const s = String(value).trim();
17
+ if (s.startsWith('[')) {
18
+ try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
56
19
  }
20
+ // Fallback : séparation par virgule
21
+ return s.split(',').map(v => v.trim()).filter(Boolean);
57
22
  }
58
23
 
59
- function toPositiveInt(value, fallback) {
60
- const parsed = Number.parseInt(value, 10);
61
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
62
- }
63
-
64
- function toStringTrim(value) {
65
- return typeof value === 'string' ? value.trim() : '';
24
+ function parseBool(v, def = false) {
25
+ if (v === undefined || v === null || v === '') return def;
26
+ if (typeof v === 'boolean') return v;
27
+ const s = String(v).toLowerCase();
28
+ return s === '1' || s === 'true' || s === 'on' || s === 'yes';
66
29
  }
67
30
 
68
- function normalizeExcludedGroups(value) {
69
- if (!value) return [];
70
- if (Array.isArray(value)) {
71
- return value.map(String).map(v => v.trim()).filter(Boolean);
31
+ async function getAllGroups() {
32
+ let names = await db.getSortedSetRange('groups:createtime', 0, -1);
33
+ if (!names || !names.length) {
34
+ names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
72
35
  }
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;
41
+ }
73
42
 
74
- const raw = String(value).trim();
75
- if (!raw) return [];
43
+ // ── Settings cache (30s TTL) ────────────────────────────────────────────────
76
44
 
77
- if (raw.startsWith('[')) {
78
- try {
79
- const parsed = JSON.parse(raw);
80
- if (Array.isArray(parsed)) {
81
- return parsed.map(String).map(v => v.trim()).filter(Boolean);
82
- }
83
- } catch {}
84
- }
45
+ let _excludeCache = new Map();
46
+ const EXCLUDE_TTL = 60_000;
85
47
 
86
- return raw.split(',').map(v => v.trim()).filter(Boolean);
48
+ function excludedKey(uid, excludedGroups) {
49
+ return `${uid}|${(excludedGroups || []).join('|')}`;
87
50
  }
88
51
 
89
- function buildSettings(raw = {}) {
52
+ function buildClientConfigPayload(settings, excluded) {
90
53
  return {
91
- enableBetweenAds: toBool(raw.enableBetweenAds, DEFAULTS.enableBetweenAds),
92
- showFirstTopicAd: toBool(raw.showFirstTopicAd, DEFAULTS.showFirstTopicAd),
93
- placeholderIds: toStringTrim(raw.placeholderIds),
94
- intervalPosts: toPositiveInt(raw.intervalPosts, DEFAULTS.intervalPosts),
95
- enableCategoryAds: toBool(raw.enableCategoryAds, DEFAULTS.enableCategoryAds),
96
- showFirstCategoryAd: toBool(raw.showFirstCategoryAd, DEFAULTS.showFirstCategoryAd),
97
- categoryPlaceholderIds: toStringTrim(raw.categoryPlaceholderIds),
98
- intervalCategories: toPositiveInt(raw.intervalCategories, DEFAULTS.intervalCategories),
99
- enableMessageAds: toBool(raw.enableMessageAds, DEFAULTS.enableMessageAds),
100
- showFirstMessageAd: toBool(raw.showFirstMessageAd, DEFAULTS.showFirstMessageAd),
101
- messagePlaceholderIds: toStringTrim(raw.messagePlaceholderIds),
102
- messageIntervalPosts: toPositiveInt(raw.messageIntervalPosts, DEFAULTS.messageIntervalPosts),
103
- excludedGroups: normalizeExcludedGroups(raw.excludedGroups),
54
+ excluded,
55
+ enableBetweenAds: settings.enableBetweenAds,
56
+ showFirstTopicAd: settings.showFirstTopicAd,
57
+ placeholderIds: settings.placeholderIds,
58
+ intervalPosts: settings.intervalPosts,
59
+ enableCategoryAds: settings.enableCategoryAds,
60
+ showFirstCategoryAd: settings.showFirstCategoryAd,
61
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
62
+ intervalCategories: settings.intervalCategories,
63
+ enableMessageAds: settings.enableMessageAds,
64
+ showFirstMessageAd: settings.showFirstMessageAd,
65
+ messagePlaceholderIds: settings.messagePlaceholderIds,
66
+ messageIntervalPosts: settings.messageIntervalPosts,
104
67
  };
105
68
  }
106
69
 
107
- async function listNonPrivilegeGroups() {
108
- let names = await db.getSortedSetRange('groups:createtime', 0, -1);
109
- if (!Array.isArray(names) || !names.length) {
110
- names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
111
- }
70
+ function serializeInlineConfigScript(cfg) {
71
+ return `<script>window.__nbbEzoicCfg=${JSON.stringify(cfg).replace(/</g, '\\u003c')};</script>`;
72
+ }
112
73
 
113
- const publicNames = (names || []).filter(name => !groups.isPrivilegeGroup(name));
114
- const groupData = await groups.getGroupsData(publicNames);
74
+ const HEAD_PRECONNECTS = `<link rel="preconnect" href="https://g.ezoic.net" crossorigin>
75
+ <link rel="preconnect" href="https://go.ezoic.net" crossorigin>
76
+ <link rel="preconnect" href="https://securepubads.g.doubleclick.net" crossorigin>
77
+ <link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>
78
+ <link rel="dns-prefetch" href="https://g.ezoic.net">
79
+ <link rel="dns-prefetch" href="https://securepubads.g.doubleclick.net">`;
115
80
 
116
- return (groupData || [])
117
- .filter(group => group && group.name)
118
- .sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
119
- }
81
+ let _settingsCache = null;
82
+ let _settingsCacheAt = 0;
83
+ const SETTINGS_TTL = 30_000;
120
84
 
121
85
  async function getSettings() {
122
86
  const now = Date.now();
123
- if (plugin._settingsCache && (now - plugin._settingsCacheAt) < SETTINGS_TTL_MS) {
124
- return plugin._settingsCache;
125
- }
126
-
127
- const raw = await meta.settings.get(SETTINGS_KEY);
128
- const settings = buildSettings(raw);
129
-
130
- plugin._settingsCache = settings;
131
- plugin._settingsCacheAt = now;
132
- return settings;
87
+ if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
88
+ const s = await meta.settings.get(SETTINGS_KEY);
89
+ _settingsCacheAt = Date.now();
90
+ _settingsCache = {
91
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
92
+ showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
93
+ placeholderIds: (s.placeholderIds || '').trim(),
94
+ intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
95
+ enableCategoryAds: parseBool(s.enableCategoryAds, false),
96
+ showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
97
+ categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
98
+ intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
99
+ enableMessageAds: parseBool(s.enableMessageAds, false),
100
+ showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
101
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
102
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
103
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
104
+ };
105
+ return _settingsCache;
133
106
  }
134
107
 
135
108
  async function isUserExcluded(uid, excludedGroups) {
136
- if (!uid || !Array.isArray(excludedGroups) || !excludedGroups.length) return false;
109
+ if (!uid || !excludedGroups.length) return false;
110
+ const key = excludedKey(uid, excludedGroups);
111
+ const now = Date.now();
112
+ const hit = _excludeCache.get(key);
113
+ if (hit && (now - hit.at) < EXCLUDE_TTL) return hit.value;
137
114
 
138
- const userGroups = await groups.getUserGroups([uid]);
139
- const names = (userGroups && userGroups[0]) || [];
140
115
  const excludedSet = new Set(excludedGroups);
116
+ const userGroups = await groups.getUserGroups([uid]);
117
+ const value = (userGroups[0] || []).some(g => excludedSet.has(g.name));
141
118
 
142
- return names.some(group => group && excludedSet.has(group.name));
119
+ _excludeCache.set(key, { value, at: now });
120
+ return value;
143
121
  }
144
122
 
145
- plugin.onSettingsSet = function onSettingsSet(data) {
146
- if (data && data.hash === SETTINGS_KEY) {
147
- plugin._settingsCache = null;
148
- plugin._settingsCacheAt = 0;
149
- }
123
+ function clearCaches() {
124
+ _settingsCache = null;
125
+ _settingsCacheAt = 0;
126
+ _excludeCache = new Map();
127
+ }
128
+
129
+ // ── Scripts Ezoic ──────────────────────────────────────────────────────────
130
+
131
+ const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
132
+ <script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
133
+ <script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
134
+ <script>
135
+ window.ezstandalone = window.ezstandalone || {};
136
+ ezstandalone.cmd = ezstandalone.cmd || [];
137
+ </script>`;
138
+
139
+ // ── Hooks ──────────────────────────────────────────────────────────────────
140
+
141
+ plugin.onSettingsSet = function (data) {
142
+ if (data && data.hash === SETTINGS_KEY) clearCaches();
150
143
  };
151
144
 
152
- plugin.addAdminNavigation = async function addAdminNavigation(header) {
145
+ plugin.addAdminNavigation = async (header) => {
153
146
  header.plugins = header.plugins || [];
154
- header.plugins.push({
155
- route: '/plugins/ezoic-infinite',
156
- icon: 'fa-ad',
157
- name: 'Ezoic Infinite Ads',
158
- });
147
+ header.plugins.push({ route: '/plugins/ezoic-infinite', icon: 'fa-ad', name: 'Ezoic Infinite Ads' });
159
148
  return header;
160
149
  };
161
150
 
162
- plugin.injectEzoicHead = async function injectEzoicHead(data) {
151
+ /**
152
+ * Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
153
+ *
154
+ * NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
155
+ * (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
156
+ * Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
157
+ * et est rendu via req.app.renderAsync('header', hookReturn.templateData).
158
+ * On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
159
+ * tout en préservant ce dernier.
160
+ */
161
+ plugin.injectEzoicHead = async (data) => {
163
162
  try {
164
163
  const settings = await getSettings();
165
- const uid = data?.req?.uid || 0;
166
- if (await isUserExcluded(uid, settings.excludedGroups)) {
167
- return data;
164
+ const uid = data.req?.uid ?? 0;
165
+ const excluded = await isUserExcluded(uid, settings.excludedGroups);
166
+ if (!excluded) {
167
+ const cfgPayload = buildClientConfigPayload(settings, excluded);
168
+ data.templateData.customHTML =
169
+ HEAD_PRECONNECTS + EZOIC_SCRIPTS + serializeInlineConfigScript(cfgPayload) + (data.templateData.customHTML || '');
168
170
  }
169
-
170
- const templateData = data.templateData || (data.templateData = {});
171
- templateData.customHTML = `${EZOIC_SCRIPTS}${templateData.customHTML || ''}`;
172
- } catch {}
173
-
171
+ } catch (_) {}
174
172
  return data;
175
173
  };
176
174
 
177
- plugin.init = async function init({ router, middleware }) {
178
- async function renderAdmin(req, res) {
179
- const [settings, allGroups] = await Promise.all([
180
- getSettings(),
181
- listNonPrivilegeGroups(),
182
- ]);
183
-
175
+ plugin.init = async ({ router, middleware }) => {
176
+ async function render(req, res) {
177
+ const settings = await getSettings();
178
+ const allGroups = await getAllGroups();
184
179
  res.render('admin/plugins/ezoic-infinite', {
185
180
  title: 'Ezoic Infinite Ads',
186
181
  ...settings,
187
182
  enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
188
- enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
183
+ enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
189
184
  allGroups,
190
185
  });
191
186
  }
192
187
 
193
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
194
- router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
188
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
189
+ router.get('/api/admin/plugins/ezoic-infinite', render);
195
190
 
196
191
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
197
192
  const settings = await getSettings();
198
193
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
199
-
200
- const payload = { excluded };
201
- for (const key of CONFIG_FIELDS) payload[key] = settings[key];
202
- res.json(payload);
194
+ res.json(buildClientConfigPayload(settings, excluded));
203
195
  });
204
196
  };
205
197
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.23",
3
+ "version": "1.8.25",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/admin.js CHANGED
@@ -1,44 +1,28 @@
1
1
  /* globals ajaxify */
2
2
  'use strict';
3
3
 
4
- (function initAdminEzoicSettings() {
5
- const FORM_SELECTOR = '.ezoic-infinite-settings';
6
- const SAVE_SELECTOR = '#save';
7
- const SETTINGS_KEY = 'ezoic-infinite';
8
- const EVENT_NS = '.ezoicInfinite';
9
-
10
- function showSaved(alerts) {
11
- if (alerts && typeof alerts.success === 'function') {
12
- alerts.success('[[admin/settings:saved]]');
13
- return;
14
- }
15
- if (window.app && typeof window.app.alertSuccess === 'function') {
16
- window.app.alertSuccess('[[admin/settings:saved]]');
17
- }
18
- }
4
+ (function () {
5
+ function init() {
6
+ const $form = $('.ezoic-infinite-settings');
7
+ if (!$form.length) return;
19
8
 
20
- function bind(Settings, alerts, $form) {
21
- Settings.load(SETTINGS_KEY, $form);
9
+ require(['settings', 'alerts'], function (Settings, alerts) {
10
+ Settings.load('ezoic-infinite', $form);
22
11
 
23
- $(SAVE_SELECTOR)
24
- .off(`click${EVENT_NS}`)
25
- .on(`click${EVENT_NS}`, function onSave(e) {
12
+ $('#save').off('click.ezoicInfinite').on('click.ezoicInfinite', function (e) {
26
13
  e.preventDefault();
27
- Settings.save(SETTINGS_KEY, $form, function onSaved() {
28
- showSaved(alerts);
14
+
15
+ Settings.save('ezoic-infinite', $form, function () {
16
+ if (alerts && typeof alerts.success === 'function') {
17
+ alerts.success('[[admin/settings:saved]]');
18
+ } else if (window.app && typeof window.app.alertSuccess === 'function') {
19
+ window.app.alertSuccess('[[admin/settings:saved]]');
20
+ }
29
21
  });
30
22
  });
31
- }
32
-
33
- function boot() {
34
- const $form = $(FORM_SELECTOR);
35
- if (!$form.length) return;
36
-
37
- require(['settings', 'alerts'], function onModules(Settings, alerts) {
38
- bind(Settings, alerts, $form);
39
23
  });
40
24
  }
41
25
 
42
- $(document).ready(boot);
43
- $(window).on('action:ajaxify.end', boot);
26
+ $(document).ready(init);
27
+ $(window).on('action:ajaxify.end', init);
44
28
  })();