@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 +17 -0
- package/package.json +1 -1
- package/public/js/scripts.js +122 -23
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.
|
|
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": {},
|
package/public/js/scripts.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
257
|
+
bindLoginRetryReset();
|
|
159
258
|
}
|
|
160
259
|
})
|
|
161
260
|
.catch(function () {
|