diff --git a/controllers/forms.js b/controllers/forms.js index 048e2fe1..a347b9f3 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -5,6 +5,7 @@ const express = require('express') , { enableUserBoards } = require(__dirname+'/../configs/main.json') , Boards = require(__dirname+'/../db/boards.js') , Posts = require(__dirname+'/../db/posts.js') + , Bans = require(__dirname+'/../db/bans.js') , Mongo = require(__dirname+'/../db/db.js') , { remove } = require('fs-extra') , upload = require('express-fileupload') @@ -36,6 +37,7 @@ const express = require('express') }) , removeBans = require(__dirname+'/../models/forms/removebans.js') , makePost = require(__dirname+'/../models/forms/makepost.js') + , deletePosts = require(__dirname+'/../models/forms/deletepost.js') , deleteTempFiles = require(__dirname+'/../helpers/files/deletetempfiles.js') , uploadBanners = require(__dirname+'/../models/forms/uploadbanners.js') , deleteBanners = require(__dirname+'/../models/forms/deletebanners.js') @@ -53,6 +55,7 @@ const express = require('express') , verifyCaptcha = require(__dirname+'/../helpers/captcha/captchaverify.js') , actionHandler = require(__dirname+'/../models/forms/actionhandler.js') , csrf = require(__dirname+'/../helpers/checks/csrfmiddleware.js') + , uploadDirectory = require(__dirname+'/../helpers/files/uploadDirectory.js') , actionChecker = require(__dirname+'/../helpers/checks/actionchecker.js'); @@ -628,6 +631,48 @@ router.post('/board/:board/unban', csrf, Boards.exists, banCheck, isLoggedIn, ch }); +//delete board +router.post('/board/:board/deleteboard', csrf, Boards.exists, banCheck, isLoggedIn, checkPermsMiddleware(2), async (req, res, next) => { + + const errors = []; + + if (!req.body.confirm) { + errors.push('Missing confirmation'); + } + if (!req.body.uri | req.body.uri !== req.params.board) { + errors.push('URI does not match') + } + + if (errors.length > 0) { + return res.status(400).render('message', { + 'title': 'Bad request', + 'errors': errors, + 'redirect': `/${req.params.board}/manage.html` + }); + } + + try { +//todo: move this to separate model file + // could be slow, might also wanna use projection to just get the files and other info necessary for deleteposts model + await Boards.deleteOne(res.locals.board._id); + const allPosts = await Posts.allBoardPosts(res.locals.board._id); + if (allPosts.length > 0) { + await deletePosts(allPosts, res.locals.board._id, true); + } + await Bans.deleteBoard(res.locals.board._id); + await remove(`${uploadDirectory}html/${req.params.board}/`) + } catch (err) { + return next(err); + } + + return res.render('message', { + 'title': 'Success', + 'message': 'Board deleted', + 'redirect': '/' + }); + +}); + router.post('/global/unban', csrf, isLoggedIn, checkPermsMiddleware(1), paramConverter, async(req, res, next) => { const errors = []; diff --git a/db/bans.js b/db/bans.js index 22fd57b1..41541721 100644 --- a/db/bans.js +++ b/db/bans.js @@ -51,6 +51,12 @@ module.exports = { }) }, + deleteBoard: (board) => { + return db.deleteMany({ + 'board': board + }); + }, + insertOne: (ban) => { return db.insertOne(ban); }, diff --git a/db/boards.js b/db/boards.js index 25a23f90..0dc6eaea 100644 --- a/db/boards.js +++ b/db/boards.js @@ -19,6 +19,10 @@ module.exports = { return db.collection('boards').insertOne(data); }, + deleteOne: (board) => { + return db.collection('boards').deleteOne({ '_id': board }); + }, + deleteAll: (board) => { return db.collection('boards').deleteMany({}); }, diff --git a/db/posts.js b/db/posts.js index 055fea17..bf66cb61 100644 --- a/db/posts.js +++ b/db/posts.js @@ -228,6 +228,12 @@ module.exports = { }, + allBoardPosts: (board) => { + return db.find({ + 'board': board + }).toArray(); + }, + //takes array "ids" of post ids getPosts: (board, ids, admin) => { diff --git a/gulpfile.js b/gulpfile.js index def7e603..c5a5e383 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -53,7 +53,7 @@ async function wipe() { 'name': 'test', 'description': 'testing board', 'captchaMode': 0, - 'locked': true, + 'locked': false, 'tphTrigger': 10, 'tphTriggerAction': 2, 'forceAnon': true, @@ -73,7 +73,7 @@ async function wipe() { 'raw':null, 'markdown':null }, - 'filters':[] + 'filters':[], 'filterMode': 0, } }) @@ -119,15 +119,17 @@ function images() { .pipe(gulp.dest(paths.images.dest)); } -//TODO: pages here that users should edit built and output by pug e.g. homepage, FAQ, contact, privacy policy, tos, etc -async function html() { - await del([ 'static/html/*' ]); //these will be now build-on-load +function html() { + return del([ 'static/html/*' ]); //these will be now build-on-load +} + +function custompages() { return gulp.src(paths.pug.src) .pipe(pug()) .pipe(gulp.dest(paths.pug.dest)); } -const build = gulp.parallel(css, images, html); +const build = gulp.parallel(css, images, html, custompages); const reset = gulp.series(wipe, build) module.exports = { @@ -135,5 +137,6 @@ module.exports = { css, images, reset, + custompages, default: build, }; diff --git a/models/forms/create.js b/models/forms/create.js index 84778295..3cdcc0c4 100644 --- a/models/forms/create.js +++ b/models/forms/create.js @@ -33,7 +33,7 @@ module.exports = async (req, res, next) => { 'settings': { name, description, - 'locked': true, + 'locked': false, 'captchaMode': 0, 'tphTrigger': 0, 'tphTriggerAction': 0, diff --git a/models/forms/deletepost.js b/models/forms/deletepost.js index b5c6c272..80be4d24 100644 --- a/models/forms/deletepost.js +++ b/models/forms/deletepost.js @@ -16,21 +16,21 @@ const uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory. } } -module.exports = async (posts, board) => { +module.exports = async (posts, board, all=false) => { //filter to threads const threads = posts.filter(x => x.thread == null); - //delete the html for threads - const deleteHTML = [] - for (let i = 0; i < threads.length; i++) { - deleteHTML.push(remove(`${uploadDirectory}html/${threads[i].board}/thread/${threads[i].postId}.html`)); + if (threads.length > 0) { + //delete the html for threads + await Promise.all(threads.map(thread => { + remove(`${uploadDirectory}html/${threads.board}/thread/${threads.postId}.html`) + })); } - await Promise.all(deleteHTML); //get posts from all threads let threadPosts = [] - if (threads.length > 0) { + if (all === false && threads.length > 0) { if (board) { //if this is board-specific, we can use a single query const threadPostIds = threads.map(thread => thread.postId); @@ -66,81 +66,83 @@ module.exports = async (posts, board) => { await Files.decrement(fileNames); } - //use this to not do unnecessary actions for posts where the thread is being deleted - const deleteThreadMap = {}; - for (let i = 0; i < threads.length; i++) { - const thread = threads[i]; - //if exists, add to set, else make the set - if (!deleteThreadMap[thread.board]) { - deleteThreadMap[thread.board] = new Set(); + const bulkWrites = []; + if (all === false) { //no need to rebuild quotes when deleting all posts for a board + const deleteThreadMap = {}; + for (let i = 0; i < threads.length; i++) { + const thread = threads[i]; + //if exists, add to set, else make the set + if (!deleteThreadMap[thread.board]) { + deleteThreadMap[thread.board] = new Set(); + } + deleteThreadMap[thread.board].add(thread.postId); } - deleteThreadMap[thread.board].add(thread.postId); - } - const bulkWrites = []; - const backlinkRebuilds = new Set(); - for (let j = 0; j < allPosts.length; j++) { - const post = allPosts[j]; - backlinkRebuilds.delete(post._id); //make sure we dont try and remarkup this post since its getting deleted. - if (post.thread != null && !deleteThreadMap[post.board] || !deleteThreadMap[post.board].has(post.thread)) { - //get backlinks for posts to remarkup - for (let i = 0; i < post.backlinks.length; i++) { - const backlink = post.backlinks[i]; - if (!backlinkRebuilds.has(backlink._id)) { - backlinkRebuilds.add(backlink._id); + const backlinkRebuilds = new Set(); + for (let j = 0; j < allPosts.length; j++) { + const post = allPosts[j]; + backlinkRebuilds.delete(post._id); //make sure we dont try and remarkup this post since its getting deleted. + if (post.thread != null && !deleteThreadMap[post.board] || !deleteThreadMap[post.board].has(post.thread)) { + //get backlinks for posts to remarkup + for (let i = 0; i < post.backlinks.length; i++) { + const backlink = post.backlinks[i]; + if (!backlinkRebuilds.has(backlink._id)) { + backlinkRebuilds.add(backlink._id); + } } - } - //remove dead backlinks to this post - if (post.quotes.length > 0) { - bulkWrites.push({ - 'updateMany': { - 'filter': { - '_id': { - '$in': post.quotes.map(q => q._id) - } - }, - 'update': { - '$pull': { - 'backlinks': { - 'postId': post.postId + //remove dead backlinks to this post + if (post.quotes.length > 0) { + bulkWrites.push({ + 'updateMany': { + 'filter': { + '_id': { + '$in': post.quotes.map(q => q._id) + } + }, + 'update': { + '$pull': { + 'backlinks': { + 'postId': post.postId + } } } } - } - }); + }); + } } } - } //deleting before remarkup so quotes are accurate const deletedPosts = await Posts.deleteMany(postMongoIds).then(result => result.deletedCount); - //get posts that quoted deleted posts so we can remarkup them - if (backlinkRebuilds.size > 0) { - const remarkupPosts = await Posts.globalGetPosts([...backlinkRebuilds]); - await Promise.all(remarkupPosts.map(async post => { //doing these all at once - if (post.nomarkup && post.nomarkup.length > 0) { //is this check even necessary? how would it have a quote with no message - //redo the markup - let message = simpleMarkdown(post.nomarkup); - const { quotedMessage, threadQuotes } = await linkQuotes(post.board, post.nomarkup, post.thread); - message = sanitize(quotedMessage, sanitizeOptions); - bulkWrites.push({ - 'updateOne': { - 'filter': { - '_id': post._id - }, - 'update': { - '$set': { - 'quotes': threadQuotes, - 'message': message - } - } - } - }); - } - })); + if (all === false) { + //get posts that quoted deleted posts so we can remarkup them + if (backlinkRebuilds.size > 0) { + const remarkupPosts = await Posts.globalGetPosts([...backlinkRebuilds]); + await Promise.all(remarkupPosts.map(async post => { //doing these all at once + if (post.nomarkup && post.nomarkup.length > 0) { //is this check even necessary? how would it have a quote with no message + //redo the markup + let message = simpleMarkdown(post.nomarkup); + const { quotedMessage, threadQuotes } = await linkQuotes(post.board, post.nomarkup, post.thread); + message = sanitize(quotedMessage, sanitizeOptions); + bulkWrites.push({ + 'updateOne': { + 'filter': { + '_id': post._id + }, + 'update': { + '$set': { + 'quotes': threadQuotes, + 'message': message + } + } + } + }); + } + })); + } } //bulkwrite it all diff --git a/models/forms/makepost.js b/models/forms/makepost.js index e2eedfee..e854c950 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -358,7 +358,7 @@ module.exports = async (req, res, next) => { 'board': res.locals.board._id }); //if its above the trigger - if (tph > tphTrigger) { + if (tph >= tphTrigger) { //update in memory for other stuff done e.g. rebuilds const update = { '$set': {} diff --git a/views/pages/manage.pug b/views/pages/manage.pug index 08f77399..0192b4dd 100644 --- a/views/pages/manage.pug +++ b/views/pages/manage.pug @@ -102,6 +102,19 @@ block content input(type='text' name='ban_duration' placeholder='e.g. 1w' value=board.settings.filterBanDuration) input(type='submit', value='save settings') hr(size=1) + h4.no-m-p Delete board: + section.form-wrapper.flexleft.mv-10 + form.form-post(action=`/forms/board/${board._id}/deleteboard`, enctype='application/x-www-form-urlencoded', method='POST') + input(type='hidden' name='_csrf' value=csrf) + section.row + .label I'm sure + label.postform-style.ph-5 + input(type='checkbox', name='confirm', value='true' required) + section.row + .label Board URI + input(type='text' name='uri' required) + input(type='submit', value='submit') + hr(size=1) h4.no-m-p Add Banners: section.form-wrapper.flexleft.mv-10 form.form-post(action=`/forms/board/${board._id}/addbanners`, enctype='multipart/form-data', method='POST')