UNPKG

nodebb-plugin-spam-be-gone-test

Version:

anti spam using both Google Recaptcha, Akismet.com, StopForumSpam.com & ProjectHoneyPot.com

456 lines (406 loc) 14.2 kB
'use strict'; var Honeypot = require('project-honeypot'); var simpleRecaptcha = require('simple-recaptcha-new'); var stopforumspam = require('stopforumspam'); var pluginData = require('./plugin.json'); var winston = require.main.require('winston'); var nconf = require.main.require('nconf'); var async = require.main.require('async'); var Meta = require.main.require('./src/meta'); var User = require.main.require('./src/user'); var Topics = require.main.require('./src/topics'); var db = require.main.require('./src/database'); var akismet; var honeypot; var recaptchaArgs; var pluginSettings; var Plugin = module.exports; pluginData.nbbId = pluginData.id.replace(/nodebb-plugin-/, ''); Plugin.nbbId = pluginData.nbbId; Plugin.middleware = {}; Plugin.middleware.isAdminOrGlobalMod = function (req, res, next) { User.isAdminOrGlobalMod(req.uid, function (err, isAdminOrGlobalMod) { if (isAdminOrGlobalMod) { return next(); } res.status(401).json({ message: '[[spam-be-gone:not-allowed]]' }); }); }; Plugin.load = function (params, callback) { Meta.settings.get(pluginData.nbbId, function (err, settings) { if (err) { return callback(err); } if (!settings) { winston.warn('[plugins/' + pluginData.nbbId + '] Settings not set or could not be retrieved!'); return callback(); } if (settings.akismetEnabled === 'on') { if (settings.akismetApiKey) { akismet = require('akismet').client({ blog: nconf.get('url'), apiKey: settings.akismetApiKey }); akismet.verifyKey(function (err, verified) { if (!verified) { winston.error('[plugins/' + pluginData.nbbId + '] Unable to verify Akismet API key.'); akismet = null; } }); } else { winston.error('[plugins/' + pluginData.nbbId + '] Akismet API Key not set!'); } } if (settings.honeypotEnabled === 'on') { if (settings.honeypotApiKey) { honeypot = Honeypot(settings.honeypotApiKey); } else { winston.error('[plugins/' + pluginData.nbbId + '] Honeypot API Key not set!'); } } if (settings.recaptchaEnabled === 'on') { if (settings.recaptchaPublicKey && settings.recaptchaPrivateKey) { recaptchaArgs = { publicKey: settings.recaptchaPublicKey, targetId: pluginData.nbbId + '-recaptcha-target', options: { // theme: settings.recaptchaTheme || 'clean', //todo: switch to custom theme, issue#9 theme: 'clean', hl: (Meta.config.defaultLang || 'en').toLowerCase(), tabindex: settings.recaptchaTabindex || 0 } }; } } if (!settings.akismetMinReputationHam) { settings.akismetMinReputationHam = 10; } if (settings.stopforumspamApiKey) { stopforumspam.Key(settings.stopforumspamApiKey); } winston.info('[plugins/' + pluginData.nbbId + '] Settings loaded'); pluginSettings = settings; params.router.get('/admin/plugins/' + pluginData.nbbId, params.middleware.admin.buildHeader, Plugin.render); params.router.get('/api/admin/plugins/' + pluginData.nbbId, Plugin.render); params.router.post('/api/user/:user/' + pluginData.nbbId + '/report', Plugin.middleware.isAdminOrGlobalMod, Plugin.report); callback(); }); }; Plugin.render = function (req, res) { res.render('admin/plugins/' + pluginData.nbbId, pluginData || {}); }; Plugin.report = function (req, res) { if (!pluginSettings.stopforumspamEnabled) { return res.status(400).send({ message: '[[spam-be-gone:sfs-not-enabled]]' }); } if (!pluginSettings.stopforumspamApiKey) { return res.status(400).send({ message: '[[spam-be-gone:sfs-api-key-not-set]]' }); } async.waterfall([ function (next) { User.getUidByUserslug(req.params.user, next); }, function (uid, next) { async.parallel({ isAdmin: function(next) { User.isAdministrator(uid, next); }, fields: function(next) { User.getUserFields(uid, ['username', 'email', 'uid'], next); }, ips: function (next) { User.getIPs(uid, 4, next); } }, next); } ], function (err, results) { if (results.isAdmin) { return res.status(403).send({ message: '[[spam-be-gone:cant-report-admin]]' }); } var data = { ip: results.ips[0], email: results.fields.email, username: results.fields.username }; stopforumspam.submit(data, 'Manual submission from user:' + req.uid + ' to user:' + results.fields.uid + ' via ' + pluginData.id) .then(function () { return res.status(200).json({ message: '[[spam-be-gone:user-reported]]' }); }) .catch(function (err) { winston.error('[plugins/' + pluginData.nbbId + '][report-error] ' + err.message, data); return res.status(400).json({ message: err.message || 'Something went wrong' }); }); }); }; Plugin.addCaptcha = function (data, callback) { if (recaptchaArgs) { var captcha = { label: 'Captcha', html: '' + '<div id="' + pluginData.nbbId + '-recaptcha-target"></div>' + '<script id="' + pluginData.nbbId + '-recaptcha-script">\n\n' + 'window.plugin = window.plugin || {};\n\t\t\t' + 'plugin["' + pluginData.nbbId + '"] = window.plugin["' + pluginData.nbbId + '"] || {};\n\t\t\t' + 'plugin["' + pluginData.nbbId + '"].recaptchaArgs = ' + JSON.stringify(recaptchaArgs) + ';\n'+ '</script>', styleName: pluginData.nbbId }; if (data.templateData.regFormEntry && Array.isArray(data.templateData.regFormEntry)) { data.templateData.regFormEntry.push(captcha); } else { data.templateData.captcha = captcha; } } callback(null, data); }; Plugin.onPostEdit = function(data, callback) { async.waterfall([ function (next) { Topics.getTopicField(data.post.tid, 'cid', next); }, function (cid, next) { Plugin.checkReply({ content: data.post.content, uid: data.post.uid, cid: cid, req: data.req, }, {type: 'post'}, function (err) { next(err, data); }); } ], callback); }; Plugin.onTopicEdit = function(data, callback) { Plugin.checkReply({ title: data.topic.title || '', uid: data.topic.uid, cid: data.topic.cid, req: data.req }, {type: 'topic'}, function(err) { callback(err, data); }); }; Plugin.onTopicPost = function(data, callback) { Plugin.checkReply(data, {type: 'topic'}, callback); }; Plugin.onTopicReply = function(data, callback) { Plugin.checkReply(data, {type: 'post'}, callback); }; Plugin.checkReply = function (data, options, callback) { if (typeof options === 'function') { callback = options; options = null; } options = options || {}; // http://akismet.com/development/api/#comment-check if (!akismet || !data || !data.req) { return callback(null, data); } var userData; var akismetData; async.waterfall([ function (next) { async.parallel({ isAdmin: function(next) { User.isAdministrator(data.uid, next); }, isModerator: function (next) { User.isModerator(data.uid, data.cid, next); }, userData: function(next) { User.getUserFields(data.uid, ['username', 'reputation', 'email'], next); } }, next); }, function (results, next) { userData = results.userData; if (results.isAdmin || results.isModerator) { return callback(null, data); } akismetData = { referrer: data.req.headers['referer'], user_ip: data.req.ip, user_agent: data.req.headers['user-agent'], permalink: nconf.get('url').replace(/\/$/, '') + data.req.path, comment_content: (data.title ? data.title + '\n\n' : '') + (data.content || ''), comment_author: userData.username, comment_author_email: userData.email, // https://github.com/akhoury/nodebb-plugin-spam-be-gone/issues/54 comment_type: options.type === 'topic' ? 'forum-post' : 'comment' }; akismet.checkSpam(akismetData, next); }, function (spam, next) { if (!spam) { return callback(null, data); } if (parseInt(userData.reputation, 10) >= parseInt(pluginSettings.akismetMinReputationHam, 10)) { akismet.submitHam(akismetData, function (err) { if (err) { winston.error(err); } }); } winston.verbose('[plugins/' + pluginData.nbbId + '] Post "' + akismetData.comment_content + '" by uid: ' + data.uid + ' username: ' + userData.username + '@' + data.req.ip + ' was flagged as spam and rejected.'); next(new Error('Post content was flagged as spam by Akismet.com')); } ], callback); }; Plugin.checkRegister = function (data, callback) { async.parallel([ function (next) { Plugin._honeypotCheck(data.req, data.res, data.userData, next); }, function (next) { Plugin._recaptchaCheck(data.req, data.res, data.userData, next); } ], function (err) { callback(err, data); }); }; function augmentWitSpamData(user, callback) { // temporary: see http://www.stopforumspam.com/forum/viewtopic.php?id=6392 user.ip = user.ip.replace('::ffff:', ''); stopforumspam.isSpammer({ ip: user.ip, email: user.email, username: user.username, f: 'json' }) .then(function (body) { // body === false, then just set the default non spam response, which stopforumspam node module doesn't return it's spam, but some template rely on it if (!body) { body = {success: 1, username: {frequency: 0, appears: 0}, email: {frequency: 0, appears: 0}, ip: {frequency: 0, appears: 0, asn: null}}; } user.spamChecked = true; user.spamData = body; user.usernameSpam = body.username ? (body.username.frequency > 0 || body.username.appears > 0) : true; user.emailSpam = body.email ? (body.email.frequency > 0 || body.email.appears > 0) : true; user.ipSpam = body.ip ? (body.ip.frequency > 0 || body.ip.appears > 0) : true; user.customActions = user.customActions || []; if (pluginSettings.stopforumspamApiKey) { user.customActions.push({ title: '[[spam-be-gone:report-user]]', id: 'report-spam-user-' + user.username, class: 'btn-warning report-spam-user', icon: 'fa-flag' }); } callback(); }) .catch(function (err) { // original nodebb core implementation did not pass the error to the cb, so im keeping it that way // https://github.com/NodeBB/NodeBB/blob/2cd1be0d041892742300a2ba2d5f1087b6272071/src/user/approval.js#L260-L264 if (err) { winston.error(err); } callback(); }); } Plugin.getRegistrationQueue = function (data, callback) { if (pluginSettings.stopforumspamEnabled) { async.each(data.users, augmentWitSpamData, function (err) { callback(err, data); }); } }; Plugin.userProfileMenu = function (data, next) { if (pluginSettings.stopforumspamEnabled && pluginSettings.stopforumspamApiKey) { data.links.push({ id: 'spamBeGoneReportUserBtn', route: 'report-user', icon: 'fa-flag', name: '[[spam-be-gone:report-user]]', visibility: { self: false, other: false, moderator: false, globalMod: true, admin: true } }); } next(null, data); }; Plugin.onPostFlagged = function (data) { var flagObj = data.flag; // Don't do anything if flag is not for a post and not for "spam" reason if (flagObj.type !== 'post' || flagObj.description !== 'Spam') { return; } if (akismet && pluginSettings.akismetFlagReporting && parseInt(flagObj.reporter.reputation, 10) >= parseInt(pluginSettings.akismetFlagReporting, 10)) { async.parallel({ userData: function (next) { User.getUserFields(flagObj.target.uid, ['username', 'email'], next); }, permalink: function (next) { Topics.getTopicField(flagObj.target.tid, 'slug', next); }, ip: function (next) { db.getSortedSetRevRange('uid:' + flagObj.target.uid + ':ip', 0, 1, next); } }, function (err, data) { // todo: we don't have access to the req here :/ var submitted = { user_ip: data.ip ? data.ip[0] : '', permalink: nconf.get('url').replace(/\/$/, '') + '/topic/' + data.permalink, comment_author: data.userData.username, comment_author_email: data.userData.email, comment_content: flagObj.target.content, comment_type: 'forum-post' }; akismet.submitSpam(submitted, function (err) { if (err) { winston.error('Error reporting to Akismet', err, submitted); } winston.info('Spam reported to Akismet.', submitted); }); }); } }; Plugin._honeypotCheck = function (req, res, userData, next) { if (honeypot && req && req.ip) { honeypot.query(req.ip, function (err, results) { if (err) { winston.error(err); next(null, userData); } else { if (results && results.found && results.type) { if (results.type.spammer || results.type.suspicious) { var message = userData.username + ' | ' + userData.email + ' was detected as ' + (results.type.spammer ? 'spammer' : 'suspicious'); winston.warn('[plugins/' + pluginData.nbbId + '] ' + message + ' and was denied registration.'); next(new Error(message), userData); } else { next(null, userData); } } else { winston.verbose('[plugins/' + pluginData.nbbId + '] username:' + userData.username + ' ip:' + req.ip + ' was not found in Honeypot database'); next(null, userData); } } }); } else { next(null, userData); } }; Plugin._recaptchaCheck = function (req, res, userData, next) { if (recaptchaArgs && req && req.ip && req.body) { simpleRecaptcha( pluginSettings.recaptchaPrivateKey, req.ip, req.body['g-recaptcha-response'], function (err) { if (err) { var message = err.Error || 'Captcha not verified, are you a robot?'; winston.verbose('[plugins/' + pluginData.nbbId + '] ' + message); next(new Error(message), userData); } else { next(null, userData); } } ); } else { next(null, userData); } }; Plugin.admin = { menu: function (custom_header, callback) { custom_header.plugins.push({ "route": '/plugins/' + pluginData.nbbId, "icon": pluginData.faIcon, "name": pluginData.name }); callback(null, custom_header); } };