@arnaudw38/nodebb-plugin-spam-be-gone 1.0.7 → 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,16 @@
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
+
5
15
  ## [1.0.8] - 2026-02-25
6
16
 
7
17
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnaudw38/nodebb-plugin-spam-be-gone",
3
- "version": "1.0.7",
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() {
@@ -61,30 +64,109 @@ $(function () {
61
64
  }
62
65
  }
63
66
 
64
- function scheduleLoginTurnstileReset() {
65
- // Turnstile tokens are single-use. On failed login, NodeBB often keeps the user on the
66
- // same page and may handle submission via AJAX/click handlers. We try multiple delayed resets.
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
+ }
88
+ }
89
+ return false;
90
+ }
91
+
92
+ function clearLoginResetTimers() {
67
93
  turnstileState.loginResetTimers.forEach(function (timerId) {
68
94
  window.clearTimeout(timerId);
69
95
  });
70
96
  turnstileState.loginResetTimers = [];
97
+ }
98
+
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
+ }
71
142
 
72
- [700, 1400, 2600, 4200].forEach(function (delay) {
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) {
73
145
  var timerId = window.setTimeout(function () {
74
- var onLoginPage = !ajaxify.data || !ajaxify.data.tpl_url || ajaxify.data.tpl_url === 'login';
75
- if (!onLoginPage) {
76
- return;
77
- }
78
- var targetId = getCurrentLoginTurnstileTargetId();
79
- if (!targetId || !document.getElementById(targetId)) {
80
- return;
81
- }
82
- resetTurnstileWidget(targetId);
146
+ resetLoginTurnstileOnDetectedError();
83
147
  }, delay);
84
148
  turnstileState.loginResetTimers.push(timerId);
85
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);
86
159
  }
87
160
 
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
+
88
170
  function bindLoginRetryReset() {
89
171
  if (turnstileState.loginBindingsAttached) {
90
172
  return;
@@ -122,31 +204,6 @@ $(function () {
122
204
  }
123
205
  }, true);
124
206
 
125
- // Extra safety: reset again when a login error alert appears (covers AJAX flows that do not
126
- // reliably trigger the expected submit/click path in some themes).
127
- if (window.MutationObserver) {
128
- var observer = new MutationObserver(function (mutations) {
129
- var onLoginPage = !ajaxify.data || !ajaxify.data.tpl_url || ajaxify.data.tpl_url === 'login';
130
- if (!onLoginPage) {
131
- return;
132
- }
133
- for (var i = 0; i < mutations.length; i += 1) {
134
- for (var j = 0; j < mutations[i].addedNodes.length; j += 1) {
135
- var n = mutations[i].addedNodes[j];
136
- if (!n || n.nodeType !== 1) {
137
- continue;
138
- }
139
- var hasError = (n.matches && n.matches('.alert-danger, .alert-error, .text-danger, [component="alerts/error"]')) ||
140
- (n.querySelector && n.querySelector('.alert-danger, .alert-error, .text-danger, [component="alerts/error"]'));
141
- if (hasError) {
142
- scheduleLoginTurnstileReset();
143
- return;
144
- }
145
- }
146
- }
147
- });
148
- observer.observe(document.body, { childList: true, subtree: true });
149
- }
150
207
  }
151
208
 
152
209
  function renderTurnstileIfNeeded(isLoginPage) {