Merge pull request #20 from fatchan/batch-action-changes

action handler improvements, omitted posts patch refer #17 #18
merge-requests/208/head
Tom 5 years ago committed by GitHub
commit a20f3bc639
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 144
      controllers/forms.js
  2. 24
      db/posts.js
  3. 48
      helpers/actionchecker.js
  4. 16
      models/forms/ban-poster.js
  5. 26
      models/forms/delete-post.js
  6. 33
      models/forms/deletepostsfiles.js
  7. 11
      models/forms/dismiss-report.js
  8. 11
      models/forms/dismissglobalreport.js
  9. 38
      models/forms/spoiler-post.js
  10. 2
      views/mixins/post.pug
  11. 2
      views/pages/message.pug

@ -21,9 +21,13 @@ const express = require('express')
, loginAccount = require(__dirname+'/../models/forms/login.js') , loginAccount = require(__dirname+'/../models/forms/login.js')
, changePassword = require(__dirname+'/../models/forms/changepassword.js') , changePassword = require(__dirname+'/../models/forms/changepassword.js')
, registerAccount = require(__dirname+'/../models/forms/register.js') , registerAccount = require(__dirname+'/../models/forms/register.js')
, hasPerms = require(__dirname+'/../helpers/haspermsmiddleware.js') , checkPermsMiddleware = require(__dirname+'/../helpers/haspermsmiddleware.js')
, checkPerms = require(__dirname+'/../helpers/hasperms.js')
, numberConverter = require(__dirname+'/../helpers/number-converter.js') , numberConverter = require(__dirname+'/../helpers/number-converter.js')
, banCheck = require(__dirname+'/../helpers/bancheck.js'); , banCheck = require(__dirname+'/../helpers/bancheck.js')
, actionChecker = require(__dirname+'/../helpers/actionchecker.js')
, actions = ['report', 'global_report', 'spoiler', 'delete', 'delete_file', 'dismiss', 'global_dismiss', 'ban', 'global_ban']
, authActions = ['dismiss', 'global_dismiss', 'ban', 'global_ban'];
// login to account // login to account
router.post('/login', (req, res, next) => { router.post('/login', (req, res, next) => {
@ -202,7 +206,7 @@ router.post('/board/:board/post', Boards.exists, banCheck, numberConverter, asyn
}); });
//upload banners //upload banners
router.post('/board/:board/addbanners', Boards.exists, banCheck, hasPerms, numberConverter, async (req, res, next) => { router.post('/board/:board/addbanners', Boards.exists, banCheck, checkPermsMiddleware, numberConverter, async (req, res, next) => {
let numFiles = 0; let numFiles = 0;
if (req.files && req.files.file) { if (req.files && req.files.file) {
@ -238,7 +242,7 @@ router.post('/board/:board/addbanners', Boards.exists, banCheck, hasPerms, numbe
}); });
//delete banners //delete banners
router.post('/board/:board/deletebanners', Boards.exists, banCheck, hasPerms, numberConverter, async (req, res, next) => { router.post('/board/:board/deletebanners', Boards.exists, banCheck, checkPermsMiddleware, numberConverter, async (req, res, next) => {
const errors = []; const errors = [];
@ -278,9 +282,25 @@ router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, a
const errors = []; const errors = [];
//make sure they checked 1-10 posts
if (!req.body.checkedposts || req.body.checkedposts.length === 0 || req.body.checkedposts.length > 10) { if (!req.body.checkedposts || req.body.checkedposts.length === 0 || req.body.checkedposts.length > 10) {
errors.push('Must select 1-10 posts') errors.push('Must select 1-10 posts')
} }
//get what type of actions
const { anyPasswords, anyAuthed, anyValid } = actionChecker(req);
//make sure they selected at least 1 action
if (!anyValid) {
errors.push('No actions selected')
}
//check if they have permission to perform the actions
const hasPerms = checkPerms(req, res);
if(!hasPerms && anyAuthed) {
errors.push('No permission')
}
//check that actions are valid
if (req.body.password && req.body.password.length > 50) { if (req.body.password && req.body.password.length > 50) {
errors.push('Password must be 50 characters or less'); errors.push('Password must be 50 characters or less');
} }
@ -290,17 +310,6 @@ router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, a
if (req.body.ban_reason && req.body.ban_reason.length > 50) { if (req.body.ban_reason && req.body.ban_reason.length > 50) {
errors.push('Ban reason must be 50 characters or less'); errors.push('Ban reason must be 50 characters or less');
} }
if (!(req.body.report
|| req.body.global_report
|| req.body.spoiler
|| req.body.delete
|| req.body.delete_file
|| req.body.dismiss
|| req.body.global_dismiss
|| req.body.ban
|| req.body.global_ban)) {
errors.push('Invalid actions selected')
}
if (req.body.report && (!req.body.report_reason || req.body.report_reason.length === 0)) { if (req.body.report && (!req.body.report_reason || req.body.report_reason.length === 0)) {
errors.push('Reports must have a reason') errors.push('Reports must have a reason')
} }
@ -317,53 +326,61 @@ router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, a
if (!posts || posts.length === 0) { if (!posts || posts.length === 0) {
return res.status(404).render('message', { return res.status(404).render('message', {
'title': 'Not found', 'title': 'Not found',
'errors': 'Selected posts not found', 'error': 'Selected posts not found',
'redirect': `/${req.params.board}` 'redirect': `/${req.params.board}`
}) })
} }
let passwordPosts = posts;
if (!hasPerms && anyPasswords) {
//if any actions require passwords, filter to ones with correct password
passwordPosts = posts.filter(post => {
return post.password != null
&& post.password.length > 0
&& post.password == req.body.password
});
if (passwordPosts.length === 0) {
return res.status(403).render('message', {
'title': 'Forbidden',
'error': 'Password did not match any selected posts',
'redirect': `/${req.params.board}`
});
}
}
const messages = []; const messages = [];
try { try {
if (hasPerms) {
// if getting global banned, board ban doesnt matter // if getting global banned, board ban doesnt matter
if (req.body.global_ban) { if (req.body.global_ban) {
messages.push((await banPoster(req, res, next, null, posts))); messages.push((await banPoster(req, res, next, null, posts)));
} else if (req.body.ban) { } else if (req.body.ban) {
messages.push((await banPoster(req, res, next, req.params.board, posts))); messages.push((await banPoster(req, res, next, req.params.board, posts)));
}
} }
//ban before deleting
if (req.body.delete) { if (req.body.delete) {
messages.push((await deletePosts(req, res, next, posts))); messages.push((await deletePosts(req, res, next, passwordPosts)));
} else { } else {
// if it was getting deleted, we cant do any of these // if it was getting deleted, we cant do any of these
if (req.body.delete_file) { if (req.body.delete_file) {
messages.push((await deletePostsFiles(req, res, next, posts))); messages.push((await deletePostsFiles(req, res, next, passwordPosts)));
} if (req.body.spoiler) { } else if (req.body.spoiler) {
messages.push((await spoilerPosts(req, res, next, posts))); messages.push((await spoilerPosts(req, res, next, passwordPosts)));
} }
// cannot report and dismiss at same time // cannot report and dismiss at same time
if (req.body.report) { if (req.body.report) {
messages.push((await reportPosts(req, res, next))); messages.push((await reportPosts(req, res, next)));
} else if (req.body.dismiss) { } else if (hasPerms && req.body.dismiss) {
messages.push((await dismissReports(req, res, next))); messages.push((await dismissReports(req, res, next)));
} }
// cannot report and dismiss at same time // cannot report and dismiss at same time
if (req.body.global_report) { if (req.body.global_report) {
messages.push((await globalReportPosts(req, res, next, posts))); messages.push((await globalReportPosts(req, res, next, posts)));
} else if (req.body.global_dismiss) { } else if (hasPerms && req.body.global_dismiss) {
messages.push((await dismissGlobalReports(req, res, next, posts))); messages.push((await dismissGlobalReports(req, res, next, posts)));
} }
} }
} catch (err) { } catch (err) {
//something not right
if (err.status) {
// return out special error
return res.status(err.status).render('message', err.message);
}
//some other error, use regular error handler
return next(err); return next(err);
} }
@ -376,7 +393,7 @@ router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, a
}); });
//unban //unban
router.post('/board/:board/unban', Boards.exists, banCheck, hasPerms, numberConverter, async (req, res, next) => { router.post('/board/:board/unban', Boards.exists, banCheck, checkPermsMiddleware, numberConverter, async (req, res, next) => {
//keep this for later in case i add other options to unbans //keep this for later in case i add other options to unbans
const errors = []; const errors = [];
@ -397,12 +414,6 @@ router.post('/board/:board/unban', Boards.exists, banCheck, hasPerms, numberConv
try { try {
messages.push((await removeBans(req, res, next))); messages.push((await removeBans(req, res, next)));
} catch (err) { } catch (err) {
//something not right
if (err.status) {
// return out special error
return res.status(err.status).render('message', err.message);
}
//some other error, use regular error handler
return next(err); return next(err);
} }
@ -414,32 +425,46 @@ router.post('/board/:board/unban', Boards.exists, banCheck, hasPerms, numberConv
}); });
router.post('/global/actions', hasPerms, numberConverter, async(req, res, next) => { router.post('/global/actions', checkPermsMiddleware, numberConverter, async(req, res, next) => {
const errors = []; const errors = [];
//make sure they checked 1-10 posts
if (!req.body.globalcheckedposts || req.body.globalcheckedposts.length === 0 || req.body.globalcheckedposts.length > 10) { if (!req.body.globalcheckedposts || req.body.globalcheckedposts.length === 0 || req.body.globalcheckedposts.length > 10) {
errors.push('Must select 1-10 posts') errors.push('Must select 1-10 posts')
} }
const { anyGlobal } = actionChecker(req);
//make sure they selected at least 1 global action
if (!anyGlobal) {
errors.push('Invalid actions selected');
}
//check that actions are valid
if (req.body.password && req.body.password.length > 50) {
errors.push('Password must be 50 characters or less');
}
if (req.body.report_reason && req.body.report_reason.length > 50) {
errors.push('Report must be 50 characters or less');
}
if (req.body.ban_reason && req.body.ban_reason.length > 50) { if (req.body.ban_reason && req.body.ban_reason.length > 50) {
errors.push('Ban reason must be 50 characters or less'); errors.push('Ban reason must be 50 characters or less');
} }
if (!(req.body.spoiler if (req.body.report && (!req.body.report_reason || req.body.report_reason.length === 0)) {
|| req.body.delete errors.push('Reports must have a reason')
|| req.body.delete_file
|| req.body.global_dismiss
|| req.body.global_ban)) {
errors.push('Invalid actions selected')
} }
//return the errors
if (errors.length > 0) { if (errors.length > 0) {
return res.status(400).render('message', { return res.status(400).render('message', {
'title': 'Bad request', 'title': 'Bad request',
'errors': errors, 'errors': errors,
'redirect': '/globalmanage' 'redirect': `/${req.params.board}`
}) })
} }
//get posts with global ids only
const posts = await Posts.globalGetPosts(req.body.globalcheckedposts, true); const posts = await Posts.globalGetPosts(req.body.globalcheckedposts, true);
if (!posts || posts.length === 0) { if (!posts || posts.length === 0) {
return res.status(404).render('message', { return res.status(404).render('message', {
@ -451,12 +476,10 @@ router.post('/global/actions', hasPerms, numberConverter, async(req, res, next)
const messages = []; const messages = [];
try { try {
//ban before delete //ban before delete
if (req.body.global_ban) { if (req.body.global_ban) {
messages.push((await banPoster(req, res, next, null, posts))); messages.push((await banPoster(req, res, next, null, posts)));
} }
// if its getting deleted, we cant do anythign else // if its getting deleted, we cant do anythign else
if (req.body.delete) { if (req.body.delete) {
messages.push((await deletePosts(req, res, next, posts))); messages.push((await deletePosts(req, res, next, posts)));
@ -471,9 +494,7 @@ router.post('/global/actions', hasPerms, numberConverter, async(req, res, next)
messages.push((await dismissGlobalReports(req, res, next, posts))); messages.push((await dismissGlobalReports(req, res, next, posts)));
} }
} }
} catch (err) { } catch (err) {
//something not right
if (err.status) { if (err.status) {
// return out special error // return out special error
return res.status(err.status).render('message', err.message); return res.status(err.status).render('message', err.message);
@ -490,9 +511,8 @@ router.post('/global/actions', hasPerms, numberConverter, async(req, res, next)
}); });
router.post('/global/unban', hasPerms, numberConverter, async(req, res, next) => { router.post('/global/unban', checkPermsMiddleware, numberConverter, async(req, res, next) => {
//TODO
const errors = []; const errors = [];
if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) { if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) {
@ -511,12 +531,6 @@ router.post('/global/unban', hasPerms, numberConverter, async(req, res, next) =>
try { try {
messages.push((await removeBans(req, res, next))); messages.push((await removeBans(req, res, next)));
} catch (err) { } catch (err) {
//something not right
if (err.status) {
// return out special error
return res.status(err.status).render('message', err.message);
}
//some other error, use regular error handler
return next(err); return next(err);
} }

@ -46,13 +46,15 @@ module.exports = {
//reverse order for board page //reverse order for board page
thread.replies = replies.reverse(); thread.replies = replies.reverse();
//count omitted image and posts //temporary mitigation for deletion issue
const numPreviewImages = replies.reduce((acc, post) => { if (replies.length >= 5) {
return acc + post.files.length; //count omitted image and posts
}, 0); const numPreviewImages = replies.reduce((acc, post) => {
thread.omittedimages = thread.replyimages - numPreviewImages; return acc + post.files.length;
thread.omittedposts = thread.replyposts - replies.length; }, 0);
thread.omittedimages = thread.replyimages - numPreviewImages;
thread.omittedposts = thread.replyposts - replies.length;
}
})); }));
return threads; return threads;
@ -262,6 +264,13 @@ module.exports = {
'$exists': true '$exists': true
}, },
'board': board 'board': board
}, {
'projection': {
'salt': 0,
'password': 0,
'ip': 0,
'globalreports': 0,
}
}).toArray(); }).toArray();
}, },
@ -289,7 +298,6 @@ module.exports = {
'password': 0, 'password': 0,
'ip': 0, 'ip': 0,
'reports': 0, 'reports': 0,
'globalreports': 0,
} }
}).toArray(); }).toArray();
}, },

@ -0,0 +1,48 @@
'use strict';
const actions = [
{name:'lock', global:false, auth:true, passwords:false},
{name:'sticky', global:false, auth:true, passwords:false},
{name:'report', global:false, auth:false, passwords:false},
{name:'global_report', global:false, auth:false, passwords:false},
{name:'spoiler', global:true, auth:false, passwords:true},
{name:'delete', global:true, auth:false, passwords:true},
{name:'delete_file', global:true, auth:false, passwords:true},
{name:'dismiss', global:false, auth:true, passwords:false},
{name:'global_dismiss', global:true, auth:true, passwords:false},
{name:'ban', global:false, auth:true, passwords:false},
{name:'global_ban', global:true, auth:true, passwords:false},
];
module.exports = (req, res) => {
let anyGlobal = false
, anyAuthed = false
, anyPasswords = false
, anyValid = false;
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
const bodyHasAction = req.body[action.name];
if (bodyHasAction) {
if (!anyGlobal && action.global) {
anyGlobal = true;
}
if (!anyAuthed && action.auth) {
anyAuthed = true;
}
if (!anyPasswords && action.passwords) {
anyPasswords = true;
}
if (!anyValid) {
anyValid = true;
}
}
if (anyGlobal && anyAuthed && anyValid) {
break;
}
}
return { anyGlobal, anyAuthed, anyValid, anyPasswords };
}

@ -5,21 +5,7 @@ const uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, Bans = require(__dirname+'/../../db/bans.js') , Bans = require(__dirname+'/../../db/bans.js')
, Posts = require(__dirname+'/../../db/posts.js'); , Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next, board, checkedPosts) => { module.exports = async (req, res, next, board, posts) => {
const posts = checkedPosts;
//if user is not logged in or if logged in but not authed, they cannot ban
if (!hasPerms(req, res)) {
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': 'You do not have permission to issue bans',
'redirect': `/${req.params.board}`
}
};
}
const bans = posts.map(post => { const bans = posts.map(post => {
return { return {

@ -5,34 +5,10 @@ const path = require('path')
, fs = require('fs') , fs = require('fs')
, unlink = util.promisify(fs.unlink) , unlink = util.promisify(fs.unlink)
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, hasPerms = require(__dirname+'/../../helpers/hasperms.js')
, Mongo = require(__dirname+'/../../db/db.js') , Mongo = require(__dirname+'/../../db/db.js')
, Posts = require(__dirname+'/../../db/posts.js'); , Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next, checkedPosts) => { module.exports = async (req, res, next, posts) => {
let posts = checkedPosts;
//if user is not logged in OR if lgoged in but not authed, filter the posts by passwords that are not null
if (!hasPerms(req, res)) {
// filter posts by password only if NOT board moderator or owner
posts = posts.filter(post => {
// only include posts that have a password and that matches
return post.password != null
&& post.password.length > 0
&& post.password == req.body.password
});
if (posts.length === 0) {
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': 'Password did not match any selected posts',
'redirect': `/${req.params.board}`
}
};
}
}
//filter to threads, then get the board and thread for each //filter to threads, then get the board and thread for each
const boardThreads = posts.filter(x => x.thread == null).map(x => { const boardThreads = posts.filter(x => x.thread == null).map(x => {

@ -9,34 +9,7 @@ const path = require('path')
, Mongo = require(__dirname+'/../../db/db.js') , Mongo = require(__dirname+'/../../db/db.js')
, Posts = require(__dirname+'/../../db/posts.js'); , Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next, checkedPosts) => { module.exports = async (req, res, next, posts) => {
let posts = checkedPosts;
//if user is not logged in OR if lgoged in but not authed, filter the posts by passwords that are not null
if (!hasPerms(req, res)) {
// filter posts by password only if NOT board moderator or owner
posts = posts.filter(post => {
// only include posts that have a password and that matches
return post.password != null
&& post.password.length > 0
&& post.password == req.body.password
});
if (posts.length === 0) {
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': 'Password did not match any selected posts',
'redirect': `/${req.params.board}`
}
};
}
}
//delete posts from DB
const postMongoIds = posts.map(post => Mongo.ObjectId(post._id))
const deletedFilesPosts = await Posts.deleteFilesMany(postMongoIds).then(result => result.deletedCount);
//get filenames from all the posts //get filenames from all the posts
let fileNames = []; let fileNames = [];
@ -53,6 +26,10 @@ module.exports = async (req, res, next, checkedPosts) => {
]) ])
})); }));
//delete posts from DB
const postMongoIds = posts.map(post => Mongo.ObjectId(post._id))
const deletedFilesPosts = await Posts.deleteFilesMany(postMongoIds).then(result => result.deletedCount);
//hooray! //hooray!
return `Deleted ${fileNames.length} files across ${deletedFilesPosts} posts` return `Deleted ${fileNames.length} files across ${deletedFilesPosts} posts`

@ -5,17 +5,6 @@ const Posts = require(__dirname+'/../../db/posts.js')
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
if (!hasPerms(req, res)) {
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': `You are not authorised to dismiss reports.`,
'redirect': `/${req.params.board}`
}
};
}
const dismissedReports = await Posts.dismissReports(req.params.board, req.body.checkedposts).then(result => result.modifiedCount); const dismissedReports = await Posts.dismissReports(req.params.board, req.body.checkedposts).then(result => result.modifiedCount);
return `Dismissed ${dismissedReports} reports successfully`; return `Dismissed ${dismissedReports} reports successfully`;

@ -6,17 +6,6 @@ const Mongo = require(__dirname+'/../../db/db.js')
module.exports = async (req, res, next, posts) => { module.exports = async (req, res, next, posts) => {
if (!hasPerms(req, res)) {
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': 'You are not authorised to dismiss global reports.',
'redirect': '/'
}
};
}
const postMongoIds = posts.map(post => Mongo.ObjectId(post._id)) const postMongoIds = posts.map(post => Mongo.ObjectId(post._id))
const dismissedCount = await Posts.dismissGlobalReports(postMongoIds).then(result => result.modifiedCount); const dismissedCount = await Posts.dismissGlobalReports(postMongoIds).then(result => result.modifiedCount);

@ -9,46 +9,14 @@ const path = require('path')
, Mongo = require(__dirname+'/../../db/db.js') , Mongo = require(__dirname+'/../../db/db.js')
, Posts = require(__dirname+'/../../db/posts.js'); , Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next, checkedPosts) => { module.exports = async (req, res, next, posts) => {
//get all posts that were checked // filter to ones not spoilered
let posts = checkedPosts;
//if user is not logged in OR if lgoged in but not authed, filter the posts by passwords that are not null
if (!hasPerms(req, res)) {
//filter by password
posts = posts.filter(post => {
return post.password != null
&& post.password.length > 0
&& post.password == req.body.password
});
if (posts.length === 0) {
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': 'Password did not match any selected posts',
'redirect': '/'
}
};
}
}
//filter by not spoilered. maybe i add filters with optiins in the controller where it gets the posts?
posts = posts.filter(post => { posts = posts.filter(post => {
return !post.spoiler return !post.spoiler
}); });
if (posts.length === 0) { if (posts.length === 0) {
throw { return 'Posts already spoilered';
'status': 409,
'message': {
'title': 'Conflict',
'message': 'Posts already spoilered',
'redirect': '/'
}
};
} }
// spoiler posts // spoiler posts

@ -44,7 +44,7 @@ mixin post(post, truncate, manage=false, globalmanage=false)
let truncatedMessage = post.mesage; let truncatedMessage = post.mesage;
let truncated = false; let truncated = false;
if (messageLines > 10 || post.message.length > 1000) { if (messageLines > 10 || post.message.length > 1000) {
truncatedMessage = splitPost.slice(0, 16).join('\n'); truncatedMessage = splitPost.slice(0, 10).join('\n');
truncated = true; truncated = true;
} }
if truncated if truncated

@ -8,6 +8,8 @@ block content
ul ul
if message if message
li #{message} li #{message}
if error
li #{error}
if messages if messages
each msg in messages each msg in messages
li #{msg} li #{msg}

Loading…
Cancel
Save