@arnaudw38/nodebb-plugin-spam-be-gone 1.0.9 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnaudw38/nodebb-plugin-spam-be-gone",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Anti-spam plugin for NodeBB 4.x using Akismet, StopForumSpam, ProjectHoneyPot, and Cloudflare Turnstile (Turnstile-only fork)",
5
5
  "main": "library.js",
6
6
  "scripts": {},
@@ -5,11 +5,22 @@
5
5
  $(function () {
6
6
  var pluginName = 'spam-be-gone';
7
7
  var turnstileScriptUrl = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
8
+ var loginTurnstile = {
9
+ widgetId: null,
10
+ observer: null,
11
+ watchTimer: null,
12
+ isWatching: false,
13
+ };
8
14
 
9
15
  function getTurnstileArgs() {
10
16
  return ajaxify.data && ajaxify.data.turnstileArgs;
11
17
  }
12
18
 
19
+ function isLoginPage() {
20
+ return !!(ajaxify.data && ajaxify.data.template && ajaxify.data.template.name === 'login') ||
21
+ (window.location.pathname && window.location.pathname.indexOf('/login') !== -1);
22
+ }
23
+
13
24
  function injectScriptOnce(src) {
14
25
  if (document.querySelector('script[src*="turnstile/v0/api.js"]')) {
15
26
  return Promise.resolve();
@@ -25,9 +36,140 @@ $(function () {
25
36
  });
26
37
  }
27
38
 
28
- function renderTurnstileIfNeeded(isLoginPage) {
39
+ function hasLoginErrorVisible() {
40
+ if (!isLoginPage()) {
41
+ return false;
42
+ }
43
+ var selectors = [
44
+ '.alert.alert-danger',
45
+ '.alert-danger',
46
+ '[component="alerts"] .alert-danger',
47
+ '[component="login"] .alert-danger',
48
+ '[component="login/login"] .alert-danger',
49
+ ];
50
+ return selectors.some(function (sel) {
51
+ var nodes = document.querySelectorAll(sel);
52
+ return Array.prototype.some.call(nodes, function (node) {
53
+ return !!(node && node.offsetParent !== null && node.textContent && node.textContent.trim());
54
+ });
55
+ });
56
+ }
57
+
58
+ function clearLoginErrorWatch() {
59
+ if (loginTurnstile.observer) {
60
+ loginTurnstile.observer.disconnect();
61
+ loginTurnstile.observer = null;
62
+ }
63
+ if (loginTurnstile.watchTimer) {
64
+ clearTimeout(loginTurnstile.watchTimer);
65
+ loginTurnstile.watchTimer = null;
66
+ }
67
+ loginTurnstile.isWatching = false;
68
+ }
69
+
70
+ function resetLoginTurnstile() {
71
+ if (!isLoginPage() || typeof turnstile === 'undefined' || !turnstile || typeof turnstile.reset !== 'function') {
72
+ return;
73
+ }
29
74
  var args = getTurnstileArgs();
30
- if (!args || (isLoginPage && !args.addLoginTurnstile)) {
75
+ var target = args && args.targetId ? document.getElementById(args.targetId) : null;
76
+ if (!target) {
77
+ return;
78
+ }
79
+ try {
80
+ if (loginTurnstile.widgetId !== null && loginTurnstile.widgetId !== undefined) {
81
+ turnstile.reset(loginTurnstile.widgetId);
82
+ } else {
83
+ turnstile.reset(target);
84
+ }
85
+ } catch (err) {
86
+ // ignore reset errors to avoid blocking login UX
87
+ }
88
+ }
89
+
90
+ function watchForLoginFailureAndReset() {
91
+ if (!isLoginPage() || loginTurnstile.isWatching) {
92
+ return;
93
+ }
94
+ if (typeof turnstile === 'undefined' || !turnstile) {
95
+ return;
96
+ }
97
+
98
+ loginTurnstile.isWatching = true;
99
+
100
+ // Fast path in case an error is already present
101
+ if (hasLoginErrorVisible()) {
102
+ resetLoginTurnstile();
103
+ clearLoginErrorWatch();
104
+ return;
105
+ }
106
+
107
+ var body = document.body;
108
+ if (body && typeof MutationObserver !== 'undefined') {
109
+ loginTurnstile.observer = new MutationObserver(function () {
110
+ if (hasLoginErrorVisible()) {
111
+ resetLoginTurnstile();
112
+ clearLoginErrorWatch();
113
+ }
114
+ });
115
+ loginTurnstile.observer.observe(body, { childList: true, subtree: true, attributes: true });
116
+ }
117
+
118
+ // Auto-cleanup if success navigation happens or no error is shown
119
+ loginTurnstile.watchTimer = window.setTimeout(function () {
120
+ clearLoginErrorWatch();
121
+ }, 5000);
122
+ }
123
+
124
+ function bindLoginRetryResetHandlers() {
125
+ document.removeEventListener('click', onLoginIntentCapture, true);
126
+ document.removeEventListener('keydown', onLoginKeydownCapture, true);
127
+ window.removeEventListener('beforeunload', clearLoginErrorWatch);
128
+ window.removeEventListener('pagehide', clearLoginErrorWatch);
129
+
130
+ document.addEventListener('click', onLoginIntentCapture, true);
131
+ document.addEventListener('keydown', onLoginKeydownCapture, true);
132
+ window.addEventListener('beforeunload', clearLoginErrorWatch);
133
+ window.addEventListener('pagehide', clearLoginErrorWatch);
134
+ }
135
+
136
+ function onLoginIntentCapture(ev) {
137
+ if (!isLoginPage()) {
138
+ return;
139
+ }
140
+ var el = ev.target && ev.target.closest ? ev.target.closest('button, input[type="submit"], a') : null;
141
+ if (!el) {
142
+ return;
143
+ }
144
+ var text = ((el.textContent || '') + ' ' + (el.value || '')).toLowerCase();
145
+ var component = (el.getAttribute && (el.getAttribute('component') || '')) || '';
146
+ var action = (el.getAttribute && (el.getAttribute('data-action') || '')) || '';
147
+ var type = (el.getAttribute && (el.getAttribute('type') || '')) || '';
148
+ var maybeLogin = component.indexOf('login') !== -1 || action.toLowerCase().indexOf('login') !== -1 || text.indexOf('log in') !== -1 || text.indexOf('login') !== -1 || type === 'submit';
149
+ if (!maybeLogin) {
150
+ return;
151
+ }
152
+ window.setTimeout(watchForLoginFailureAndReset, 0);
153
+ }
154
+
155
+ function onLoginKeydownCapture(ev) {
156
+ if (!isLoginPage() || ev.key !== 'Enter') {
157
+ return;
158
+ }
159
+ var input = ev.target;
160
+ if (!input || input.tagName !== 'INPUT') {
161
+ return;
162
+ }
163
+ var type = (input.type || '').toLowerCase();
164
+ var name = (input.name || '').toLowerCase();
165
+ if (type === 'password' || name === 'password' || name === 'username' || name === 'email') {
166
+ window.setTimeout(watchForLoginFailureAndReset, 0);
167
+ }
168
+ }
169
+
170
+ function renderTurnstileIfNeeded(isLoginTpl) {
171
+ var args = getTurnstileArgs();
172
+ if (!args || (isLoginTpl && !args.addLoginTurnstile)) {
31
173
  return;
32
174
  }
33
175
 
@@ -40,7 +182,7 @@ $(function () {
40
182
  if (!target || target.dataset.turnstileRendered === '1') {
41
183
  return;
42
184
  }
43
- turnstile.render('#' + args.targetId, {
185
+ var widgetId = turnstile.render('#' + args.targetId, {
44
186
  sitekey: args.siteKey,
45
187
  theme: args.theme || 'auto',
46
188
  size: args.size || 'normal',
@@ -55,8 +197,22 @@ $(function () {
55
197
  'error-callback': function () {
56
198
  require(['alerts'], function (alerts) { alerts.error('[[spam-be-gone:captcha-not-verified]]'); });
57
199
  },
200
+ 'expired-callback': function () {
201
+ if (isLoginTpl) {
202
+ clearLoginErrorWatch();
203
+ }
204
+ },
205
+ 'timeout-callback': function () {
206
+ if (isLoginTpl) {
207
+ clearLoginErrorWatch();
208
+ }
209
+ },
58
210
  });
59
211
  target.dataset.turnstileRendered = '1';
212
+ if (isLoginTpl) {
213
+ loginTurnstile.widgetId = widgetId;
214
+ bindLoginRetryResetHandlers();
215
+ }
60
216
  })
61
217
  .catch(function () {
62
218
  require(['alerts'], function (alerts) { alerts.error('Failed to load Cloudflare Turnstile'); });
@@ -94,6 +250,7 @@ $(function () {
94
250
  }
95
251
 
96
252
  $(window).on('action:ajaxify.end', function (evt, data) {
253
+ clearLoginErrorWatch();
97
254
  switch (data.tpl_url) {
98
255
  case 'register':
99
256
  renderTurnstileIfNeeded(false);