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

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,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.0.8] - 2026-02-25
6
+
7
+ ### Fixed
8
+ - Fixed Turnstile login retry reset in themes/login flows where previous click/enter hooks did not reliably trigger the widget reset.
9
+ - Made login retry reset logic target the current Turnstile container dynamically instead of relying on a stale target id.
10
+ - Added a fallback reset trigger when login error alerts are rendered on the login page.
11
+
5
12
  ## [1.0.7] - 2026-02-25
6
13
 
7
14
  ### 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.7",
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": {},
@@ -35,36 +35,48 @@ $(function () {
35
35
  return input && input.value;
36
36
  }
37
37
 
38
+ function getCurrentLoginTurnstileTargetId() {
39
+ var args = getTurnstileArgs();
40
+ return args && args.targetId;
41
+ }
42
+
38
43
  function resetTurnstileWidget(targetId) {
39
44
  if (typeof turnstile === 'undefined') {
40
- return;
45
+ return false;
46
+ }
47
+ targetId = targetId || getCurrentLoginTurnstileTargetId();
48
+ if (!targetId) {
49
+ return false;
41
50
  }
42
51
  var widgetId = turnstileState.widgets[targetId];
43
52
  if (widgetId === undefined || widgetId === null) {
44
- return;
53
+ return false;
45
54
  }
46
55
  try {
47
56
  turnstile.reset(widgetId);
57
+ return true;
48
58
  } catch (err) {
49
59
  // Ignore reset errors when the widget is already destroyed by navigation.
60
+ return false;
50
61
  }
51
62
  }
52
63
 
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.
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.
56
67
  turnstileState.loginResetTimers.forEach(function (timerId) {
57
68
  window.clearTimeout(timerId);
58
69
  });
59
70
  turnstileState.loginResetTimers = [];
60
71
 
61
- [1000, 2200, 4000].forEach(function (delay) {
72
+ [700, 1400, 2600, 4200].forEach(function (delay) {
62
73
  var timerId = window.setTimeout(function () {
63
74
  var onLoginPage = !ajaxify.data || !ajaxify.data.tpl_url || ajaxify.data.tpl_url === 'login';
64
75
  if (!onLoginPage) {
65
76
  return;
66
77
  }
67
- if (!document.getElementById(targetId)) {
78
+ var targetId = getCurrentLoginTurnstileTargetId();
79
+ if (!targetId || !document.getElementById(targetId)) {
68
80
  return;
69
81
  }
70
82
  resetTurnstileWidget(targetId);
@@ -73,20 +85,24 @@ $(function () {
73
85
  });
74
86
  }
75
87
 
76
- function bindLoginRetryReset(targetId) {
88
+ function bindLoginRetryReset() {
77
89
  if (turnstileState.loginBindingsAttached) {
78
90
  return;
79
91
  }
80
92
  turnstileState.loginBindingsAttached = true;
81
93
 
82
94
  function loginSubmitTrigger() {
83
- scheduleLoginTurnstileReset(targetId);
95
+ scheduleLoginTurnstileReset();
84
96
  }
85
97
 
86
98
  // Native capture listeners only (avoid duplicate handlers/race conditions with jQuery delegates).
87
99
  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) {
100
+ 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;
101
+ if (!btn) {
102
+ return;
103
+ }
104
+ var inLogin = btn.closest && btn.closest('#login, form[action*="/login"], form[data-action="login"], [component="login"]');
105
+ if (inLogin || (ajaxify.data && ajaxify.data.tpl_url === 'login')) {
90
106
  loginSubmitTrigger();
91
107
  }
92
108
  }, true);
@@ -100,14 +116,40 @@ $(function () {
100
116
  if (!el || !el.closest) {
101
117
  return;
102
118
  }
103
- var inLogin = el.closest('#login, form[action*="/login"], form[data-action="login"]');
104
- if (inLogin) {
119
+ var inLogin = el.closest('#login, form[action*="/login"], form[data-action="login"], [component="login"]');
120
+ if (inLogin || (ajaxify.data && ajaxify.data.tpl_url === 'login')) {
105
121
  loginSubmitTrigger();
106
122
  }
107
123
  }, true);
124
+
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
+ }
108
150
  }
109
151
 
110
- function renderTurnstileIfNeeded(isLoginPage) {
152
+ function renderTurnstileIfNeeded(isLoginPage) {
111
153
  var args = getTurnstileArgs();
112
154
  if (!args || (isLoginPage && !args.addLoginTurnstile)) {
113
155
  return;
@@ -125,7 +167,7 @@ $(function () {
125
167
 
126
168
  if (target.dataset.turnstileRendered === '1') {
127
169
  if (isLoginPage) {
128
- bindLoginRetryReset(args.targetId);
170
+ bindLoginRetryReset();
129
171
  }
130
172
  return;
131
173
  }
@@ -155,7 +197,7 @@ $(function () {
155
197
  target.dataset.turnstileRendered = '1';
156
198
  turnstileState.widgets[args.targetId] = widgetId;
157
199
  if (isLoginPage) {
158
- bindLoginRetryReset(args.targetId);
200
+ bindLoginRetryReset();
159
201
  }
160
202
  })
161
203
  .catch(function () {