references #2 more options for reply vs thread post settings on manage pages

merge-requests/208/head
fatchan 5 years ago
parent 8cdd235e8f
commit 4787c0c1d8
  1. 49
      controllers/forms.js
  2. 2
      gulp/res/css/style.css
  3. 11
      gulpfile.js
  4. 12
      helpers/paramconverter.js
  5. 3
      models/forms/actionhandler.js
  6. 17
      models/forms/changeboardsettings.js
  7. 11
      models/forms/create.js
  8. 102
      views/pages/manage.pug

@ -264,28 +264,38 @@ router.post('/board/:board/post', Boards.exists, calcPerms, banCheck, postFiles,
const errors = []; 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) { 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 // check file, subject and message enforcement according to board settings
if (!req.body.thread && res.locals.board.settings.forceOPSubject && (!req.body.subject || req.body.subject.length === 0)) { if (!req.body.subject || req.body.subject.length === 0) {
errors.push('Threads must include a subject'); 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) { if (res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0) {
errors.push('Threads must include a file'); 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)) { if (!req.body.message || req.body.message.length === 0) {
errors.push('Threads must include a message'); 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) {
if (req.body.message.length > 4000) { if (req.body.message.length > 4000) {
errors.push('Message must be 4000 characters or less'); errors.push('Message must be 4000 characters or less');
} else if (req.body.message.length < res.locals.board.settings.minMessageLength) { } else if (!req.body.thread && req.body.message.length < res.locals.board.settings.minThreadMessageLength) {
errors.push(`Message must be at least ${res.locals.board.settings.minMessageLength} characters long`); 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)) { 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'); 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)) { 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 message length must be 0-4000. 0 is disabled.'); 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)) { 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.');
@ -487,10 +500,10 @@ async function boardActionController(req, res, next) {
} }
//check if they have permission to perform the actions //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 >= 4) {
if (res.locals.permLevel > res.locals.actions.authRequired) {
errors.push('No permission');
}
if (req.body.delete && !res.locals.board.settings.userPostDelete) { if (req.body.delete && !res.locals.board.settings.userPostDelete) {
errors.push('Post deletion is disabled on this board'); errors.push('Post deletion is disabled on this board');
} }

@ -252,7 +252,7 @@ p {
} }
.required { .required {
margin-left: 5px; margin: 0 5px;
} }
.pinktext { .pinktext {

@ -64,10 +64,13 @@ async function wipe() {
'threadLimit': 200, 'threadLimit': 200,
'replyLimit': 500, 'replyLimit': 500,
'maxFiles': 0, 'maxFiles': 0,
'forceOPSubject': false, 'forceReplyMessage':false,
'forceOPMessage': true, 'forceReplyFile':false,
'forceOPFile': false, 'forceThreadMessage'false,
'minMessageLength': 0, 'forceThreadFile':false,
'forceThreadSubject'false,
'minThreadMessageLength':0',
'minReplyMessageLength':0,
'defaultName': 'Anonymous', 'defaultName': 'Anonymous',
'announcement': { 'announcement': {
'raw':null, 'raw':null,

@ -2,8 +2,10 @@
const Mongo = require(__dirname+'/../db/db.js') 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 , 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 , trimFields = ['uri', 'moderators', 'filters', 'announcement', 'description', 'message',
, 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 '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 = /^(?<year>[\d]+y)?(?<month>[\d]+m)?(?<week>[\d]+w)?(?<day>[\d]+d)?(?<hour>[\d]+h)?$/ , banDurationRegex = /^(?<year>[\d]+y)?(?<month>[\d]+m)?(?<week>[\d]+w)?(?<day>[\d]+d)?(?<hour>[\d]+h)?$/
, msTime = require(__dirname+'/mstime.js') , msTime = require(__dirname+'/mstime.js')
@ -28,11 +30,7 @@ module.exports = (req, res, next) => {
for (let i = 0; i < trimFields.length; i++) { for (let i = 0; i < trimFields.length; i++) {
const field = trimFields[i]; const field = trimFields[i];
if (req.body[field]) { if (req.body[field]) {
/* //trimEnd() because trailing whitespace doesnt affect how a post appear and if it is all whitespace, trimEnd will get it all anyway
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
*/
req.body[field] = req.body[field].trimEnd(); req.body[field] = req.body[field].trimEnd();
} }
} }

@ -33,8 +33,7 @@ module.exports = async (req, res, next) => {
const inputPasswordHash = createHash('sha256').update(postPasswordSecret + req.body.password).digest('base64'); const inputPasswordHash = createHash('sha256').update(postPasswordSecret + req.body.password).digest('base64');
const inputPasswordBuffer = Buffer.from(inputPasswordHash); const inputPasswordBuffer = Buffer.from(inputPasswordHash);
passwordPosts = res.locals.posts.filter(post => { 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) {
if (post.password != null && post.password.length === req.body.password) {
const postBuffer = Buffer.from(post.password); const postBuffer = Buffer.from(post.password);
if (timingSafeEqual(inputBuffer, postBuffer) === true) { if (timingSafeEqual(inputBuffer, postBuffer) === true) {
passwordPostMongoIds.push(Mongo.ObjectId(post._id)); passwordPostMongoIds.push(Mongo.ObjectId(post._id));

@ -49,19 +49,22 @@ module.exports = async (req, res, next) => {
locked: req.body.locked ? true : false, locked: req.body.locked ? true : false,
ids: req.body.ids ? true : false, ids: req.body.ids ? true : false,
forceAnon: req.body.force_anon ? true : false, forceAnon: req.body.force_anon ? true : false,
userPostDelete: req.body.user_post_delete ? true : false, userPostDelete: req.body.user_reply_delete ? true : false,
userPostSpoiler: req.body.user_post_spoiler ? true : false, userPostSpoiler: req.body.user_reply_spoiler ? true : false,
userPostUnlink: req.body.user_post_unlink ? 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, 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, 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, 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, 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, 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, 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, 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,
forceOPSubject: req.body.force_op_subject ? true : false, 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,
forceOPMessage: req.body.force_op_message ? true : false, forceThreadMessage: req.body.force_thread_message ? true : false,
forceOPFile: req.body.force_op_file ? 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, defaultName: req.body.default_name && req.body.default_name.trim().length > 0 ? req.body.default_name : oldSettings.defaultName,
announcement: { announcement: {
raw: req.body.announcement !== null ? req.body.announcement : oldSettings.announcement.raw, raw: req.body.announcement !== null ? req.body.announcement : oldSettings.announcement.raw,

@ -45,10 +45,13 @@ module.exports = async (req, res, next) => {
'threadLimit': 200, 'threadLimit': 200,
'replyLimit': 500, 'replyLimit': 500,
'maxFiles': 3, 'maxFiles': 3,
'forceOPSubject': false, 'forceReplyMessage': false,
'forceOPMessage': true, 'forceReplyFile': false,
'forceOPFile': false, 'forceThreadMessage': false,
'minMessageLength': 0, 'forceThreadFile': false,
'forceThreadSubject': false,
'minThreadMessageLength': 0,
'minReplyMessageLength': 0,
'defaultName': 'Anonymous', 'defaultName': 'Anonymous',
'announcement': { 'announcement': {
'raw':null, 'raw':null,

@ -19,78 +19,98 @@ block content
.label Board Description .label Board Description
input(type='text' name='description' value=board.settings.description) input(type='text' name='description' value=board.settings.description)
section.row section.row
.label Moderators .label Announcement
textarea(name='moderators' placeholder='newline separated') #{board.settings.moderators.join('\n')} textarea(name='announcement' placeholder='supports post styling') #{board.settings.announcement.raw}
section.row section.row
.label Board Locked .label Anon Name
label.postform-style.ph-5 input(type='text' name='default_name' value=board.settings.defaultName)
input(type='checkbox', name='locked', value='true' checked=board.settings.locked) section.row
.label Max Files
input(type='number' name='max_files' value=board.settings.maxFiles)
section.row section.row
.label IDs .label IDs
label.postform-style.ph-5 label.postform-style.ph-5
input(type='checkbox', name='ids', value='true' checked=board.settings.ids) input(type='checkbox', name='ids', value='true' checked=board.settings.ids)
section.row section.row
.label Force Anon .label User Post Deletion
label.postform-style.ph-5 label.postform-style.ph-5
input(type='checkbox', name='force_anon', value='true' checked=board.settings.forceAnon) input(type='checkbox', name='user_post_delete', value='true' checked=board.settings.userPostDelete)
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 section.row
.label TPH Trigger Threshold .label User File Spoilering
input(type='number', name='tph_trigger', value=board.settings.tphTrigger) label.postform-style.ph-5
input(type='checkbox', name='user_post_spoiler', value='true' checked=board.settings.userPostSpoiler)
section.row section.row
.label TPH Trigger Action .label User File Unlinking
select(name='tph_trigger_action') label.postform-style.ph-5
option(value='0', selected=board.settings.tphTriggerAction === 0) Do nothing input(type='checkbox', name='user_post_unlink', value='true' checked=board.settings.userPostUnlink)
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 section.row
.label Post Deletion .label Force Anon
label.postform-style.ph-5 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 section.row
.label File Spoilers .label Force Thread Subject
label.postform-style.ph-5 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 section.row
.label File Unlinking .label Force Thread Message
.required *
label.postform-style.ph-5 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 section.row
.label Force OP Message .label Force Thread File
.required *
label.postform-style.ph-5 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 section.row
.label Force OP Subject .label Force Reply Message
.required *
label.postform-style.ph-5 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 section.row
.label Force OP File .label Force Reply File
.required *
label.postform-style.ph-5 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 section.row
.label Anon Name .label Min Thread Message Length
input(type='text' name='default_name' value=board.settings.defaultName) input(type='number' name='min_thread_message_length' value=board.settings.minThreadMessageLength placeholder='0-4000')
section.row section.row
.label Min Message Length .label Min Reply Message Length
input(type='number' name='min_message_length' value=board.settings.minMessageLength) input(type='number' name='min_reply_message_length' value=board.settings.minReplyMessageLength placeholder='0-4000')
section.row section.row
.label Thread Limit .label Thread Limit
input(type='number' name='thread_limit' value=board.settings.threadLimit) input(type='number' name='thread_limit' value=board.settings.threadLimit)
section.row section.row
.label Reply Limit .label Reply Limit
input(type='number' name='reply_limit' value=board.settings.replyLimit) input(type='number' name='reply_limit' value=board.settings.replyLimit)
br
section.row section.row
.label Max Files .label Moderators
input(type='number' name='max_files' value=board.settings.maxFiles) textarea(name='moderators' placeholder='newline separated') #{board.settings.moderators.join('\n')}
section.row section.row
.label Announcement .label Board Locked
textarea(name='announcement' placeholder='supports post styling') #{board.settings.announcement.raw} 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 section.row
.label Filters .label Filters
textarea(name='filters' placeholder='newline separated') #{board.settings.filters.join('\n')} textarea(name='filters' placeholder='newline separated') #{board.settings.filters.join('\n')}

Loading…
Cancel
Save