From 4787c0c1d841dc96f6947ce02a7a0de6e00338e9 Mon Sep 17 00:00:00 2001 From: fatchan Date: Fri, 9 Aug 2019 08:09:50 +0000 Subject: [PATCH] references #2 more options for reply vs thread post settings on manage pages --- controllers/forms.js | 49 ++++++++----- gulp/res/css/style.css | 2 +- gulpfile.js | 11 +-- helpers/paramconverter.js | 12 ++-- models/forms/actionhandler.js | 3 +- models/forms/changeboardsettings.js | 17 +++-- models/forms/create.js | 11 +-- views/pages/manage.pug | 102 +++++++++++++++++----------- 8 files changed, 123 insertions(+), 84 deletions(-) diff --git a/controllers/forms.js b/controllers/forms.js index 034b9a20..2c5dca3c 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -264,28 +264,38 @@ router.post('/board/:board/post', Boards.exists, calcPerms, banCheck, postFiles, const errors = []; - // even if force file and message are off, the psot must contain one of either. + // even if force file and message are off, the post must contain one of either. if (!req.body.message && res.locals.numFiles === 0) { - errors.push('Must provide a message or file'); + errors.push('Posts must include a message or file'); } - // ensure OP has file, subject and message acording to board settings - if (!req.body.thread && res.locals.board.settings.forceOPSubject && (!req.body.subject || req.body.subject.length === 0)) { - errors.push('Threads must include a subject'); + // 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 (!req.body.thread && (res.locals.board.settings.forceOPFile && res.locals.board.settings.maxFiles !== 0) && res.locals.numFiles === 0) { - errors.push('Threads must include a file'); + if (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 (res.locals.board.settings.forceReplyFile) { + errors.push('Posts must include a file'); + } } - if (!req.body.thread && res.locals.board.settings.forceOPMessage && (!req.body.message || req.body.message.length === 0)) { - errors.push('Threads must include a message'); + if (!req.body.message || req.body.message.length === 0) { + if (!req.body.thread && res.locals.board.settings.forceThreadMessage) { + errors.push('Threads must include a message'); + } else if (res.locals.board.settings.forceReplyMessage) { + errors.push('Posts must include a message'); + } } - - // make sure, min message length <= message length < max length (4k) if (req.body.message) { if (req.body.message.length > 4000) { errors.push('Message must be 4000 characters or less'); - } else if (req.body.message.length < res.locals.board.settings.minMessageLength) { - errors.push(`Message must be at least ${res.locals.board.settings.minMessageLength} characters long`); + } else if (!req.body.thread && req.body.message.length < res.locals.board.settings.minThreadMessageLength) { + errors.push(`Thread messages must be at least ${res.locals.board.settings.minMessageLength} characters long`); + } else if (req.body.thread && req.body.message.length < res.locals.board.settings.minReplyMessageLength) { + errors.push(`Reply messages must be at least ${res.locals.board.settings.minMessageLength} characters long`); } } @@ -357,8 +367,11 @@ router.post('/board/:board/settings', csrf, Boards.exists, calcPerms, banCheck, if (typeof req.body.max_files === 'number' && (req.body.max_files < 0 || req.body.max_files > 3)) { errors.push('Max files must be 0-3'); } - if (typeof req.body.min_message_length === 'number' && (req.body.min_message_length < 0 || req.body.min_message_length > 4000)) { - errors.push('Min message length must be 0-4000. 0 is disabled.'); + if (typeof req.body.min_thread_message_length === 'number' && (req.body.min_thread_message_length < 0 || req.body.min_thread_message_length > 4000)) { + errors.push('Min thread message length must be 0-4000. 0 is disabled.'); + } + if (typeof req.body.min_reply_message_length === 'number' && (req.body.min_reply_message_length < 0 || req.body.min_reply_message_length > 4000)) { + errors.push('Min reply message length must be 0-4000. 0 is disabled.'); } if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) { errors.push('Invalid captcha mode.'); @@ -487,10 +500,10 @@ async function boardActionController(req, res, next) { } //check if they have permission to perform the actions + if (res.locals.permLevel > res.locals.actions.authRequired) { + errors.push('No permission'); + } if (res.locals.permLevel >= 4) { - if (res.locals.permLevel > res.locals.actions.authRequired) { - errors.push('No permission'); - } if (req.body.delete && !res.locals.board.settings.userPostDelete) { errors.push('Post deletion is disabled on this board'); } diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index 3720ed8e..fa09acf5 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -252,7 +252,7 @@ p { } .required { - margin-left: 5px; + margin: 0 5px; } .pinktext { diff --git a/gulpfile.js b/gulpfile.js index fe4d305e..d1a43afe 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -64,10 +64,13 @@ async function wipe() { 'threadLimit': 200, 'replyLimit': 500, 'maxFiles': 0, - 'forceOPSubject': false, - 'forceOPMessage': true, - 'forceOPFile': false, - 'minMessageLength': 0, + 'forceReplyMessage':false, + 'forceReplyFile':false, + 'forceThreadMessage'false, + 'forceThreadFile':false, + 'forceThreadSubject'false, + 'minThreadMessageLength':0', + 'minReplyMessageLength':0, 'defaultName': 'Anonymous', 'announcement': { 'raw':null, diff --git a/helpers/paramconverter.js b/helpers/paramconverter.js index 9fb480f8..32a56644 100644 --- a/helpers/paramconverter.js +++ b/helpers/paramconverter.js @@ -2,8 +2,10 @@ const Mongo = require(__dirname+'/../db/db.js') , allowedArrays = new Set(['checkedposts', 'globalcheckedposts', 'checkedbans', 'checkedbanners']) //only these can be arrays, since express bodyparser will output arrays - , trimFields = ['uri', 'moderators', 'filters', 'announcement', 'description', 'message', 'name', 'subject', 'email', 'password', 'default_name', 'report_reason', 'ban_reason'] //trim if we dont want filed with whitespace - , numberFields = ['filter_mode', 'captcha_mode', 'tph_trigger', 'tph_trigger_action', 'reply_limit', 'max_files', 'thread_limit', 'thread', 'min_message_length'] //convert these to numbers before they hit our routes + , trimFields = ['uri', 'moderators', 'filters', 'announcement', 'description', 'message', + 'name', 'subject', 'email', 'password', 'default_name', 'report_reason', 'ban_reason'] //trim if we dont want filed with whitespace + , numberFields = ['filter_mode', 'captcha_mode', 'tph_trigger', 'tph_trigger_action', 'reply_limit', + 'max_files', 'thread_limit', 'thread', 'min_thread_message_length', 'min_reply_message_length'] //convert these to numbers before they hit our routes , banDurationRegex = /^(?[\d]+y)?(?[\d]+m)?(?[\d]+w)?(?[\d]+d)?(?[\d]+h)?$/ , msTime = require(__dirname+'/mstime.js') @@ -28,11 +30,7 @@ module.exports = (req, res, next) => { for (let i = 0; i < trimFields.length; i++) { const field = trimFields[i]; if (req.body[field]) { - /* - we only trimEnd() because: - - trailing whitespace doesnt matter, but leading can affect how a post appears - - if it is all whitespace, trimEnd will get it all anyway - */ + //trimEnd() because trailing whitespace doesnt affect how a post appear and if it is all whitespace, trimEnd will get it all anyway req.body[field] = req.body[field].trimEnd(); } } diff --git a/models/forms/actionhandler.js b/models/forms/actionhandler.js index 67de0b12..8b580051 100644 --- a/models/forms/actionhandler.js +++ b/models/forms/actionhandler.js @@ -33,8 +33,7 @@ module.exports = async (req, res, next) => { const inputPasswordHash = createHash('sha256').update(postPasswordSecret + req.body.password).digest('base64'); const inputPasswordBuffer = Buffer.from(inputPasswordHash); passwordPosts = res.locals.posts.filter(post => { - //length comparison could reveal the length, but not contents, and is better than comparing and hashing for empty password (most posts) - if (post.password != null && post.password.length === req.body.password) { + if (post.password != null) { const postBuffer = Buffer.from(post.password); if (timingSafeEqual(inputBuffer, postBuffer) === true) { passwordPostMongoIds.push(Mongo.ObjectId(post._id)); diff --git a/models/forms/changeboardsettings.js b/models/forms/changeboardsettings.js index 25372f0e..4d6cd56c 100644 --- a/models/forms/changeboardsettings.js +++ b/models/forms/changeboardsettings.js @@ -49,19 +49,22 @@ module.exports = async (req, res, next) => { locked: req.body.locked ? true : false, ids: req.body.ids ? true : false, forceAnon: req.body.force_anon ? true : false, - userPostDelete: req.body.user_post_delete ? true : false, - userPostSpoiler: req.body.user_post_spoiler ? true : false, - userPostUnlink: req.body.user_post_unlink ? true : false, + userPostDelete: req.body.user_reply_delete ? true : false, + userPostSpoiler: req.body.user_reply_spoiler ? true : false, + userPostUnlink: req.body.user_reply_unlink ? true : false, captchaMode: typeof req.body.captcha_mode === 'number' && req.body.captcha_mode !== oldSettings.captchaMode ? req.body.captcha_mode : oldSettings.captchaMode, tphTrigger: typeof req.body.tph_trigger === 'number' && req.body.tph_trigger !== oldSettings.tphTrigger ? req.body.tph_trigger : oldSettings.tphTrigger, tphTriggerAction: typeof req.body.tph_trigger_action === 'number' && req.body.tph_trigger_action !== oldSettings.tphTriggerAction ? req.body.tph_trigger_action : oldSettings.tphTriggerAction, threadLimit: typeof req.body.thread_limit === 'number' && req.body.thread_limit !== oldSettings.threadLimit ? req.body.thread_limit : oldSettings.threadLimit, replyLimit: typeof req.body.reply_limit === 'number' && req.body.reply_limit !== oldSettings.replyLimit ? req.body.reply_limit : oldSettings.replyLimit, maxFiles: typeof req.body.max_files === 'number' && req.body.max_files !== oldSettings.maxFiles ? req.body.max_files : oldSettings.maxFiles, - minMessageLength: typeof req.body.min_message_length === 'number' && req.body.min_message_length !== oldSettings.maxFiles ? req.body.min_message_length : oldSettings.minMessageLength, - forceOPSubject: req.body.force_op_subject ? true : false, - forceOPMessage: req.body.force_op_message ? true : false, - forceOPFile: req.body.force_op_file ? true : false, + minThreadMessageLength: typeof req.body.min_thread_message_length === 'number' && req.body.min_thread_message_length !== oldSettings.minThreadMessageLength ? req.body.min_thread_message_length : oldSettings.minThreadMessageLength, + minReplyMessageLength: typeof req.body.min_reply_message_length === 'number' && req.body.min_reply_message_length !== oldSettings.minReplyMessageLength ? req.body.min_reply_message_length : oldSettings.minReplyMessageLength, + forceThreadMessage: req.body.force_thread_message ? true : false, + forceThreadFile: req.body.force_thread_file ? true : false, + forceReplyMessage: req.body.force_reply_message ? true : false, + forceReplyFile: req.body.force_reply_file ? true : false, + forceThreadSubject: req.body.force_thread_subject ? true : false, defaultName: req.body.default_name && req.body.default_name.trim().length > 0 ? req.body.default_name : oldSettings.defaultName, announcement: { raw: req.body.announcement !== null ? req.body.announcement : oldSettings.announcement.raw, diff --git a/models/forms/create.js b/models/forms/create.js index 4da56b79..4e8743e8 100644 --- a/models/forms/create.js +++ b/models/forms/create.js @@ -45,10 +45,13 @@ module.exports = async (req, res, next) => { 'threadLimit': 200, 'replyLimit': 500, 'maxFiles': 3, - 'forceOPSubject': false, - 'forceOPMessage': true, - 'forceOPFile': false, - 'minMessageLength': 0, + 'forceReplyMessage': false, + 'forceReplyFile': false, + 'forceThreadMessage': false, + 'forceThreadFile': false, + 'forceThreadSubject': false, + 'minThreadMessageLength': 0, + 'minReplyMessageLength': 0, 'defaultName': 'Anonymous', 'announcement': { 'raw':null, diff --git a/views/pages/manage.pug b/views/pages/manage.pug index 812331af..830a58d5 100644 --- a/views/pages/manage.pug +++ b/views/pages/manage.pug @@ -19,78 +19,98 @@ block content .label Board Description input(type='text' name='description' value=board.settings.description) section.row - .label Moderators - textarea(name='moderators' placeholder='newline separated') #{board.settings.moderators.join('\n')} + .label Announcement + textarea(name='announcement' placeholder='supports post styling') #{board.settings.announcement.raw} section.row - .label Board Locked - label.postform-style.ph-5 - input(type='checkbox', name='locked', value='true' checked=board.settings.locked) + .label Anon Name + input(type='text' name='default_name' value=board.settings.defaultName) + section.row + .label Max Files + input(type='number' name='max_files' value=board.settings.maxFiles) section.row .label IDs label.postform-style.ph-5 input(type='checkbox', name='ids', value='true' checked=board.settings.ids) section.row - .label Force Anon + .label User Post Deletion label.postform-style.ph-5 - input(type='checkbox', name='force_anon', value='true' checked=board.settings.forceAnon) - section.row - .label Captcha Mode - select(name='captcha_mode' checked=board.settings.captchaMode) - option(value='0', selected=board.settings.captchaMode === 0) No Captcha - option(value='1', selected=board.settings.captchaMode === 1) Captcha for new thread - option(value='2', selected=board.settings.captchaMode === 2) Captcha for all posts + input(type='checkbox', name='user_post_delete', value='true' checked=board.settings.userPostDelete) section.row - .label TPH Trigger Threshold - input(type='number', name='tph_trigger', value=board.settings.tphTrigger) + .label User File Spoilering + label.postform-style.ph-5 + input(type='checkbox', name='user_post_spoiler', value='true' checked=board.settings.userPostSpoiler) section.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 Board + .label User File Unlinking + label.postform-style.ph-5 + input(type='checkbox', name='user_post_unlink', value='true' checked=board.settings.userPostUnlink) section.row - .label Post Deletion + .label Force Anon label.postform-style.ph-5 - input(type='checkbox', name='user_post_delete', value='true' checked=board.settings.userPostDelete) + input(type='checkbox', name='force_anon', value='true' checked=board.settings.forceAnon) section.row - .label File Spoilers + .label Force Thread Subject label.postform-style.ph-5 - input(type='checkbox', name='user_post_spoiler', value='true' checked=board.settings.userPostSpoiler) + input(type='checkbox', name='force_thread_subject', value='true' checked=board.settings.forceThreadSubject) section.row - .label File Unlinking + .label Force Thread Message + .required * label.postform-style.ph-5 - input(type='checkbox', name='user_post_unlink', value='true' checked=board.settings.userPostUnlink) + input(type='checkbox', name='force_thread_message', value='true' checked=board.settings.forceThreadMessage) section.row - .label Force OP Message + .label Force Thread File + .required * label.postform-style.ph-5 - input(type='checkbox', name='force_op_message', value='true' checked=board.settings.forceOPMessage) + input(type='checkbox', name='force_thread_file', value='true' checked=board.settings.forceThreadFile) section.row - .label Force OP Subject + .label Force Reply Message + .required * label.postform-style.ph-5 - input(type='checkbox', name='force_op_subject', value='true' checked=board.settings.forceOPSubject) + input(type='checkbox', name='force_reply_message', value='true' checked=board.settings.forceReplyMessage) section.row - .label Force OP File + .label Force Reply File + .required * label.postform-style.ph-5 - input(type='checkbox', name='force_op_file', value='true' checked=board.settings.forceOPFile) + input(type='checkbox', name='force_reply_file', value='true' checked=board.settings.forceReplyFile) + .row + .label + .required * + | All posts still require either a message or file(s) section.row - .label Anon Name - input(type='text' name='default_name' value=board.settings.defaultName) + .label Min Thread Message Length + input(type='number' name='min_thread_message_length' value=board.settings.minThreadMessageLength placeholder='0-4000') section.row - .label Min Message Length - input(type='number' name='min_message_length' value=board.settings.minMessageLength) + .label Min Reply Message Length + input(type='number' name='min_reply_message_length' value=board.settings.minReplyMessageLength placeholder='0-4000') section.row .label Thread Limit input(type='number' name='thread_limit' value=board.settings.threadLimit) section.row .label Reply Limit input(type='number' name='reply_limit' value=board.settings.replyLimit) + br section.row - .label Max Files - input(type='number' name='max_files' value=board.settings.maxFiles) + .label Moderators + textarea(name='moderators' placeholder='newline separated') #{board.settings.moderators.join('\n')} section.row - .label Announcement - textarea(name='announcement' placeholder='supports post styling') #{board.settings.announcement.raw} + .label Board Locked + label.postform-style.ph-5 + input(type='checkbox', name='locked', value='true' checked=board.settings.locked) + section.row + .label Captcha Mode + select(name='captcha_mode' checked=board.settings.captchaMode) + option(value='0', selected=board.settings.captchaMode === 0) No Captcha + option(value='1', selected=board.settings.captchaMode === 1) Captcha for new thread + option(value='2', selected=board.settings.captchaMode === 2) Captcha for all posts + section.row + .label TPH Trigger Threshold + input(type='number', name='tph_trigger', value=board.settings.tphTrigger) + section.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 Board section.row .label Filters textarea(name='filters' placeholder='newline separated') #{board.settings.filters.join('\n')}