diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd37785..8fe7706d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,11 @@ - Update socket-io 2.x to 4.x ##### 0.1.3 - - Script optimizations, improve script execution speed especially on longer threads + - Script optimizations, improves page load speed especially on longer threads - Extra (u) download link for no reason - favicon, webmanifest, browserconfig, etc for browsers made into gulp task - Webring now sends and checks for ppd stat - Board search improved (prefix matches) + - Update code for form submission and data validation, faster and easier to maintain + - Bugfix flags not updating in the post form properly when BO add/removes them + - Bugfix reporter bans breaking in some cases diff --git a/controllers/forms/boardsettings.js b/controllers/forms/boardsettings.js index 3c3d782a..6392f6b3 100644 --- a/controllers/forms/boardsettings.js +++ b/controllers/forms/boardsettings.js @@ -22,141 +22,51 @@ module.exports = { controller: async (req, res, next) => { - const { globalLimits, rateLimitCost } = config.get; - const errors = []; - - //TODO: add helpers for different checks, passing name, min/max and return true with error if hit - if (req.body.description && - (req.body.description.length < 1 || - req.body.description.length > globalLimits.fieldLength.description)) { - errors.push(`Board description must be 1-${globalLimits.fieldLength.description} characters`); - } - if (req.body.announcements && (req.body.announcements.length < 1 || req.body.announcements.length > 2000)) { - errors.push('Board announcements must be 1-2000 characters'); - } - if (req.body.tags && req.body.tags.length > 2000) { - errors.push('Tags length must be 2000 characters or less'); - } - if (req.body.filters && req.body.filters.length > 2000) { - errors.push('Filters length must be 2000 characters or less'); - } - if (req.body.custom_css && globalLimits.customCss.enabled) { - if (res.locals.permLevel > 1 && globalLimits.customCss.strict && globalLimits.customCss.filters.some(filter => req.body.custom_css.includes(filter))) { - errors.push(`Custom CSS strict mode is enabled and does not allow the following: "${globalLimits.customCss.filters.join('", "')}"`); - } - if (req.body.custom_css.length > globalLimits.customCss.max) { - errors.push(`Custom CSS must be ${globalLimits.customCss.max} characters or less`); - } - } - if (req.body.moderators && req.body.moderators.length > 500) { - errors.push('Moderators length must be 500 characters orless'); - } - if (req.body.name && - (req.body.name.length < 1 || - req.body.name.length > globalLimits.fieldLength.boardname)) { - errors.push(`Board name must be 1-${globalLimits.fieldLength.boardname} characters`); - } - if (req.body.default_name && (req.body.default_name.length < 1 || req.body.default_name.length > 50)) { - errors.push('Anon name must be 1-50 characters'); - } - if (typeof req.body.reply_limit === 'number' - && (req.body.reply_limit < globalLimits.replyLimit.min - || req.body.reply_limit > globalLimits.replyLimit.max)) { - errors.push(`Reply Limit must be ${globalLimits.replyLimit.min}-${globalLimits.replyLimit.max}`); - } - if (typeof req.body.bump_limit === 'number' - && (req.body.bump_limit < globalLimits.bumpLimit.min - || req.body.bump_limit > globalLimits.bumpLimit.max)) { - errors.push(`Bump Limit must be ${globalLimits.bumpLimit.min}-${globalLimits.bumpLimit.max}`); - } - if (typeof req.body.thread_limit === 'number' - && (req.body.thread_limit < globalLimits.threadLimit.min - || req.body.thread_limit > globalLimits.threadLimit.max)) { - errors.push(`Threads Limit must be ${globalLimits.threadLimit.min}-${globalLimits.threadLimit.max}`); - } - if (typeof req.body.max_files === 'number' && (req.body.max_files < 0 || req.body.max_files > globalLimits.postFiles.max)) { - errors.push(`Max files must be 0-${globalLimits.postFiles.max}`); - } - - //make sure new min/max message dont conflict - if (typeof req.body.min_thread_message_length === 'number' - && typeof req.body.max_thread_message_length === 'number' - && req.body.min_thread_message_length - && req.body.max_thread_message_length - && req.body.min_thread_message_length > req.body.max_thread_message_length) { - errors.push('Min and max thread message lengths must not violate eachother'); - } - if (typeof req.body.min_reply_message_length === 'number' - && typeof req.body.max_reply_message_length === 'number' - && req.body.min_reply_message_length > req.body.max_reply_message_length) { - errors.push('Min and max reply message lengths must not violate eachother'); - } - - //make sure existing min/max message dont conflict - const minThread = Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength) || globalLimits.fieldLength.message; - if (typeof req.body.min_thread_message_length === 'number' - && (req.body.min_thread_message_length < 0 - || req.body.min_thread_message_length > minThread)) { - errors.push(`Min thread message length must be 0-${globalLimits.fieldLength.message} and not more than "Max Thread Message Length" (currently ${res.locals.board.settings.maxThreadMessageLength})`); - } - const minReply = Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) || globalLimits.fieldLength.message; - if (typeof req.body.min_reply_message_length === 'number' - && (req.body.min_reply_message_length < 0 - || req.body.min_reply_message_length > minReply)) { - errors.push(`Min reply message length must be 0-${globalLimits.fieldLength.message} and not more than "Max Reply Message Length" (currently ${res.locals.board.settings.maxReplyMessageLength})`); - } - if (typeof req.body.max_thread_message_length === 'number' - && (req.body.max_thread_message_length < 0 - || req.body.max_thread_message_length > globalLimits.fieldLength.message - || (req.body.max_thread_message_length - && req.body.max_thread_message_length < res.locals.board.settings.minThreadMessageLength))) { - errors.push(`Max thread message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Thread Message Length" (currently ${res.locals.board.settings.minThreadMessageLength})`); - } - if (typeof req.body.max_reply_message_length === 'number' - && (req.body.max_reply_message_length < 0 - || req.body.max_reply_message_length > globalLimits.fieldLength.message - || (req.body.max_reply_message_length - && req.body.max_reply_message_length < res.locals.board.settings.minReplyMessageLength))) { - errors.push(`Max reply message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Reply Message Length" (currently ${res.locals.board.settings.minReplyMessageLength})`); - } - - if (typeof req.body.lock_mode === 'number' && (req.body.lock_mode < 0 || req.body.lock_mode > 2)) { - errors.push('Invalid lock mode'); - } - if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) { - errors.push('Invalid captcha mode'); - } - if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) { - errors.push('Invalid filter mode'); - } - if (typeof req.body.ban_duration === 'number' && req.body.ban_duration < 0) { - errors.push('Invalid filter auto ban duration'); - } - if (req.body.theme && !themes.includes(req.body.theme)) { - errors.push('Invalid theme'); - } - if (req.body.code_theme && !codeThemes.includes(req.body.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'); - } + const { globalLimits, rateLimitCost } = config.get + , maxThread = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength) || globalLimits.fieldLength.message) + , maxReply = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) || globalLimits.fieldLength.message); + + const errors = await checkSchema([ + { result: lengthBody(req.body.description, 0, globalLimits.fieldLength.description), expected: false, error: `Board description must be ${globalLimits.fieldLength.description} characters or less` }, + { result: lengthBody(req.body.announcements, 0, 5000), expected: false, error: 'Board announcements must be 5000 characters or less' }, + { result: lengthBody(req.body.tags, 0, 2000), expected: false, error: 'Tags length must be 2000 characters or less' }, + { result: lengthBody(req.body.filters, 0, 2000), expected: false, error: 'Filters length must be 2000 characters or less' }, + { result: lengthBody(req.body.custom_css, 0, globalLimits.customCss.max), expected: false, error: `Custom CSS must be ${globalLimits.customCss.max} characters or less` }, + { result: arrayInBody(globalLimits.customCss.filters, req.body.custom_css), permLevel: 1, expected: false, error: `Custom CSS strict mode is enabled and does not allow the following: "${globalLimits.customCss.filters.join('", "')}"` }, + { result: lengthBody(req.body.moderators, 0, 500), expected: false, error: 'Moderators length must be 500 characters orless' }, + { result: lengthBody(req.body.name, 1, globalLimits.fieldLength.boardname), expected: false, error: `Board name must be 1-${globalLimits.fieldLength.boardname} characters` }, + { result: lengthBody(req.body.default_name, 0, 50), expected: false, error: 'Anon name must be 50 characters or less' }, + { result: numberBody(req.body.reply_limit, globalLimits.replyLimit.min, globalLimits.replyLimit.max), expected: true, error: `Reply Limit must be ${globalLimits.replyLimit.min}-${globalLimits.replyLimit.max}` }, + { result: numberBody(req.body.bump_limit, globalLimits.bumpLimit.min, globalLimits.bumpLimit.max), expected: true, error: `Bump Limit must be ${globalLimits.bumpLimit.min}-${globalLimits.bumpLimit.max}` }, + { result: numberBody(req.body.thread_limit, globalLimits.threadLimit.min, globalLimits.threadLimit.max), expected: true, error: `Threads Limit must be ${globalLimits.threadLimit.min}-${globalLimits.threadLimit.max}` }, + { result: numberBody(req.body.max_files, 0, globalLimits.postFiles.max), expected: true, error: `Max files must be 0-${globalLimits.postFiles.max}` }, + { result: minmaxBody(req.body.min_thread_message_length, req.body.max_thread_message_length), expected: true, error: 'Min and max thread message lengths must not violate eachother' }, + { result: minmaxBody(req.body.min_reply_message_length, req.body.max_reply_message_length), expected: true, error: 'Min and max reply message lengths must not violate eachother' }, + { result: numberBodyVariable(req.body.min_thread_message_length, res.locals.board.settings.minThreadMessageLength, + req.body.min_thread_message_length, maxThread, req.body.max_thread_message_length), expected: true, + error: `Min thread message length must be 0-${globalLimits.fieldLength.message} and not more than "Max Thread Message Length" (currently ${res.locals.board.settings.maxThreadMessageLength})` }, + { result: numberBodyVariable(req.body.min_reply_message_length, res.locals.board.settings.minReplyMessageLength, + req.body.min_reply_message_length, maxReply, req.body.max_reply_message_length), expected: true, + error: `Min reply message length must be 0-${globalLimits.fieldLength.message} and not more than "Max Reply Message Length" (currently ${res.locals.board.settings.maxReplyMessageLength})` }, + { result: numberBodyVariable(req.body.max_thread_message_length, res.locals.board.settings.minThreadMessageLength, + req.body.min_thread_message_length, globalLimits.fieldLength.message, globalLimits.fieldLength.message), expected: true, + error: `Max thread message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Thread Message Length" (currently ${res.locals.board.settings.minThreadMessageLength})` }, + { result: numberBodyVariable(req.body.max_reply_message_length, res.locals.board.settings.minReplyMessageLength, + req.body.min_reply_message_length, globalLimits.fieldLength.message, globalLimits.fieldLength.message), expected: true, + error: `Max reply message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Reply Message Length" (currently ${res.locals.board.settings.minReplyMessageLength})` }, + { result: numberBody(req.body.lock_mode, 0, 2), expected: true, error: 'Invalid lock mode' }, + { result: numberBody(req.body.captcha_mode, 0, 2), expected: true, error: 'Invalid captcha mode' }, + { result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: 'Invalid filter mode' }, + { result: numberBody(req.body.tph_trigger, 0, 10000), expected: true, error: 'Invalid tph trigger threshold' }, + { result: numberBody(req.body.tph_trigger_action, 0, 4), expected: true, error: 'Invalid tph trigger action' }, + { result: numberBody(req.body.pph_trigger, 0, 10000), expected: true, error: 'Invalid pph trigger threshold' }, + { result: numberBody(req.body.pph_trigger_action, 0, 4), expected: true, error: 'Invalid pph trigger action' }, + { result: numberBody(req.body.lock_reset, 0, 2), expected: true, error: 'Invalid trigger reset lock' }, + { result: numberBody(req.body.captcha_reset, 0, 2), expected: true, error: 'Invalid trigger reset captcha' }, + { result: numberBody(req.body.ban_duration, 0), expected: true, error: 'Invalid filter auto ban duration' }, + { result: inArrayBody(req.body.theme, themes), expected: true, error: 'Invalid theme' }, + { result: inArrayBody(req.body.code_theme, codeThemes), expected: true, error: 'Invalid code theme' }, + ], res.locals.permLevel); if (errors.length > 0) { return dynamicResponse(req, res, 400, 'message', { @@ -168,8 +78,8 @@ module.exports = { if (res.locals.permLevel > 1) { //if not global staff or above const ratelimitBoard = await Ratelimits.incrmentQuota(req.params.board, 'settings', rateLimitCost.boardSettings); //2 changes a minute - // const ratelimitIp = await Ratelimits.incrmentQuota(res.locals.ip.single, 'settings', rateLimitCost.boardSettings); - if (ratelimitBoard > 100 /* || ratelimitIp > 100 */) { + const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.single, 'settings', rateLimitCost.boardSettings)); + if (ratelimitBoard > 100 || ratelimitIp > 100) { return dynamicResponse(req, res, 429, 'message', { 'title': 'Ratelimited', 'error': 'You are changing settings too quickly, please wait a minute and try again', diff --git a/controllers/forms/makepost.js b/controllers/forms/makepost.js index ec78bf07..aa1d5984 100644 --- a/controllers/forms/makepost.js +++ b/controllers/forms/makepost.js @@ -22,71 +22,33 @@ module.exports = { controller: async (req, res, next) => { const { pruneImmediately, globalLimits, disableAnonymizerFilePosting } = config.get; - const errors = []; + + const hasNoMandatoryFile = globalLimits.postFiles.max !== 0 && res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0; + //maybe add more duplicates here? - // even if force file and message are off, the post must contain one of either. - if ((!req.body.message || res.locals.messageLength === 0) && res.locals.numFiles === 0) { - errors.push('Posts must include a message or file'); - } - if (res.locals.anonymizer - && (disableAnonymizerFilePosting || res.locals.board.settings.disableAnonymizerFilePosting) - && res.locals.numFiles > 0) { - errors.push(`Posting files through anonymizers has been disabled ${disableAnonymizerFilePosting ? 'globally' : 'on this board'}`); - } - if (res.locals.numFiles > res.locals.board.settings.maxFiles) { - errors.push(`Too many files. Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}`); - } - // check file, subject and message enforcement according to board settings - if (!req.body.subject || req.body.subject.length === 0) { - if (!req.body.thread && res.locals.board.settings.forceThreadSubject) { - errors.push('Threads must include a subject'); - } //no option to force op subject, seems useless - } - if (globalLimits.postFiles.max !== 0 && res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0) { - if (!req.body.thread && res.locals.board.settings.forceThreadFile) { - errors.push('Threads must include a file'); - } else if (req.body.thread && res.locals.board.settings.forceReplyFile) { - errors.push('Posts must include a file'); - } - } - if (!req.body.message || res.locals.messageLength === 0) { - if (!req.body.thread && res.locals.board.settings.forceThreadMessage) { - errors.push('Threads must include a message'); - } else if (req.body.therad && res.locals.board.settings.forceReplyMessage) { - errors.push('Posts must include a message'); - } - } - if (req.body.message) { - if (res.locals.messageLength > globalLimits.fieldLength.message) { - errors.push(`Message must be ${globalLimits.fieldLength.message} characters or less`); - } else if (!req.body.thread - && res.locals.board.settings.maxThreadMessageLength - && res.locals.messageLength > res.locals.board.settings.maxThreadMessageLength) { - errors.push(`Thread messages must be ${res.locals.board.settings.maxThreadLength} characters or less`); - } else if (req.body.thread - && res.locals.board.settings.maxReplyMessageLength - && res.locals.messageLength > res.locals.board.settings.maxReplyMessageLength) { - errors.push(`Reply messages must be ${res.locals.board.settings.maxReplyMessageLength} characters or less`); - } else if (!req.body.thread && res.locals.messageLength < res.locals.board.settings.minThreadMessageLength) { - errors.push(`Thread messages must be at least ${res.locals.board.settings.minThreadMessageLength} characters long`); - } else if (req.body.thread && res.locals.messageLength < res.locals.board.settings.minReplyMessageLength) { - errors.push(`Reply messages must be at least ${res.locals.board.settings.minReplyMessageLength} characters long`); - } - } - - // subject, email, name, password limited length - if (req.body.postpassword && req.body.postpassword.length > globalLimits.fieldLength.postpassword) { - errors.push(`Password must be ${globalLimits.fieldLength.postpassword} characters or less`); - } - if (req.body.name && req.body.name.length > globalLimits.fieldLength.name) { - errors.push(`Name must be ${globalLimits.fieldLength.name} characters or less`); - } - if (req.body.subject && req.body.subject.length > globalLimits.fieldLength.subject) { - errors.push(`Subject must be ${globalLimits.fieldLength.subject} characters or less`); - } - if (req.body.email && req.body.email.length > globalLimits.fieldLength.email) { - errors.push(`Email must be ${globalLimits.fieldLength.email} characters or less`); - } + const errors = await checkSchema([ + { result: (lengthBody(req.body.message, 1) && res.locals.numFiles === 0), expected: false, error: 'Posts must include a message or file' }, + { result: (res.locals.anonymizer && (disableAnonymizerFilePosting || res.locals.board.settings.disableAnonymizerFilePosting) + && res.locals.numFiles > 0), expected: false, error: `Posting files through anonymizers has been disabled ${disableAnonymizerFilePosting ? 'globally' : 'on this board'}` }, + { result: res.locals.numFiles > res.locals.board.settings.maxFiles, blocking: true, permLevel: 1, expected: true, error: `Too many files. Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}` }, + { result: (lengthBody(req.body.subject, 0, 0) && (!existsBody(req.body.thread) + && res.locals.board.settings.forceThreadSubject)), expected: false, error: 'Threads must include a subject' }, + { result: lengthBody(req.body.message, 1) && (!existsBody(req.body.thread) + && res.locals.board.settings.forceThreadMessage), expected: false, error: 'Threads must include a message' }, + { result: lengthBody(req.body.message, 1) && (existsBody(req.body.thread) + && res.locals.board.settings.forceReplyMessage), expected: false, error: 'Replies must include a message' }, + { result: hasNoMandatoryFile && !existsBody(req.body.thread) && res.locals.board.settings.forceThreadFile , expected: false, error: 'Threads must include a file' }, + { result: hasNoMandatoryFile && existsBody(req.body.thread) && res.locals.board.settings.forceReplyFile , expected: false, error: 'Replies must include a file' }, + { result: lengthBody(req.body.message, 0, globalLimits.fieldLength.message), expected: false, blocking: true, error: `Message must be ${globalLimits.fieldLength.message} characters or less` }, + { result: existsBody(req.body.message) && existsBody(req.body.thread) && lengthBody(req.body.message, res.locals.board.settings.minReplyMessageLength, res.locals.board.settings.maxReplyMessageLength), + expected: false, error: `Reply messages must be ${res.locals.board.settings.minReplyMessageLength}-${res.locals.board.settings.maxReplyMessageLength} characters` }, + { result: existsBody(req.body.message) && !existsBody(req.body.thread) && lengthBody(req.body.message, res.locals.board.settings.minThreadMessageLength, res.locals.board.settings.maxThreadMessageLength), + expected: false, error: `Thread messages must be ${res.locals.board.settings.minThreadMessageLength}-${res.locals.board.settings.maxThreadMessageLength} characters` }, + { result: lengthBody(req.body.postpassword, 0, globalLimits.fieldLength.postpassword), expected: false, error: `Password must be ${globalLimits.fieldLength.postpassword} characters or less` }, + { result: lengthBody(req.body.name, 0, globalLimits.fieldLength.name), expected: false, error: `Password must be ${globalLimits.fieldLength.name} characters or less` }, + { result: lengthBody(req.body.subject, 0, globalLimits.fieldLength.subject), expected: false, error: `Password must be ${globalLimits.fieldLength.subject} characters or less` }, + { result: lengthBody(req.body.email, 0, globalLimits.fieldLength.email), expected: false, error: `Password must be ${globalLimits.fieldLength.email} characters or less` }, + ]); if (errors.length > 0) { await deleteTempFiles(req).catch(e => console.error);