@arnaudw38/nodebb-plugin-spam-be-gone 1.0.5 → 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,28 @@
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
+
12
+ ## [1.0.7] - 2026-02-25
13
+
14
+ ### Fixed
15
+ - Login retry Turnstile reset now uses native event listeners only (removed jQuery delegated login listeners).
16
+ - Prevented duplicate retry-reset triggers caused by mixed jQuery + native listeners on some NodeBB themes.
17
+
18
+ ### Changed
19
+ - Simplified login retry detection to click + Enter key flows (removed submit listener).
20
+
21
+ ## [1.0.6] - 2026-02-25
22
+
23
+ ### Fixed
24
+ - Fixed login retry Turnstile reset trigger on NodeBB login flows that do not emit the expected jQuery submit/click events.
25
+ - Prevented missing reset after failed login without page reload by adding capture-phase submit/click/Enter listeners.
26
+
5
27
  ## [1.0.5] - 2026-02-25
6
28
 
7
29
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnaudw38/nodebb-plugin-spam-be-gone",
3
- "version": "1.0.5",
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": {},
@@ -8,6 +8,7 @@ $(function () {
8
8
  var turnstileState = {
9
9
  widgets: {},
10
10
  loginBindingsAttached: false,
11
+ loginResetTimers: [],
11
12
  };
12
13
 
13
14
  function getTurnstileArgs() {
@@ -34,59 +35,121 @@ $(function () {
34
35
  return input && input.value;
35
36
  }
36
37
 
38
+ function getCurrentLoginTurnstileTargetId() {
39
+ var args = getTurnstileArgs();
40
+ return args && args.targetId;
41
+ }
42
+
37
43
  function resetTurnstileWidget(targetId) {
38
44
  if (typeof turnstile === 'undefined') {
39
- return;
45
+ return false;
46
+ }
47
+ targetId = targetId || getCurrentLoginTurnstileTargetId();
48
+ if (!targetId) {
49
+ return false;
40
50
  }
41
51
  var widgetId = turnstileState.widgets[targetId];
42
52
  if (widgetId === undefined || widgetId === null) {
43
- return;
53
+ return false;
44
54
  }
45
55
  try {
46
56
  turnstile.reset(widgetId);
57
+ return true;
47
58
  } catch (err) {
48
59
  // Ignore reset errors when the widget is already destroyed by navigation.
60
+ return false;
49
61
  }
50
62
  }
51
63
 
52
- function scheduleLoginTurnstileReset(targetId) {
53
- // Turnstile tokens are single-use. On failed login, NodeBB keeps the user on the
54
- // same page, so we refresh the widget shortly after submit to allow a retry.
55
- [1200, 3000].forEach(function (delay) {
56
- window.setTimeout(function () {
57
- if (ajaxify.data && ajaxify.data.template && ajaxify.data.template.name && ajaxify.data.template.name !== 'login') {
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
+ turnstileState.loginResetTimers.forEach(function (timerId) {
68
+ window.clearTimeout(timerId);
69
+ });
70
+ turnstileState.loginResetTimers = [];
71
+
72
+ [700, 1400, 2600, 4200].forEach(function (delay) {
73
+ var timerId = window.setTimeout(function () {
74
+ var onLoginPage = !ajaxify.data || !ajaxify.data.tpl_url || ajaxify.data.tpl_url === 'login';
75
+ if (!onLoginPage) {
58
76
  return;
59
77
  }
60
- if (!document.getElementById(targetId)) {
78
+ var targetId = getCurrentLoginTurnstileTargetId();
79
+ if (!targetId || !document.getElementById(targetId)) {
61
80
  return;
62
81
  }
63
- // Reset unconditionally if we're still on the login page after submit.
64
- // Tokens are single-use and may already have been cleared by the form logic
65
- // when a login attempt fails, so checking the hidden input is unreliable.
66
82
  resetTurnstileWidget(targetId);
67
83
  }, delay);
84
+ turnstileState.loginResetTimers.push(timerId);
68
85
  });
69
86
  }
70
87
 
71
- function bindLoginRetryReset(targetId) {
88
+ function bindLoginRetryReset() {
72
89
  if (turnstileState.loginBindingsAttached) {
73
90
  return;
74
91
  }
75
92
  turnstileState.loginBindingsAttached = true;
76
93
 
77
- $(document)
78
- .off('submit.spamBeGoneTurnstileLogin')
79
- .on('submit.spamBeGoneTurnstileLogin', 'form[action="/login"], form[data-action="login"], #login', function () {
80
- scheduleLoginTurnstileReset(targetId);
81
- })
82
- .off('click.spamBeGoneTurnstileLogin')
83
- .on('click.spamBeGoneTurnstileLogin', '[component="login/submit"]', function () {
84
- scheduleLoginTurnstileReset(targetId);
85
- })
86
- .off('input.spamBeGoneTurnstileLogin change.spamBeGoneTurnstileLogin');
94
+ function loginSubmitTrigger() {
95
+ scheduleLoginTurnstileReset();
96
+ }
97
+
98
+ // Native capture listeners only (avoid duplicate handlers/race conditions with jQuery delegates).
99
+ document.addEventListener('click', function (ev) {
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')) {
106
+ loginSubmitTrigger();
107
+ }
108
+ }, true);
109
+
110
+ // Enter key in login fields for themes that trigger login without a click event.
111
+ document.addEventListener('keydown', function (ev) {
112
+ if (ev.key !== 'Enter') {
113
+ return;
114
+ }
115
+ var el = ev.target;
116
+ if (!el || !el.closest) {
117
+ return;
118
+ }
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')) {
121
+ loginSubmitTrigger();
122
+ }
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
+ }
87
150
  }
88
151
 
89
- function renderTurnstileIfNeeded(isLoginPage) {
152
+ function renderTurnstileIfNeeded(isLoginPage) {
90
153
  var args = getTurnstileArgs();
91
154
  if (!args || (isLoginPage && !args.addLoginTurnstile)) {
92
155
  return;
@@ -104,7 +167,7 @@ $(function () {
104
167
 
105
168
  if (target.dataset.turnstileRendered === '1') {
106
169
  if (isLoginPage) {
107
- bindLoginRetryReset(args.targetId);
170
+ bindLoginRetryReset();
108
171
  }
109
172
  return;
110
173
  }
@@ -134,7 +197,7 @@ $(function () {
134
197
  target.dataset.turnstileRendered = '1';
135
198
  turnstileState.widgets[args.targetId] = widgetId;
136
199
  if (isLoginPage) {
137
- bindLoginRetryReset(args.targetId);
200
+ bindLoginRetryReset();
138
201
  }
139
202
  })
140
203
  .catch(function () {