separate trigger action for when tph vs pph is exceeded

option for lock reset and captcha reset, to pick what you want the lock mode and captcha mod to go back to at the end of the hour
also fix avuln in boardsettings where pph trigger/mode settings were not range checked
jschan
Thomas Lynch 4 years ago
parent 029af471fc
commit fed92d6621
  1. 11
      configs/main.js.example
  2. 25
      controllers/forms/boardsettings.js
  3. 14
      db/boards.js
  4. 3
      helpers/paramconverter.js
  5. 18
      helpers/tasks.js
  6. 1
      migrations/index.js
  7. 25
      migrations/migration-0.0.16.js
  8. 10
      models/forms/changeboardsettings.js
  9. 69
      models/forms/makepost.js
  10. 2
      package.json
  11. 42
      views/pages/managesettings.pug

@ -316,10 +316,13 @@ module.exports = {
unlistedLocal: false, //board hidden from on-site board list and frontpage unlistedLocal: false, //board hidden from on-site board list and frontpage
unlistedWebring: false, //board hidden from webring unlistedWebring: false, //board hidden from webring
captchaMode: 0, //0=disabled, 1=for threads, 2=for all posts captchaMode: 0, //0=disabled, 1=for threads, 2=for all posts
tphTrigger: 0, //numebr of threads in an hour before trigger action is activated tphTrigger: 10, //numebr of threads in an hour before trigger action is activated
pphTrigger: 0, //number of posts in an hour before ^ pphTrigger: 50, //number of posts in an hour before ^
triggerAction: 0, //0=nothing, 1=captcha enable for threads, 2=captcha enable for all posts, 3=lock board //0=none, 1=captcha enable for threads, 2=captcha enable for all posts, 3=lock board
resetTrigger: false, //reset captcha/lock settings back to original at the end of hour tphTriggerAction: 1,
pphTriggerAction: 2,
captchaReset: 0,
lockReset: 0,
forceAnon: false, //disable name and subject, only allow sage email forceAnon: false, //disable name and subject, only allow sage email
sageOnlyEmail: false, //only allow sage email sageOnlyEmail: false, //only allow sage email
early404: true, //delete threads beyond the first 1/3 of pages with less than 5 replies early404: true, //delete threads beyond the first 1/3 of pages with less than 5 replies

@ -111,12 +111,6 @@ module.exports = async (req, res, next) => {
if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) { if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) {
errors.push('Invalid captcha mode'); errors.push('Invalid captcha mode');
} }
if (typeof req.body.tph_trigger === 'number' && (req.body.tph_trigger < 0 || req.body.tph_trigger > 10000)) {
errors.push('Invalid tph trigger threshold');
}
if (typeof req.body.tph_trigger_action === 'number' && (req.body.tph_trigger_action < 0 || req.body.tph_trigger_action > 4)) {
errors.push('Invalid tph trigger action');
}
if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) { if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) {
errors.push('Invalid filter mode'); errors.push('Invalid filter mode');
} }
@ -130,6 +124,25 @@ module.exports = async (req, res, next) => {
errors.push('Invalid code theme'); errors.push('Invalid code theme');
} }
if (typeof req.body.tph_trigger === 'number' && (req.body.tph_trigger < 0 || req.body.tph_trigger > 10000)) {
errors.push('Invalid tph trigger threshold');
}
if (typeof req.body.tph_trigger_action === 'number' && (req.body.tph_trigger_action < 0 || req.body.tph_trigger_action > 4)) {
errors.push('Invalid tph trigger action');
}
if (typeof req.body.pph_trigger === 'number' && (req.body.pph_trigger < 0 || req.body.pph_trigger > 10000)) {
errors.push('Invalid pph trigger threshold');
}
if (typeof req.body.pph_trigger_action === 'number' && (req.body.pph_trigger_action < 0 || req.body.pph_trigger_action > 4)) {
errors.push('Invalid pph trigger action');
}
if (typeof req.body.lock_reset === 'number' && (req.body.lock_reset < 0 || req.body.lock_reset > 2)) {
errors.push('Invalid trigger reset lock');
}
if (typeof req.body.captcha_reset === 'number' && (req.body.captcha_reset < 0 || req.body.captcha_reset > 2)) {
errors.push('Invalid trigger reset captcha');
}
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': 'Bad request',

@ -291,15 +291,11 @@ module.exports = {
}, { }, {
'$project': { '$project': {
'_id': 1, '_id': 1,
'lockMode': { 'lockMode': '$settings.lockMode',
'new': '$settings.lockMode', 'lockReset': '$settings.lockReset',
'old': '$preTriggerMode.lockMode' 'captchaMode': '$settings.captchaMode',
}, 'captchaReset': '$settings.captchaReset',
'captchaMode': { 'threadLimit': '$settings.threadLimit',
'new': '$settings.captchaMode',
'old': '$preTriggerMode.captchaMode'
},
'threadLimit': '$settings.threadLimit'
} }
} }
]).toArray(); ]).toArray();

@ -5,7 +5,8 @@ const { ObjectId } = require(__dirname+'/../db/db.js')
'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts', 'countries']) //only these should be arrays, since express bodyparser can output arrays 'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts', 'countries']) //only these should be arrays, since express bodyparser can output arrays
, trimFields = ['tags', 'uri', 'moderators', 'filters', 'announcement', 'description', 'message', , trimFields = ['tags', 'uri', 'moderators', 'filters', 'announcement', 'description', 'message',
'name', 'subject', 'email', 'postpassword', 'password', 'default_name', 'report_reason', 'ban_reason', 'log_message', 'custom_css'] //trim if we dont want filed with whitespace 'name', 'subject', 'email', 'postpassword', 'password', 'default_name', 'report_reason', 'ban_reason', 'log_message', 'custom_css'] //trim if we dont want filed with whitespace
, numberFields = ['filter_mode', 'lock_mode', 'message_r9k_mode', 'file_r9k_mode', 'captcha_mode', 'tph_trigger', 'pph_trigger', 'trigger_action', 'bump_limit', 'reply_limit', 'move_to_thread',, 'postId', , numberFields = ['lock_reset', 'captcha_reset', 'filter_mode', 'lock_mode', 'message_r9k_mode', 'file_r9k_mode', 'captcha_mode',
'tph_trigger', 'pph_trigger', 'pph_trigger_action', 'tph_trigger_action', 'bump_limit', 'reply_limit', 'move_to_thread', 'postId',
'max_files', 'thread_limit', 'thread', 'max_thread_message_length', 'max_reply_message_length', 'min_thread_message_length', 'min_reply_message_length', 'auth_level'] //convert these to numbers before they hit our routes 'max_files', 'thread_limit', 'thread', 'max_thread_message_length', 'max_reply_message_length', 'min_thread_message_length', 'min_reply_message_length', 'auth_level'] //convert these to numbers before they hit our routes
, banDurationRegex = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+mo)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?(?<MINUTE>[\d]+m)?(?<SECOND>[\d]+s)?$/ , banDurationRegex = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+mo)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?(?<MINUTE>[\d]+m)?(?<SECOND>[\d]+s)?$/
, timeUtils = require(__dirname+'/timeutils.js') , timeUtils = require(__dirname+'/timeutils.js')

@ -236,6 +236,7 @@ module.exports = {
if (triggeredBoards.length === 0) { if (triggeredBoards.length === 0) {
return; //no label is no triggers return; //no label is no triggers
} }
//console.log(triggeredBoards);
await cache.del('triggered'); await cache.del('triggered');
const triggerModes = await Boards.triggerModes(triggeredBoards); const triggerModes = await Boards.triggerModes(triggeredBoards);
const bulkWrites = triggerModes.map(p => { const bulkWrites = triggerModes.map(p => {
@ -246,22 +247,26 @@ module.exports = {
}, },
'update': { 'update': {
'$set': { '$set': {
'settings.lockMode': p.lockMode.old, /* reset=0 is "no change", the options go from 0-2, and get reset to 0 or 1,
'settings.captchaMode': p.captchaMode.old so if >0, we subtract 1 otherwise no change */
'settings.lockMode': (p.lockReset > 0 ? p.lockReset-1 : p.lockMode),
'settings.captchaMode': (p.captchaReset > 0 ? p.captchaReset-1 : p.captchaMode),
} }
} }
} }
} }
}) });
//console.log(bulkWrites);
await Boards.db.bulkWrite(bulkWrites); await Boards.db.bulkWrite(bulkWrites);
const promises = []; const promises = [];
triggerModes.forEach(async (p) => { triggerModes.forEach(async (p) => {
await cache.del(`board:${p._id}`); await cache.del(`board:${p._id}`);
if (p.captchaMode.old < p.captchaMode.new) { //console.log(p, p.captchaReset > 0 && p.captchaReset-1 < p.captchaMode);
if (p.captchaMode.old === 2) { if (p.captchaReset > 0 && p.captchaReset-1 < p.captchaMode) {
if (p.captchaReset-1 <= 1) {
promises.push(remove(`${uploadDirectory}/html/${p._id}/thread/`)); promises.push(remove(`${uploadDirectory}/html/${p._id}/thread/`));
} }
if (p.captchaMode.old === 0) { if (p.captchaReset-1 === 0) {
buildQueue.push({ buildQueue.push({
'task': 'buildBoardMultiple', 'task': 'buildBoardMultiple',
'options': { 'options': {
@ -282,7 +287,6 @@ module.exports = {
await Promise.all(promises); await Promise.all(promises);
const end = process.hrtime(start); const end = process.hrtime(start);
debugLogs && console.log(timeDiffString(label, end)); debugLogs && console.log(timeDiffString(label, end));
}, },
buildChangePassword: async () => { buildChangePassword: async () => {

@ -16,4 +16,5 @@ module.exports = {
'0.0.13': require(__dirname+'/migration-0.0.13.js'), //add r9k mode (files) '0.0.13': require(__dirname+'/migration-0.0.13.js'), //add r9k mode (files)
'0.0.14': require(__dirname+'/migration-0.0.14.js'), //add option for disable .onion file posts to board settings '0.0.14': require(__dirname+'/migration-0.0.14.js'), //add option for disable .onion file posts to board settings
'0.0.15': require(__dirname+'/migration-0.0.15.js'), //messages r9k option '0.0.15': require(__dirname+'/migration-0.0.15.js'), //messages r9k option
'0.0.16': require(__dirname+'/migration-0.0.16.js'), //separate tph/pph triggers
} }

@ -0,0 +1,25 @@
'use strict';
module.exports = async(db, redis) => {
console.log('Allow tph/pph separate triggers and resets');
await db.collection('boards').updateMany({}, {
'$rename': {
'settings.triggerAction' : 'settings.pphTriggerAction',
}
});
await db.collection('boards').updateMany({}, {
'$unset': {
'settings.resetTrigger' : '',
'preTriggerMode': '',
}
});
await db.collection('boards').updateMany({}, {
'$set': {
'settings.tphTriggerAction' : 0,
'settings.captchaReset' : 0,
'settings.lockReset' : 0,
}
});
console.log('Cleared boards cache');
await redis.deletePattern('board:*');
};

@ -93,11 +93,13 @@ module.exports = async (req, res, next) => {
'forceReplyFile': booleanSetting(req.body.force_reply_file), 'forceReplyFile': booleanSetting(req.body.force_reply_file),
'forceThreadSubject': booleanSetting(req.body.force_thread_subject), 'forceThreadSubject': booleanSetting(req.body.force_thread_subject),
'disableReplySubject': booleanSetting(req.body.disable_reply_subject), 'disableReplySubject': booleanSetting(req.body.disable_reply_subject),
'resetTrigger': booleanSetting(req.body.reset_trigger),
'captchaMode': numberSetting(req.body.captcha_mode, oldSettings.captchaMode), 'captchaMode': numberSetting(req.body.captcha_mode, oldSettings.captchaMode),
'tphTrigger': numberSetting(req.body.tph_trigger, oldSettings.tphTrigger), 'tphTrigger': numberSetting(req.body.tph_trigger, oldSettings.tphTrigger),
'tphTriggerAction': numberSetting(req.body.tph_trigger_action, oldSettings.tphTriggerAction),
'pphTrigger': numberSetting(req.body.pph_trigger, oldSettings.pphTrigger), 'pphTrigger': numberSetting(req.body.pph_trigger, oldSettings.pphTrigger),
'triggerAction': numberSetting(req.body.trigger_action, oldSettings.triggerAction), 'pphTriggerAction': numberSetting(req.body.pph_trigger_action, oldSettings.pphTriggerAction),
'captchaReset': numberSetting(req.body.captcha_reset, oldSettings.captchaReset),
'lockReset': numberSetting(req.body.lock_reset, oldSettings.lockReset),
'threadLimit': numberSetting(req.body.thread_limit, oldSettings.threadLimit), 'threadLimit': numberSetting(req.body.thread_limit, oldSettings.threadLimit),
'replyLimit': numberSetting(req.body.reply_limit, oldSettings.replyLimit), 'replyLimit': numberSetting(req.body.reply_limit, oldSettings.replyLimit),
'bumpLimit': numberSetting(req.body.bump_limit, oldSettings.bumpLimit), 'bumpLimit': numberSetting(req.body.bump_limit, oldSettings.bumpLimit),
@ -134,10 +136,6 @@ module.exports = async (req, res, next) => {
await Boards.updateOne(req.params.board, { await Boards.updateOne(req.params.board, {
'$set': { '$set': {
'settings': newSettings, 'settings': newSettings,
'preTriggerMode': {
'lockMode': newSettings.lockMode,
'captchaMode': newSettings.captchaMode
}
} }
}); });

@ -47,9 +47,9 @@ module.exports = async (req, res, next) => {
let redirect = `/${req.params.board}/` let redirect = `/${req.params.board}/`
let salt = null; let salt = null;
let thread = null; let thread = null;
const { filterBanDuration, filterMode, filters, blockedCountries, resetTrigger, const { filterBanDuration, filterMode, filters, blockedCountries, threadLimit, ids, userPostSpoiler,
lockReset, captchaReset, pphTrigger, tphTrigger, tphTriggerAction, pphTriggerAction,
maxFiles, sageOnlyEmail, forceAnon, replyLimit, disableReplySubject, maxFiles, sageOnlyEmail, forceAnon, replyLimit, disableReplySubject,
threadLimit, ids, userPostSpoiler, pphTrigger, tphTrigger, triggerAction,
captchaMode, lockMode, allowedFileTypes, flags, fileR9KMode, messageR9KMode } = res.locals.board.settings; captchaMode, lockMode, allowedFileTypes, flags, fileR9KMode, messageR9KMode } = res.locals.board.settings;
if (res.locals.permLevel >= 4 if (res.locals.permLevel >= 4
&& res.locals.country && res.locals.country
@ -463,41 +463,42 @@ module.exports = async (req, res, next) => {
const postId = await Posts.insertOne(res.locals.board, data, thread); const postId = await Posts.insertOne(res.locals.board, data, thread);
let enableCaptcha = false; let enableCaptcha = false; //make this returned from some function, refactor and move the next section to another file
if (triggerAction > 0 //if trigger is enabled const pphTriggerActive = (pphTriggerAction > 0 && pphTrigger > 0);
&& (tphTrigger > 0 || pphTrigger > 0) //and has a threshold > 0 const tphTriggerActive = (tphTriggerAction > 0 && tphTrigger > 0);
&& ((triggerAction < 3 && captchaMode < triggerAction) //and its triggering captcha and captcha isnt on if (pphTriggerAction || tphTriggerActive) { //if a trigger is enabled
|| (triggerAction === 3 && lockMode < 1) //or triggering locking and board isnt locked const triggerUpdate = {
|| (triggerAction === 4 && lockMode < 2))) { '$set': {},
//read stats to check number threads in past hour };
const hourPosts = await Stats.getHourPosts(res.locals.board._id); //and a setting needs to be updated
if (hourPosts //if stats exist for this hour and its above either trigger const pphTriggerUpdate = (pphTriggerAction < 3 && captchaMode < pphTriggerAction)
&& (tphTrigger > 0 && hourPosts.tph >= tphTrigger) || (pphTriggerAction === 3 && lockMode < 1)
|| (pphTrigger > 0 && hourPosts.pph > pphTrigger)) { || (pphTriggerAction === 4 && lockMode < 2);
//update in memory for other stuff done e.g. rebuilds const tphTriggerUpdate = (tphTriggerAction < 3 && captchaMode < tphTriggerAction)
const update = { || (tphTriggerAction === 3 && lockMode < 1)
'$set': { || (tphTriggerAction === 4 && lockMode < 2);
'preTriggerMode': { if (tphTriggerUpdate || pphTriggerUpdate) {
lockMode, const hourPosts = await Stats.getHourPosts(res.locals.board._id);
captchaMode const calcTriggerMode = (update, trigger, triggerAction, stat) => { //todo: move this somewhere else
if (trigger > 0 && stat >= trigger) {
//update in memory for other stuff done e.g. rebuilds
if (triggerAction < 3) {
res.locals.board.settings.captchaMode = triggerAction;
update['$set']['settings.captchaMode'] = triggerAction;
enableCaptcha = true; //todo make this also returned after moving/refactoring this
} else {
res.locals.board.settings.lockMode = triggerAction-2;
update['$set']['settings.lockMode'] = triggerAction-2;
} }
return true;
} }
}; return false;
if (triggerAction < 3) {
res.locals.board.settings.captchaMode = triggerAction;
update['$set']['settings.captchaMode'] = triggerAction;
enableCaptcha = true;
} else if (triggerAction === 3) {
res.locals.board.settings.lockMode = 1;
update['$set']['settings.lockMode'] = 1;
} else if (triggerAction === 4) {
res.locals.board.settings.lockMode = 2;
update['$set']['settings.lockMode'] = 2;
} }
//set it in the db const updatedPphTrigger = pphTriggerUpdate && calcTriggerMode(triggerUpdate, pphTrigger, pphTriggerAction, hourPosts.pph);
await Boards.updateOne(res.locals.board._id, update); const updatedTphTrigger = tphTriggerUpdate && calcTriggerMode(triggerUpdate, tphTrigger, tphTriggerAction, hourPosts.tph);
if (resetTrigger) { if (updatedPphTrigger || updatedTphTrigger) {
//mark the board as being triggered so we can return it to old mode after on schedule //set it in the db
await Boards.updateOne(res.locals.board._id, triggerUpdate);
await cache.sadd('triggered', res.locals.board._id); await cache.sadd('triggered', res.locals.board._id);
} }
} }

@ -1,7 +1,7 @@
{ {
"name": "jschan", "name": "jschan",
"version": "0.0.1", "version": "0.0.1",
"migrateVersion": "0.0.15", "migrateVersion": "0.0.16",
"description": "", "description": "",
"main": "server.js", "main": "server.js",
"dependencies": { "dependencies": {

@ -206,24 +206,40 @@ block content
option(value='0', selected=board.settings.captchaMode === 0) No Captcha option(value='0', selected=board.settings.captchaMode === 0) No Captcha
option(value='1', selected=board.settings.captchaMode === 1) Captcha for new thread option(value='1', selected=board.settings.captchaMode === 1) Captcha for new thread
option(value='2', selected=board.settings.captchaMode === 2) Captcha for all posts option(value='2', selected=board.settings.captchaMode === 2) Captcha for all posts
.row
.label TPH Trigger Threshold
input(type='number', name='tph_trigger', value=board.settings.tphTrigger)
.row .row
.label PPH Trigger Threshold .label PPH Trigger Threshold
input(type='number', name='pph_trigger', value=board.settings.pphTrigger) input(type='number', name='pph_trigger', value=board.settings.pphTrigger)
.row .row
.label TPH/PPH Trigger Action .label PPH Trigger Action
select(name='trigger_action') select(name='pph_trigger_action')
option(value='0', selected=board.settings.triggerAction === 0) Do nothing option(value='0', selected=board.settings.pphTriggerAction === 0) Do nothing
option(value='1', selected=board.settings.triggerAction === 1) Enable captcha for new thread option(value='1', selected=board.settings.pphTriggerAction === 1) Enable captcha for new thread
option(value='2', selected=board.settings.triggerAction === 2) Enable captcha for all posts option(value='2', selected=board.settings.pphTriggerAction === 2) Enable captcha for all posts
option(value='3', selected=board.settings.triggerAction === 3) Lock thread creation option(value='3', selected=board.settings.pphTriggerAction === 3) Lock thread creation
option(value='4', selected=board.settings.triggerAction === 4) Lock board option(value='4', selected=board.settings.pphTriggerAction === 4) Lock board
.row .row
.label Auto Reset Trigger .label TPH Trigger Threshold
label.postform-style.ph-5 input(type='number', name='tph_trigger', value=board.settings.tphTrigger)
input(type='checkbox', name='reset_trigger', value='true' checked=board.settings.resetTrigger) .row
.label TPH Trigger Action
select(name='tph_trigger_action')
option(value='0', selected=board.settings.tphTriggerAction === 0) Do nothing
option(value='1', selected=board.settings.tphTriggerAction === 1) Enable captcha for new thread
option(value='2', selected=board.settings.tphTriggerAction === 2) Enable captcha for all posts
option(value='3', selected=board.settings.tphTriggerAction === 3) Lock thread creation
option(value='4', selected=board.settings.tphTriggerAction === 4) Lock board
.row
.label Trigger Reset Lock Mode
select(name='lock_reset')
option(value='0', selected=board.settings.lockReset === 0) No change
option(value='1', selected=board.settings.lockReset === 1) Unlock board
option(value='2', selected=board.settings.lockReset === 2) Lock thread creation
.row
.label Trigger Reset Captcha Mode
select(name='captcha_reset')
option(value='0', selected=board.settings.captchaReset === 0) No change
option(value='1', selected=board.settings.captchaReset === 1) Captcha disbaled
option(value='2', selected=board.settings.captchaReset === 2) Captcha for new thread
.row .row
.label Early 404 .label Early 404
label.postform-style.ph-5 label.postform-style.ph-5

Loading…
Cancel
Save