@arnaudw38/nodebb-plugin-spam-be-gone 1.0.8 → 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/library.js +7 -68
- package/package.json +5 -5
- package/public/js/scripts.js +121 -163
- package/CHANGELOG.md +0 -78
package/library.js
CHANGED
|
@@ -4,6 +4,7 @@ const util = require('util');
|
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const querystring = require('querystring');
|
|
6
6
|
const Honeypot = require('project-honeypot');
|
|
7
|
+
const stopforumspam = require('stopforumspam');
|
|
7
8
|
|
|
8
9
|
const winston = require.main.require('winston');
|
|
9
10
|
const nconf = require.main.require('nconf');
|
|
@@ -99,6 +100,9 @@ Plugin.load = async function (params) {
|
|
|
99
100
|
if (!settings.akismetMinReputationHam) {
|
|
100
101
|
settings.akismetMinReputationHam = 10;
|
|
101
102
|
}
|
|
103
|
+
if (settings.stopforumspamApiKey) {
|
|
104
|
+
stopforumspam.Key(settings.stopforumspamApiKey);
|
|
105
|
+
}
|
|
102
106
|
|
|
103
107
|
pluginSettings = settings;
|
|
104
108
|
|
|
@@ -133,7 +137,7 @@ Plugin.report = async function (req, res, next) {
|
|
|
133
137
|
if (isAdmin) {
|
|
134
138
|
return res.status(403).send({ message: '[[spam-be-gone:cant-report-admin]]' });
|
|
135
139
|
}
|
|
136
|
-
await
|
|
140
|
+
await stopforumspam.submit({ ip: ips[0], email: fields.email, username: fields.username }, `Manual submission from user: ${req.uid} to user: ${fields.uid} via ${pluginData.id}`);
|
|
137
141
|
res.status(200).json({ message: '[[spam-be-gone:user-reported]]' });
|
|
138
142
|
} catch (err) {
|
|
139
143
|
winston.error(`[plugins/${pluginData.nbbId}][report-error] ${err.message}`);
|
|
@@ -148,7 +152,7 @@ Plugin.reportFromQueue = async (req, res) => {
|
|
|
148
152
|
}
|
|
149
153
|
const submitData = { ip: data.ip, email: data.email, username: data.username };
|
|
150
154
|
try {
|
|
151
|
-
await
|
|
155
|
+
await stopforumspam.submit(submitData, `Manual submission from user: ${req.uid} to user: ${data.username} via ${pluginData.id}`);
|
|
152
156
|
res.status(200).json({ message: '[[spam-be-gone:user-reported]]' });
|
|
153
157
|
} catch (err) {
|
|
154
158
|
winston.error(`[plugins/${pluginData.nbbId}][report-error] ${err.message}\n${JSON.stringify(submitData, null, 4)}`);
|
|
@@ -279,7 +283,7 @@ Plugin.getRegistrationQueue = async function (data) {
|
|
|
279
283
|
async function augmentWitSpamData(user) {
|
|
280
284
|
try {
|
|
281
285
|
user.ip = user.ip.replace('::ffff:', '');
|
|
282
|
-
let body = await
|
|
286
|
+
let body = await stopforumspam.isSpammer({ ip: user.ip, email: user.email, username: user.username, f: 'json' });
|
|
283
287
|
if (!body) {
|
|
284
288
|
body = { success: 1, username: { frequency: 0, appears: 0 }, email: { frequency: 0, appears: 0 }, ip: { frequency: 0, appears: 0, asn: null } };
|
|
285
289
|
}
|
|
@@ -392,71 +396,6 @@ Plugin._turnstileCheck = async function (req) {
|
|
|
392
396
|
});
|
|
393
397
|
};
|
|
394
398
|
|
|
395
|
-
|
|
396
|
-
async function stopForumSpamLookup({ ip, email, username }) {
|
|
397
|
-
const params = new URLSearchParams();
|
|
398
|
-
params.set('f', 'json');
|
|
399
|
-
if (ip) params.set('ip', ip);
|
|
400
|
-
if (email) params.set('email', email);
|
|
401
|
-
if (username) params.set('username', username);
|
|
402
|
-
|
|
403
|
-
const res = await fetch(`https://api.stopforumspam.org/api?${params.toString()}`, {
|
|
404
|
-
headers: {
|
|
405
|
-
accept: 'application/json',
|
|
406
|
-
'user-agent': pluginData.id,
|
|
407
|
-
},
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
if (!res.ok) {
|
|
411
|
-
throw new Error(`StopForumSpam lookup failed (${res.status})`);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return await res.json();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function stopForumSpamSubmit({ ip, email, username }, evidence) {
|
|
418
|
-
if (!pluginSettings.stopforumspamApiKey) {
|
|
419
|
-
throw new Error('[[spam-be-gone:sfs-api-key-not-set]]');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const body = new URLSearchParams();
|
|
423
|
-
body.set('api_key', pluginSettings.stopforumspamApiKey);
|
|
424
|
-
body.set('api', 'json');
|
|
425
|
-
if (ip) body.set('ip_addr', ip);
|
|
426
|
-
if (email) body.set('email', email);
|
|
427
|
-
if (username) body.set('username', username);
|
|
428
|
-
if (evidence) body.set('evidence', evidence);
|
|
429
|
-
|
|
430
|
-
const res = await fetch('https://www.stopforumspam.com/add.php', {
|
|
431
|
-
method: 'POST',
|
|
432
|
-
headers: {
|
|
433
|
-
'content-type': 'application/x-www-form-urlencoded',
|
|
434
|
-
accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
|
|
435
|
-
'user-agent': pluginData.id,
|
|
436
|
-
},
|
|
437
|
-
body: body.toString(),
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
const text = await res.text();
|
|
441
|
-
if (!res.ok) {
|
|
442
|
-
throw new Error(`StopForumSpam submit failed (${res.status})`);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
const parsed = JSON.parse(text);
|
|
447
|
-
if (parsed.success === 0 || parsed.error) {
|
|
448
|
-
throw new Error(parsed.error || 'StopForumSpam submit failed');
|
|
449
|
-
}
|
|
450
|
-
return parsed;
|
|
451
|
-
} catch (err) {
|
|
452
|
-
// Some SFS responses can be plain text; consider HTTP 200 success as accepted.
|
|
453
|
-
if (/error/i.test(text)) {
|
|
454
|
-
throw err instanceof Error ? err : new Error('StopForumSpam submit failed');
|
|
455
|
-
}
|
|
456
|
-
return { success: 1, raw: text };
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
399
|
Plugin.admin = {
|
|
461
400
|
menu: function (custom_header, callback) {
|
|
462
401
|
custom_header.plugins.push({ route: `/plugins/${pluginData.nbbId}`, icon: pluginData.faIcon, name: pluginData.name });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnaudw38/nodebb-plugin-spam-be-gone",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Anti-spam plugin for NodeBB 4.x using Akismet, StopForumSpam
|
|
3
|
+
"version": "1.0.10",
|
|
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": {},
|
|
7
7
|
"repository": {
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"async": "^3.2.0",
|
|
36
|
-
"project-honeypot": "~0.0.0"
|
|
36
|
+
"project-honeypot": "~0.0.0",
|
|
37
|
+
"stopforumspam": "^1.3.8"
|
|
37
38
|
},
|
|
38
39
|
"nbbpm": {
|
|
39
40
|
"compatibility": "^4.0.0"
|
|
@@ -49,8 +50,7 @@
|
|
|
49
50
|
"upgrades/",
|
|
50
51
|
"plugin.json",
|
|
51
52
|
"README.md",
|
|
52
|
-
"LICENSE"
|
|
53
|
-
"CHANGELOG.md"
|
|
53
|
+
"LICENSE"
|
|
54
54
|
],
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|
package/public/js/scripts.js
CHANGED
|
@@ -5,19 +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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
loginAttemptArmed: false,
|
|
14
|
-
navigatingAway: false,
|
|
8
|
+
var loginTurnstile = {
|
|
9
|
+
widgetId: null,
|
|
10
|
+
observer: null,
|
|
11
|
+
watchTimer: null,
|
|
12
|
+
isWatching: false,
|
|
15
13
|
};
|
|
16
14
|
|
|
17
15
|
function getTurnstileArgs() {
|
|
18
16
|
return ajaxify.data && ajaxify.data.turnstileArgs;
|
|
19
17
|
}
|
|
20
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
|
+
|
|
21
24
|
function injectScriptOnce(src) {
|
|
22
25
|
if (document.querySelector('script[src*="turnstile/v0/api.js"]')) {
|
|
23
26
|
return Promise.resolve();
|
|
@@ -33,182 +36,140 @@ $(function () {
|
|
|
33
36
|
});
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
});
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
44
68
|
}
|
|
45
69
|
|
|
46
|
-
function
|
|
47
|
-
if (typeof turnstile === 'undefined') {
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
targetId = targetId || getCurrentLoginTurnstileTargetId();
|
|
51
|
-
if (!targetId) {
|
|
52
|
-
return false;
|
|
70
|
+
function resetLoginTurnstile() {
|
|
71
|
+
if (!isLoginPage() || typeof turnstile === 'undefined' || !turnstile || typeof turnstile.reset !== 'function') {
|
|
72
|
+
return;
|
|
53
73
|
}
|
|
54
|
-
var
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
var args = getTurnstileArgs();
|
|
75
|
+
var target = args && args.targetId ? document.getElementById(args.targetId) : null;
|
|
76
|
+
if (!target) {
|
|
77
|
+
return;
|
|
57
78
|
}
|
|
58
79
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
if (loginTurnstile.widgetId !== null && loginTurnstile.widgetId !== undefined) {
|
|
81
|
+
turnstile.reset(loginTurnstile.widgetId);
|
|
82
|
+
} else {
|
|
83
|
+
turnstile.reset(target);
|
|
84
|
+
}
|
|
61
85
|
} catch (err) {
|
|
62
|
-
//
|
|
63
|
-
return false;
|
|
86
|
+
// ignore reset errors to avoid blocking login UX
|
|
64
87
|
}
|
|
65
88
|
}
|
|
66
89
|
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
function watchForLoginFailureAndReset() {
|
|
91
|
+
if (!isLoginPage() || loginTurnstile.isWatching) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (typeof turnstile === 'undefined' || !turnstile) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
70
97
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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();
|
|
86
113
|
}
|
|
87
|
-
}
|
|
114
|
+
});
|
|
115
|
+
loginTurnstile.observer.observe(body, { childList: true, subtree: true, attributes: true });
|
|
88
116
|
}
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
117
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
96
|
-
turnstileState.loginResetTimers = [];
|
|
118
|
+
// Auto-cleanup if success navigation happens or no error is shown
|
|
119
|
+
loginTurnstile.watchTimer = window.setTimeout(function () {
|
|
120
|
+
clearLoginErrorWatch();
|
|
121
|
+
}, 5000);
|
|
97
122
|
}
|
|
98
123
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
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);
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
function
|
|
113
|
-
if (!
|
|
136
|
+
function onLoginIntentCapture(ev) {
|
|
137
|
+
if (!isLoginPage()) {
|
|
114
138
|
return;
|
|
115
139
|
}
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
var el = ev.target && ev.target.closest ? ev.target.closest('button, input[type="submit"], a') : null;
|
|
141
|
+
if (!el) {
|
|
118
142
|
return;
|
|
119
143
|
}
|
|
120
|
-
|
|
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) {
|
|
121
150
|
return;
|
|
122
151
|
}
|
|
123
|
-
|
|
124
|
-
if (targetId && document.getElementById(targetId)) {
|
|
125
|
-
resetTurnstileWidget(targetId);
|
|
126
|
-
}
|
|
127
|
-
disarmLoginAttemptWatch();
|
|
152
|
+
window.setTimeout(watchForLoginFailureAndReset, 0);
|
|
128
153
|
}
|
|
129
154
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
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 });
|
|
155
|
+
function onLoginKeydownCapture(ev) {
|
|
156
|
+
if (!isLoginPage() || ev.key !== 'Enter') {
|
|
157
|
+
return;
|
|
141
158
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
[250, 600, 1000, 1600, 2400, 3400].forEach(function (delay) {
|
|
145
|
-
var timerId = window.setTimeout(function () {
|
|
146
|
-
resetLoginTurnstileOnDetectedError();
|
|
147
|
-
}, delay);
|
|
148
|
-
turnstileState.loginResetTimers.push(timerId);
|
|
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);
|
|
159
|
-
}
|
|
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
|
-
|
|
170
|
-
function bindLoginRetryReset() {
|
|
171
|
-
if (turnstileState.loginBindingsAttached) {
|
|
159
|
+
var input = ev.target;
|
|
160
|
+
if (!input || input.tagName !== 'INPUT') {
|
|
172
161
|
return;
|
|
173
162
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
178
167
|
}
|
|
179
|
-
|
|
180
|
-
// Native capture listeners only (avoid duplicate handlers/race conditions with jQuery delegates).
|
|
181
|
-
document.addEventListener('click', function (ev) {
|
|
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')) {
|
|
188
|
-
loginSubmitTrigger();
|
|
189
|
-
}
|
|
190
|
-
}, true);
|
|
191
|
-
|
|
192
|
-
// Enter key in login fields for themes that trigger login without a click event.
|
|
193
|
-
document.addEventListener('keydown', function (ev) {
|
|
194
|
-
if (ev.key !== 'Enter') {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
var el = ev.target;
|
|
198
|
-
if (!el || !el.closest) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
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')) {
|
|
203
|
-
loginSubmitTrigger();
|
|
204
|
-
}
|
|
205
|
-
}, true);
|
|
206
|
-
|
|
207
168
|
}
|
|
208
169
|
|
|
209
|
-
function renderTurnstileIfNeeded(
|
|
170
|
+
function renderTurnstileIfNeeded(isLoginTpl) {
|
|
210
171
|
var args = getTurnstileArgs();
|
|
211
|
-
if (!args || (
|
|
172
|
+
if (!args || (isLoginTpl && !args.addLoginTurnstile)) {
|
|
212
173
|
return;
|
|
213
174
|
}
|
|
214
175
|
|
|
@@ -218,17 +179,9 @@ function renderTurnstileIfNeeded(isLoginPage) {
|
|
|
218
179
|
return;
|
|
219
180
|
}
|
|
220
181
|
var target = document.getElementById(args.targetId);
|
|
221
|
-
if (!target) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (target.dataset.turnstileRendered === '1') {
|
|
226
|
-
if (isLoginPage) {
|
|
227
|
-
bindLoginRetryReset();
|
|
228
|
-
}
|
|
182
|
+
if (!target || target.dataset.turnstileRendered === '1') {
|
|
229
183
|
return;
|
|
230
184
|
}
|
|
231
|
-
|
|
232
185
|
var widgetId = turnstile.render('#' + args.targetId, {
|
|
233
186
|
sitekey: args.siteKey,
|
|
234
187
|
theme: args.theme || 'auto',
|
|
@@ -245,16 +198,20 @@ function renderTurnstileIfNeeded(isLoginPage) {
|
|
|
245
198
|
require(['alerts'], function (alerts) { alerts.error('[[spam-be-gone:captcha-not-verified]]'); });
|
|
246
199
|
},
|
|
247
200
|
'expired-callback': function () {
|
|
248
|
-
|
|
201
|
+
if (isLoginTpl) {
|
|
202
|
+
clearLoginErrorWatch();
|
|
203
|
+
}
|
|
249
204
|
},
|
|
250
205
|
'timeout-callback': function () {
|
|
251
|
-
|
|
206
|
+
if (isLoginTpl) {
|
|
207
|
+
clearLoginErrorWatch();
|
|
208
|
+
}
|
|
252
209
|
},
|
|
253
210
|
});
|
|
254
211
|
target.dataset.turnstileRendered = '1';
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
212
|
+
if (isLoginTpl) {
|
|
213
|
+
loginTurnstile.widgetId = widgetId;
|
|
214
|
+
bindLoginRetryResetHandlers();
|
|
258
215
|
}
|
|
259
216
|
})
|
|
260
217
|
.catch(function () {
|
|
@@ -293,6 +250,7 @@ function renderTurnstileIfNeeded(isLoginPage) {
|
|
|
293
250
|
}
|
|
294
251
|
|
|
295
252
|
$(window).on('action:ajaxify.end', function (evt, data) {
|
|
253
|
+
clearLoginErrorWatch();
|
|
296
254
|
switch (data.tpl_url) {
|
|
297
255
|
case 'register':
|
|
298
256
|
renderTurnstileIfNeeded(false);
|
package/CHANGELOG.md
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
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
|
-
|
|
22
|
-
## [1.0.7] - 2026-02-25
|
|
23
|
-
|
|
24
|
-
### Fixed
|
|
25
|
-
- Login retry Turnstile reset now uses native event listeners only (removed jQuery delegated login listeners).
|
|
26
|
-
- Prevented duplicate retry-reset triggers caused by mixed jQuery + native listeners on some NodeBB themes.
|
|
27
|
-
|
|
28
|
-
### Changed
|
|
29
|
-
- Simplified login retry detection to click + Enter key flows (removed submit listener).
|
|
30
|
-
|
|
31
|
-
## [1.0.6] - 2026-02-25
|
|
32
|
-
|
|
33
|
-
### Fixed
|
|
34
|
-
- Fixed login retry Turnstile reset trigger on NodeBB login flows that do not emit the expected jQuery submit/click events.
|
|
35
|
-
- Prevented missing reset after failed login without page reload by adding capture-phase submit/click/Enter listeners.
|
|
36
|
-
|
|
37
|
-
## [1.0.5] - 2026-02-25
|
|
38
|
-
|
|
39
|
-
### Fixed
|
|
40
|
-
- Fixed Turnstile not resetting after a failed login attempt when retrying on the same page.
|
|
41
|
-
- Reset is now triggered after login submit if the user remains on the login page, even if the hidden Turnstile token input has already been cleared by client-side login logic.
|
|
42
|
-
|
|
43
|
-
## [1.0.4] - 2026-02-25
|
|
44
|
-
|
|
45
|
-
### Fixed
|
|
46
|
-
- Fixed login UX regression where the Turnstile widget was resetting while typing in the username/password fields.
|
|
47
|
-
- Turnstile now resets only after a login submission retry flow (failed login without page reload), not on every keystroke.
|
|
48
|
-
|
|
49
|
-
## [1.0.3] - 2026-02-25
|
|
50
|
-
|
|
51
|
-
### Fixed
|
|
52
|
-
- Fixed Cloudflare Turnstile failure on login retry without page reload.
|
|
53
|
-
- Reset Turnstile widget automatically after a failed login attempt (Turnstile tokens are single-use).
|
|
54
|
-
- Improved login flow reliability when users retry authentication on the same page.
|
|
55
|
-
|
|
56
|
-
### Improved
|
|
57
|
-
- Added safer Turnstile widget state handling on the login form.
|
|
58
|
-
- Added callbacks handling for expired/timeout token states.
|
|
59
|
-
- Better UX during repeated login attempts without manual refresh.
|
|
60
|
-
- Removed deprecated `stopforumspam` npm dependency and replaced it with direct StopForumSpam API requests using native `fetch` (eliminates `q` / `node-domexception` install warnings).
|
|
61
|
-
|
|
62
|
-
## [1.0.2] - 2026-02-25
|
|
63
|
-
|
|
64
|
-
### UI
|
|
65
|
-
- Replaced visible 'Turnstile' label with a more user-friendly label:
|
|
66
|
-
- French: `Vérification de sécurité`
|
|
67
|
-
|
|
68
|
-
## [1.0.1] - 2026-02-25
|
|
69
|
-
|
|
70
|
-
### Changed
|
|
71
|
-
- Refactored plugin for NodeBB 4.x compatibility.
|
|
72
|
-
- Removed legacy reCAPTCHA and hCaptcha integrations.
|
|
73
|
-
- Added Cloudflare Turnstile support (register + optional login protection).
|
|
74
|
-
- Simplified package/tooling for a minimal runtime-focused plugin setup.
|
|
75
|
-
|
|
76
|
-
### Docs
|
|
77
|
-
- Rewrote README in English (Turnstile-only, no images).
|
|
78
|
-
- Added npm-ready/publish-ready package metadata and documentation.
|