@arnaudw38/nodebb-plugin-spam-be-gone 1.0.6 → 1.0.8

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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.0.9] - 2026-02-25
6
+
7
+ ### Fixed
8
+ - Fixed login success flow causing an unnecessary Turnstile refresh shortly before redirect.
9
+ - Login retry reset now triggers only when a login error is actually detected on the page.
10
+
11
+ ### Improved
12
+ - Added navigation-aware cleanup (`beforeunload`/`pagehide`) to avoid reset races during successful login redirects.
13
+ - Reworked login retry watcher to use short-lived DOM error detection + fallback polling instead of blind timed resets.
14
+
15
+ ## [1.0.8] - 2026-02-25
16
+
17
+ ### Fixed
18
+ - Fixed Turnstile login retry reset in themes/login flows where previous click/enter hooks did not reliably trigger the widget reset.
19
+ - Made login retry reset logic target the current Turnstile container dynamically instead of relying on a stale target id.
20
+ - Added a fallback reset trigger when login error alerts are rendered on the login page.
21
+
5
22
  ## [1.0.7] - 2026-02-25
6
23
 
7
24
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnaudw38/nodebb-plugin-spam-be-gone",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Anti-spam plugin for NodeBB 4.x using Akismet, StopForumSpam API, ProjectHoneyPot, and Cloudflare Turnstile (Turnstile-only fork)",
5
5
  "main": "library.js",
6
6
  "scripts": {},
@@ -9,6 +9,9 @@ $(function () {
9
9
  widgets: {},
10
10
  loginBindingsAttached: false,
11
11
  loginResetTimers: [],
12
+ loginErrorObserver: null,
13
+ loginAttemptArmed: false,
14
+ navigatingAway: false,
12
15
  };
13
16
 
14
17
  function getTurnstileArgs() {
@@ -35,58 +38,153 @@ $(function () {
35
38
  return input && input.value;
36
39
  }
37
40
 
41
+ function getCurrentLoginTurnstileTargetId() {
42
+ var args = getTurnstileArgs();
43
+ return args && args.targetId;
44
+ }
45
+
38
46
  function resetTurnstileWidget(targetId) {
39
47
  if (typeof turnstile === 'undefined') {
40
- return;
48
+ return false;
49
+ }
50
+ targetId = targetId || getCurrentLoginTurnstileTargetId();
51
+ if (!targetId) {
52
+ return false;
41
53
  }
42
54
  var widgetId = turnstileState.widgets[targetId];
43
55
  if (widgetId === undefined || widgetId === null) {
44
- return;
56
+ return false;
45
57
  }
46
58
  try {
47
59
  turnstile.reset(widgetId);
60
+ return true;
48
61
  } catch (err) {
49
62
  // Ignore reset errors when the widget is already destroyed by navigation.
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function onLoginPageNow() {
68
+ return !ajaxify.data || !ajaxify.data.tpl_url || ajaxify.data.tpl_url === 'login';
69
+ }
70
+
71
+ function hasLoginErrorVisible() {
72
+ var selectors = [
73
+ '.alert-danger',
74
+ '.alert-error',
75
+ '[component="alerts"] .alert.alert-danger',
76
+ '[component="alerts/error"]',
77
+ '.login-error',
78
+ '.text-danger',
79
+ ];
80
+ for (var i = 0; i < selectors.length; i += 1) {
81
+ var nodes = document.querySelectorAll(selectors[i]);
82
+ for (var j = 0; j < nodes.length; j += 1) {
83
+ var n = nodes[j];
84
+ if (n && (n.offsetParent !== null || (n.textContent && n.textContent.trim()))) {
85
+ return true;
86
+ }
87
+ }
50
88
  }
89
+ return false;
51
90
  }
52
91
 
53
- function scheduleLoginTurnstileReset(targetId) {
54
- // Turnstile tokens are single-use. On failed login, NodeBB keeps the user on the
55
- // same page, so we refresh the widget shortly after submit to allow a retry.
92
+ function clearLoginResetTimers() {
56
93
  turnstileState.loginResetTimers.forEach(function (timerId) {
57
94
  window.clearTimeout(timerId);
58
95
  });
59
96
  turnstileState.loginResetTimers = [];
97
+ }
60
98
 
61
- [1000, 2200, 4000].forEach(function (delay) {
99
+ function disarmLoginAttemptWatch() {
100
+ turnstileState.loginAttemptArmed = false;
101
+ clearLoginResetTimers();
102
+ if (turnstileState.loginErrorObserver) {
103
+ try {
104
+ turnstileState.loginErrorObserver.disconnect();
105
+ } catch (err) {
106
+ // noop
107
+ }
108
+ turnstileState.loginErrorObserver = null;
109
+ }
110
+ }
111
+
112
+ function resetLoginTurnstileOnDetectedError() {
113
+ if (!turnstileState.loginAttemptArmed) {
114
+ return;
115
+ }
116
+ if (turnstileState.navigatingAway || !onLoginPageNow()) {
117
+ disarmLoginAttemptWatch();
118
+ return;
119
+ }
120
+ if (!hasLoginErrorVisible()) {
121
+ return;
122
+ }
123
+ var targetId = getCurrentLoginTurnstileTargetId();
124
+ if (targetId && document.getElementById(targetId)) {
125
+ resetTurnstileWidget(targetId);
126
+ }
127
+ disarmLoginAttemptWatch();
128
+ }
129
+
130
+ function scheduleLoginTurnstileReset() {
131
+ // Arm a short-lived watcher and reset only when an actual login error is rendered.
132
+ turnstileState.navigatingAway = false;
133
+ disarmLoginAttemptWatch();
134
+ turnstileState.loginAttemptArmed = true;
135
+
136
+ if (window.MutationObserver) {
137
+ turnstileState.loginErrorObserver = new MutationObserver(function () {
138
+ resetLoginTurnstileOnDetectedError();
139
+ });
140
+ turnstileState.loginErrorObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
141
+ }
142
+
143
+ // Poll briefly as a fallback for themes that toggle classes/text without obvious DOM insertions.
144
+ [250, 600, 1000, 1600, 2400, 3400].forEach(function (delay) {
62
145
  var timerId = window.setTimeout(function () {
63
- var onLoginPage = !ajaxify.data || !ajaxify.data.tpl_url || ajaxify.data.tpl_url === 'login';
64
- if (!onLoginPage) {
65
- return;
66
- }
67
- if (!document.getElementById(targetId)) {
68
- return;
69
- }
70
- resetTurnstileWidget(targetId);
146
+ resetLoginTurnstileOnDetectedError();
71
147
  }, delay);
72
148
  turnstileState.loginResetTimers.push(timerId);
73
149
  });
150
+
151
+ // Stop watching after a while to avoid stale observers.
152
+ var cleanupTimerId = window.setTimeout(function () {
153
+ if (!turnstileState.navigatingAway && onLoginPageNow()) {
154
+ // no-op: just let the user try again manually if no explicit error was detected
155
+ }
156
+ disarmLoginAttemptWatch();
157
+ }, 6000);
158
+ turnstileState.loginResetTimers.push(cleanupTimerId);
74
159
  }
75
160
 
76
- function bindLoginRetryReset(targetId) {
161
+ window.addEventListener('beforeunload', function () {
162
+ turnstileState.navigatingAway = true;
163
+ disarmLoginAttemptWatch();
164
+ });
165
+ window.addEventListener('pagehide', function () {
166
+ turnstileState.navigatingAway = true;
167
+ disarmLoginAttemptWatch();
168
+ });
169
+
170
+ function bindLoginRetryReset() {
77
171
  if (turnstileState.loginBindingsAttached) {
78
172
  return;
79
173
  }
80
174
  turnstileState.loginBindingsAttached = true;
81
175
 
82
176
  function loginSubmitTrigger() {
83
- scheduleLoginTurnstileReset(targetId);
177
+ scheduleLoginTurnstileReset();
84
178
  }
85
179
 
86
180
  // Native capture listeners only (avoid duplicate handlers/race conditions with jQuery delegates).
87
181
  document.addEventListener('click', function (ev) {
88
- var btn = ev.target && ev.target.closest ? ev.target.closest('[component="login/submit"], #login [type="submit"], form[action*="/login"] [type="submit"]') : null;
89
- if (btn) {
182
+ var btn = ev.target && ev.target.closest ? ev.target.closest('[component="login/submit"], #login [type="submit"], form[action*="/login"] [type="submit"], form[data-action="login"] [type="submit"], button[type="submit"]') : null;
183
+ if (!btn) {
184
+ return;
185
+ }
186
+ var inLogin = btn.closest && btn.closest('#login, form[action*="/login"], form[data-action="login"], [component="login"]');
187
+ if (inLogin || (ajaxify.data && ajaxify.data.tpl_url === 'login')) {
90
188
  loginSubmitTrigger();
91
189
  }
92
190
  }, true);
@@ -100,14 +198,15 @@ $(function () {
100
198
  if (!el || !el.closest) {
101
199
  return;
102
200
  }
103
- var inLogin = el.closest('#login, form[action*="/login"], form[data-action="login"]');
104
- if (inLogin) {
201
+ var inLogin = el.closest('#login, form[action*="/login"], form[data-action="login"], [component="login"]');
202
+ if (inLogin || (ajaxify.data && ajaxify.data.tpl_url === 'login')) {
105
203
  loginSubmitTrigger();
106
204
  }
107
205
  }, true);
206
+
108
207
  }
109
208
 
110
- function renderTurnstileIfNeeded(isLoginPage) {
209
+ function renderTurnstileIfNeeded(isLoginPage) {
111
210
  var args = getTurnstileArgs();
112
211
  if (!args || (isLoginPage && !args.addLoginTurnstile)) {
113
212
  return;
@@ -125,7 +224,7 @@ $(function () {
125
224
 
126
225
  if (target.dataset.turnstileRendered === '1') {
127
226
  if (isLoginPage) {
128
- bindLoginRetryReset(args.targetId);
227
+ bindLoginRetryReset();
129
228
  }
130
229
  return;
131
230
  }
@@ -155,7 +254,7 @@ $(function () {
155
254
  target.dataset.turnstileRendered = '1';
156
255
  turnstileState.widgets[args.targetId] = widgetId;
157
256
  if (isLoginPage) {
158
- bindLoginRetryReset(args.targetId);
257
+ bindLoginRetryReset();
159
258
  }
160
259
  })
161
260
  .catch(function () {