code:
'use strict';

module.exports = async(db, redis) => {
	console.log('add globalsettings board default option, and tegaki replay mime to allowed mime types');
	await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
		'$set': {
			'boardDefaults.hideBanners': false,
			'overboardReverseLinks': true,
		},
		'$push': {
			'otherMimeTypes': 'tegaki/replay',
		},
	});
	console.log('add board option to hide banners and [banners] link');
	await db.collection('boards').updateMany({
		'webring': false,
	}, {
		'$set':{
			'settings.hideBanners': false,
		},
	});
	console.log('Clearing globalsettings cache');
	await redis.deletePattern('globalsettings');
	console.log('Clearing boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('make captcha font option apply to grid captcha too');
	await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
		'$unset': {
			'captchaOptions.text.font': '',
		},
		'$set': {
			'captchaOptions.font': 'default',
		}
	});
	console.log('Clearing globalsettings cache');
	await redis.deletePattern('globalsettings');
};

repo_name: diegolegion/algochan

license: mit


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Updating edited posts username property for "hidden users" to support localisation');
	await db.collection('posts').updateMany({
		'edited.username': 'Hidden User',
	}, {
		'$set': {
			'edited.username': null,
		},
	});
	console.log('Updating db for language settings');
	await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
		'$set': {
			'language': 'en-GB',
		},
	});
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.language': 'en-GB',
		},
	});
	await db.collection('modlog').updateMany({
		'actions': 'Edit',
	}, {
		'$set': {
			'actions': ['Edit'],
		},
	});
	console.log('Clearing globalsettings cache');
	await redis.deletePattern('globalsettings');
	console.log('Clearing boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Updating db for language settings, fixes broken 1.0.0 migration');
	await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
		'$set': {
			'boardDefaults.language': 'en-GB',
		},
	});

	console.log('Clearing globalsettings cache');
	await redis.deletePattern('globalsettings');
	console.log('Clearing boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {

	console.log('Updating globalsettings to add filter limit');
	await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
		'$set': {
			'globalLimits.filters': {
				max: 50,
			},
		},
	});

	console.log('Updating globalsettings to convert old filters into new filter db format');
	const globalSettings = await db.collection('globalsettings').findOne({ _id: 'globalsettings' });
	if (globalSettings.filters.length > 0) {
		await db.collection('filters').insertOne({
			board: null,
			filters: globalSettings.filters,
			filterMode: globalSettings.filterMode,
			filterMessage: null,
			filterBanDuration: globalSettings.filterBanDuration,
			filterBanAppealable: globalSettings.filterBanAppealable ? true : false,
			strictFiltering: globalSettings.strictFiltering ? true : false,
		});
	}
	await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
		'$unset': {
			filters: '',
			filterMode: '',
			filterBanDuration: '',
			filterBanAppealable: '',
			strictFiltering: '',
			'boardDefaults.filters': '',
			'boardDefaults.filterMode': '',
			'boardDefaults.filterBanDuration': '',
			'boardDefaults.filterBanAppealable': '',
			'boardDefaults.strictFiltering': '',
		},
	});

	console.log('Updating boards to convert old filters into new filter db format');
	const allBoards = await db.collection('boards').find({}).toArray();
	for (let b of allBoards) {
		if (b.settings.filters.length > 0) {
			await db.collection('filters').insertOne({
				board: b._id,
				filters: b.settings.filters,
				filterMode: b.settings.filterMode,
				filterMessage: null,
				filterBanDuration: b.settings.filterBanDuration,
				filterBanAppealable: b.settings.filterBanAppealable ? true : false,
				strictFiltering: b.settings.strictFiltering ? true : false,
			});
		}
	}
	await db.collection('boards').updateMany({}, {
		'$unset': {
			'settings.filters': '',
			'settings.filterMode': '',
			'settings.filterBanDuration': '',
			'settings.filterBanAppealable': '',
			'settings.strictFiltering': '',
		},
	});

	console.log('Clearing globalsettings cache');
	await redis.deletePattern('globalsettings');
	console.log('Clearing boards cache');
	await redis.deletePattern('board:*');

};


code:
'use strict';

module.exports = async(db, redis) => {

	console.log('Updating globalsettings to add uriDecodeFileNames');
	await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
		'$set': {
			'uriDecodeFileNames': false,
		},
	});

	console.log('Clearing globalsettings cache');
	await redis.deletePattern('globalsettings');

};


code:
'use strict';

module.exports = async(db, redis) => {

	console.log('Updating globalsettings to add web3 settings');
	await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
		'$set': {
			'enableWeb3': false,
			'ethereumLinksURL': 'https://etherscan.io/address/%s',
		},
	});

	console.log('Updating boards to add web3 settings');
	await db.collection('boards').updateMany({}, {
		'$set': {
			'enableWeb3': false,
		},
	});

	console.log('Clearing globalsettings cache');
	await redis.deletePattern('globalsettings');
	console.log('Clearing boards cache');
	await redis.deletePattern('board:*');

};


code:
'use strict';

//Note: if I drastically change permissions lib in future this might break. hmmmmm......
const { Permissions } = require(__dirname+'/../lib/permission/permissions.js')
	, Permission = require(__dirname+'/../lib/permission/permission.js')
	, Mongo = require(__dirname+'/../db/db.js');

module.exports = async(db, redis) => {

	console.log('Updating account permissions to shift some bits');
	const accounts = await db.collection('accounts').find().toArray();
	for (let acc of accounts) {
		const updatingPerms = new Permission(acc.permissions.toString('base64'));
		console.log('updating', acc._id);
		//take the old permissions
		const INITIAL_MANAGE_GLOBAL_GENERAL = updatingPerms.get(10);
		const INITIAL_MANAGE_GLOBAL_BANS = updatingPerms.get(11);
		//move them to new bits
		updatingPerms.set(Permissions.MANAGE_GLOBAL_GENERAL, INITIAL_MANAGE_GLOBAL_GENERAL);
		updatingPerms.set(Permissions.MANAGE_GLOBAL_BANS, INITIAL_MANAGE_GLOBAL_BANS);
		//add new anonymizer bypass permission, default false
		updatingPerms.set(Permissions.BYPASS_ANONYMIZER_RESTRICTIONS, false);
		//write the change to DB
		await db.collection('accounts').updateOne({
			'_id': acc._id,
		}, {
			'$set': {
				'permissions': Mongo.Binary(updatingPerms.array),
			}
		});
	}

	console.log('Updating role permissions to shift some bits');
	const roles = await db.collection('roles').find().toArray();
	for (let r of roles) {
		const updatingPerms = new Permission(r.permissions.toString('base64'));
		console.log('updating', r.name);
		//take the old permissions
		const INITIAL_MANAGE_GLOBAL_GENERAL = updatingPerms.get(10);
		const INITIAL_MANAGE_GLOBAL_BANS = updatingPerms.get(11);
		//move them to new bits
		updatingPerms.set(Permissions.MANAGE_GLOBAL_GENERAL, INITIAL_MANAGE_GLOBAL_GENERAL);
		updatingPerms.set(Permissions.MANAGE_GLOBAL_BANS, INITIAL_MANAGE_GLOBAL_BANS);
		//add new anonymizer bypass permission, default false
		updatingPerms.set(Permissions.BYPASS_ANONYMIZER_RESTRICTIONS, false);
		//write the change to DB
		await db.collection('role').updateOne({
			'_id': r._id,
		}, {
			'$set': {
				'permissions': Mongo.Binary(updatingPerms.array),
			}
		});
	}

	console.log('Clearing globalsettings cache');
	await redis.deletePattern('globalsettings');
	console.log('Clearing sessions and users cache');
	await redis.deletePattern('users:*');
	await redis.deletePattern('sess:*');

};


code:
'use strict';

module.exports = async(db) => {

	console.log('Updating modlogs to add public flag');
	await db.collection('modlog').updateMany({}, {
		'$set': {
			'public': true,
		},
	});

};


code:
'use strict';

module.exports = async(db) => {

	console.log('Updating all bans with board: null to be global true');
	await db.collection('bans').updateMany({
		board: null,
	}, {
		'$set': {
			'global': true,
		},
	});

};


code:
'use strict';

const fs = require('fs')
	, semver = require('semver');

fs.readdirSync(__dirname).forEach(file => {
	const name = file.substring(0, file.length-3);
	if (!semver.valid(name)) { return; }
	module.exports[name] = require(__dirname+'/'+file);
});


code:
'use strict';

const { Posts, Boards, Modlogs } = require(__dirname+'/../../db/')
	, Mongo = require(__dirname+'/../../db/db.js')
	, banPoster = require(__dirname+'/banposter.js')
	, deletePosts = require(__dirname+'/deletepost.js')
	, spoilerPosts = require(__dirname+'/spoilerpost.js')
	, stickyPosts = require(__dirname+'/stickyposts.js')
	, bumplockPosts = require(__dirname+'/bumplockposts.js')
	, lockPosts = require(__dirname+'/lockposts.js')
	, cyclePosts = require(__dirname+'/cycleposts.js')
	, deletePostsFiles = require(__dirname+'/deletepostsfiles.js')
	, reportPosts = require(__dirname+'/reportpost.js')
	, dismissReports = require(__dirname+'/dismissreport.js')
	, movePosts = require(__dirname+'/moveposts.js')
	, { remove } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, ModlogActions = require(__dirname+'/../../lib/input/modlogactions.js')
	, getAffectedBoards = require(__dirname+'/../../lib/misc/affectedboards.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js')
	, { postPasswordSecret } = require(__dirname+'/../../configs/secrets.js')
	, threadRegex = /\/[a-z0-9]+\/(?:manage\/)?thread\/(\d+)\.html/i
	, { createHash, timingSafeEqual } = require('crypto');

module.exports = async (req, res, next) => {

	const { __ } = res.locals;

	//try to set a good redirect
	let redirect = req.headers.referer;
	if (!redirect) {
		if (!req.params.board) {
			redirect = '/globalmanage/recent.html';
		} else {
			redirect = `/${req.params.board}/${req.path.endsWith('modactions') ? 'manage/reports' : 'index'}.html`;
		}
	}

	/*
		Handle checking passwords (in a time-constant) when doing actions that require a password.
		Staff skip this section because they don't need passwords to do such actions.
	*/
	const isStaffOrGlobal = res.locals.permissions.hasAny(Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_BOARD_GENERAL);
	if (!isStaffOrGlobal && res.locals.actions.numPasswords > 0) {
		let passwordPosts = [];
		if (req.body.postpassword && req.body.postpassword.length > 0) {
			const inputPasswordHash = createHash('sha256').update(postPasswordSecret + req.body.postpassword).digest('base64');
			const inputPasswordBuffer = Buffer.from(inputPasswordHash);
			passwordPosts = res.locals.posts.filter(post => {
				if (post.password != null) { //null password doesnt matter for timing attack, it cant be deleted by non-staff
					const postBuffer = Buffer.from(post.password);
					return timingSafeEqual(inputPasswordBuffer, postBuffer);
				}
			});
		}
		if (passwordPosts.length === 0) {
			return dynamicResponse(req, res, 403, 'message', {
				'title': __('Forbidden'),
				'error': __('Password did not match any selected posts'),
				redirect,
			});
		}
		//if the password is correct for at least *some* posts, silently ignore the wrong ones (dont action them), and continue.
		res.locals.posts = passwordPosts;
	}

	const deleting = req.body.delete || req.body.delete_ip_board || req.body.delete_ip_global || req.body.delete_ip_thread;

	//affected boards, their threads, and how many pages each one has before the actions
	let { boardThreadMap, numPagesBeforeActions, affectedBoardNames } = await getAffectedBoards(res.locals.posts, deleting);
	let minimalThreadsMap = await Posts.getMinimalThreads(affectedBoardNames);

	//adjust the redirect to go back to the thread if it was done from there
	if (deleting
		&& req.params.board
		&& req.headers.referer
		&& boardThreadMap[req.params.board]) {
		const threadRefMatch = req.headers.referer.match(threadRegex);
		if (threadRefMatch && boardThreadMap[req.params.board].directThreads.has(+threadRefMatch[1])) {
			redirect = `/${req.params.board}/${req.path.endsWith('modactions') ? 'manage/' : ''}index.html`;
		}
	}

	const messages = [];
	const modlogActions = [];
	const combinedQuery = {};
	let recalculateThreadMetadata = false;

	//handle bans, independent of other actions
	if (req.body.ban || req.body.global_ban || req.body.report_ban || req.body.global_report_ban) {
		const { message, action, query } = await banPoster(req, res, next);
		if (req.body.ban) {
			modlogActions.push(ModlogActions.BAN);
		} else if (req.body.global_ban) {
			modlogActions.push(ModlogActions.GLOBAL_BAN);
		}
		if (req.body.report_ban) {
			modlogActions.push(ModlogActions.BAN_REPORTER);
		} else if (req.body.global_report_ban) {
			modlogActions.push(ModlogActions.GLOBAL_BAN_REPORTER);
		}
		if (action) {
			combinedQuery[action] = { ...combinedQuery[action], ...query};
		}
		messages.push(message);
	}

	if (deleting) {

		//OP delete protection. for old OPs or with a lot of replies
		if (!isStaffOrGlobal) { //TODO: make this use a permission bit
			const { deleteProtectionAge, deleteProtectionCount } = res.locals.board.settings;
			if (deleteProtectionAge > 0 || deleteProtectionCount > 0) {
				const protectedThread = res.locals.posts.some(p => {
					return p.thread === null //is a thread
						&& ((deleteProtectionCount > 0 && p.replyposts > deleteProtectionCount) //and it has more replies than the protection count
							|| (deleteProtectionAge > 0 && new Date() > new Date(p.date.getTime() + deleteProtectionAge))); //or was created too long ago
				});
				if (protectedThread === true) {
					return dynamicResponse(req, res, 403, 'message', {
						'title': __('Forbidden'),
						'error': __('You cannot delete old threads or threads with too many replies'),
						redirect,
					});
				}
			}
		}

		const postsBefore = res.locals.posts.length;

		if (req.body.delete_ip_board || req.body.delete_ip_global || req.body.delete_ip_thread) {
			const deletePostIps = res.locals.posts.map(x => x.ip.cloak);
			const deletePostMongoIds = res.locals.posts.map(x => x._id);
			let query = {
				'_id': {
					'$nin': deletePostMongoIds
				},
				'ip.cloak': {
					'$in': deletePostIps
				}
			};
			if (req.body.delete_ip_thread) {
				const ips_threads = [...boardThreadMap[req.params.board].threads];
				query['board'] = req.params.board;
				query['$or'] = [
					{
						'thread': {
							'$in': ips_threads
						}
					},
					{
						'postId': {
							'$in': ips_threads
						}
					}
				];
			} else if (req.body.delete_ip_board) {
				query['board'] = req.params.board;
			}
			const deleteIpPosts = await Posts.db.find(query).toArray();
			res.locals.posts = res.locals.posts.concat(deleteIpPosts);
		}
		if (res.locals.posts.length > postsBefore) {
			//recalc for extra fetched posts
			({ boardThreadMap, numPagesBeforeActions, affectedBoardNames } = await getAffectedBoards(res.locals.posts, deleting));
			minimalThreadsMap = await Posts.getMinimalThreads(affectedBoardNames);
		}

		if (req.body.delete_file) {
			const { message } = await deletePostsFiles(res.locals, false); //delete files, not just unlink
			messages.push(message);
		}
		const { action, message } = await deletePosts(res.locals.posts, req.body.delete_ip_global ? null : req.params.board, res.locals);
		messages.push(message);
		if (action) {
			if (req.body.delete) {
				modlogActions.push(ModlogActions.DELETE);
			} else if (req.body.delete_ip_board) {
				modlogActions.push(ModlogActions.DELETE_BY_IP);
			} else if (req.body.delete_ip_global) {
				modlogActions.push(ModlogActions.GLOBAL_DELETE_BY_IP);
			}
			recalculateThreadMetadata = true;
		}

	} else if (req.body.move) {

		if (boardThreadMap[req.params.board].directThreads.size > 0) {
			const threadIds = [...boardThreadMap[req.params.board].directThreads];
			const fetchMovePosts = await Posts.db.find({
				'board': req.params.board,
				'thread': {
					'$in': threadIds
				}
			}).toArray();
			res.locals.posts = res.locals.posts.concat(fetchMovePosts);
		}
		const { message, action } = await movePosts(req, res);
		if (action) {
			modlogActions.push(ModlogActions.MOVE);
			recalculateThreadMetadata = true;
			if (res.locals.destinationBoard && res.locals.destinationThread) {
				res.locals.posts.push(res.locals.destinationThread);
				({ boardThreadMap, numPagesBeforeActions, affectedBoardNames } = await getAffectedBoards(res.locals.posts, deleting));
				minimalThreadsMap = await Posts.getMinimalThreads(affectedBoardNames);
			}
		}
		messages.push(message);

	} else {

		// if it was getting deleted/moved, dont do these actions
		if (req.body.unlink_file || req.body.delete_file) {
			const { message, action, query } = await deletePostsFiles(res.locals, req.body.unlink_file);
			if (action) {
				if (req.body.unlink_file) {
					modlogActions.push(ModlogActions.UNLINK_FILES);
				} else if (req.body.delete_file) {
					modlogActions.push(ModlogActions.DELETE_FILES);
				}
				recalculateThreadMetadata = true;
				combinedQuery[action] = { ...combinedQuery[action], ...query};
			}
			messages.push(message);
		} else if (req.body.spoiler) {
			const { message, action, query } = spoilerPosts(res.locals);
			if (action) {
				modlogActions.push(ModlogActions.SPOILER_FILES);
				combinedQuery[action] = { ...combinedQuery[action], ...query};
			}
			messages.push(message);
		}
		//lock, sticky, bumplock, cyclic
		if (req.body.bumplock) {
			const { message, action, query } = bumplockPosts(res.locals);
			if (action) {
				modlogActions.push(ModlogActions.BUMPLOCK);
				combinedQuery[action] = { ...combinedQuery[action], ...query};
			}
			messages.push(message);
		}
		if (req.body.lock) {
			const { message, action, query } = lockPosts(res.locals);
			if (action) {
				modlogActions.push(ModlogActions.LOCK);
				combinedQuery[action] = { ...combinedQuery[action], ...query};
			}
			messages.push(message);
		}
		if (req.body.sticky != null) {
			const { message, action, query } = stickyPosts(res.locals, req.body.sticky);
			if (action) {
				modlogActions.push(ModlogActions.STICKY);
				combinedQuery[action] = { ...combinedQuery[action], ...query};
			}
			messages.push(message);
		}
		if (req.body.cyclic) {
			const { message, action, query } = cyclePosts(res.locals);
			if (action) {
				modlogActions.push(ModlogActions.CYCLE);
				combinedQuery[action] = { ...combinedQuery[action], ...query};
			}
			messages.push(message);
		}
		// cannot report and dismiss at same time
		if (req.body.report || req.body.global_report) {
			const { message, action, query } = reportPosts(req, res);
			if (action) {
				//no modlog entry for making reports
				combinedQuery[action] = { ...combinedQuery[action], ...query};
			}
			messages.push(message);
		} else if (req.body.dismiss || req.body.global_dismiss) {
			const { message, action, query } = dismissReports(req, res);
			if (action) {
				if (req.body.dismiss) {
					modlogActions.push(ModlogActions.DISMISS);
				} else if (req.body.global_dismiss) {
					modlogActions.push(ModlogActions.GLOBAL_DISMISS);
				}
				combinedQuery[action] = { ...combinedQuery[action], ...query};
			}
			messages.push(message);
		}

	}

	//execute the actions from the resulting combined query in one shot
	if (Object.keys(combinedQuery).length > 0) {
		await Posts.db.updateMany({
			'_id': {
				'$in': res.locals.posts.map(p => Mongo.ObjectId(p._id))
			}
		}, combinedQuery);
	}

	//fetch boards for templates if necessary. can be multiple boards from global actions
	let buildBoards = {};
	if (modlogActions.length > 0 || res.locals.actions.numBuild > 0) {
		buildBoards = (await Boards.db.find({
			'_id': {
				'$in': affectedBoardNames
			},
		}).toArray()).reduce((acc, curr) => {
			if (!acc[curr._id]) {
				acc[curr._id] = curr;
			}
			return acc;
		}, buildBoards);
	}

	const parallelPromises = [];

	//modlog
	if (modlogActions.length > 0) {
		const modlog = {};
		const logDate = new Date(); //all events current date
		const message = req.body.log_message || null;
		let logUser = null;
		//could even do if (req.session.user) {...}, but might cause cross-board log username contamination
		if (isStaffOrGlobal) {
			logUser = req.session.user;
		}
		for (let i = 0; i < res.locals.posts.length; i++) {
			const post = res.locals.posts[i];
			if (!modlog[post.board]) {
				//per board actions, all actions combined to one event
				modlog[post.board] = {
					showLinks: !deleting,
					postLinks: [],
					actions: modlogActions,
					public: true,
					date: logDate,
					showUser: !req.body.hide_name || logUser === null ? true : false,
					message: message,
					user: logUser,
					ip: {
						cloak: res.locals.ip.cloak,
						raw: res.locals.ip.raw
					}
				};
			}
			modlog[post.board].postLinks.push({
				postId: post.postId,
				thread: res.locals.destinationThread ? res.locals.destinationThread.postId : post.thread,
				board: res.locals.destinationBoard ? res.locals.destinationBoard._id : post.board,
			});
		}
		const modlogDocuments = [];
		for (let i = 0; i < affectedBoardNames.length; i++) {
			const boardName = affectedBoardNames[i];
			const boardLog = modlog[boardName];
			//make it into documents for the db
			modlogDocuments.push({
				...boardLog,
				'board': boardName
			});
		}
		if (modlogDocuments.length > 0) {
			//insert the modlog docs
			await Modlogs.insertMany(modlogDocuments);
			for (let i = 0; i < affectedBoardNames.length; i++) {
				const board = buildBoards[affectedBoardNames[i]];
				buildQueue.push({
					'task': 'buildModLog',
					'options': {
						'board': board,
					}
				});
				buildQueue.push({
					'task': 'buildModLogList',
					'options': {
						'board': board,
					}
				});
			}
		}
	}

	//if there are actions that can cause some rebuilding
	if (res.locals.actions.numBuild > 0) {

		//Make a map of all the unique threads for any posts we selected in those threads (and directly selected threads)
		const queryOrs = [];
		for (let i = 0; i < affectedBoardNames.length; i++) {
			const threadBoard = affectedBoardNames[i];
			//convert this to an array while we are here
			boardThreadMap[threadBoard].threads = [...boardThreadMap[threadBoard].threads];
			boardThreadMap[threadBoard].directThreads = [...boardThreadMap[threadBoard].directThreads];
			queryOrs.push({
				'board': threadBoard,
				'postId': {
					'$in': boardThreadMap[threadBoard].threads
				}
			});
		}
		let threadsEachBoard = [];
		if (queryOrs.length > 0) {
			threadsEachBoard = await Posts.db.find({
				'thread': null,
				'$or': queryOrs
			}).toArray();
		}
		const selectedThreads = res.locals.posts.filter(post => post.thread === null);
		threadsEachBoard = threadsEachBoard.concat(selectedThreads);

		//recalculate replies and image counts if necessary
		if (recalculateThreadMetadata) {
			const selectedPosts = res.locals.posts
				.filter(p => p.thread !== null);
			if (selectedPosts.length > 0) {
				const replyOrs = selectedPosts
					.map(p => ({ board: p.board, thread: p.thread }));
				if (req.body.move && res.locals.destinationBoard && res.locals.destinationThread) {
					replyOrs.push({ board: res.locals.destinationThread.board, thread: res.locals.destinationThread.postId });
				}
				const threadReplyAggregates = await Posts.getThreadAggregates(replyOrs);
				const bulkWrites = [];
				const threads = threadsEachBoard;
				for (let i = 0; i < threads.length; i++) {
					const replyAggregate = threadReplyAggregates.find(ra => ra._id.thread === threads[i].postId && ra._id.board === threads[i].board);
					if (!replyAggregate) {
						//thread no longer has any reply post/files, set to 0 and reset bump date to post date
						bulkWrites.push({
							'updateOne': {
								'filter': {
									'postId': threads[i].postId,
									'board': threads[i].board
								},
								'update': {
									'$set': {
										'replyposts': 0,
										'replyfiles': 0,
										'bumped': threads[i].date
									},
								}
							}
						});
					} else {
						//use results from first aggregate for threads with replies still existing
						const aggregateSet = {
							'replyposts': replyAggregate.replyposts,
							'replyfiles': replyAggregate.replyfiles,
						};
						if (!threads[i].bumplocked) {
							aggregateSet['bumped'] = replyAggregate.bumped;
						}
						bulkWrites.push({
							'updateOne': {
								'filter': {
									'postId': replyAggregate._id.thread,
									'board': replyAggregate._id.board
								},
								'update': {
									'$set': aggregateSet,
								}
							}
						});
					}
				}
				if (bulkWrites.length > 0) {
					await Posts.db.bulkWrite(bulkWrites);
				}
			}
			await Posts.fixLatest(affectedBoardNames);
		}

		/*
			Get a minimal data of the threads for each affected board, used to get the page of a thread later.
			Using the proper ordering of threads, to account for sticky, bumplocks, etc.
		*/
		const pageBounds = threadsEachBoard.reduce((acc, t) => {
			if (!minimalThreadsMap[t.board]) { return acc; }
			if (!acc[t.board]) { acc[t.board] = { first: null, last: null }; }
			const threadIndex = minimalThreadsMap[t.board].findIndex(p => p.postId === t.postId);
			const threadPage = Math.max(1, Math.ceil((threadIndex+1)/10));
			if (!acc[t.board].first || threadPage < acc[t.board].first) {
				acc[t.board].first = threadPage;
			}
			if (!acc[t.board].last || threadPage > acc[t.board].last) {
				acc[t.board].last = threadPage;
			}
			return acc;
		}, {});

		for (let i = 0; i < affectedBoardNames.length; i++) {

			//always assume catalog rebuild, gets set to false in specific cases later
			let catalogRebuild = true;

			//get the board data for build tasks, and highest/lowest affected pages for rebuilding
			const boardName = affectedBoardNames[i];
			const board = buildBoards[boardName];

			//rebuild destination thread for "move" action
			if (req.body.move) {
				buildQueue.push({
					'task': 'buildThread',
					'options': {
						'threadId': req.body.move_to_thread,
						'board': board,
					}
				});
			}

			//rebuild affected threads
			for (let j = 0; j < boardThreadMap[boardName].threads.length; j++) {
				buildQueue.push({
					'task': 'buildThread',
					'options': {
						'threadId': boardThreadMap[boardName].threads[j],
						'board': board,
					}
				});
			}

			//use to compare number of pages after actions. fetch from db again if any actions that can change number of pages (delete and move)
			let numPagesAfterActions = numPagesBeforeActions[boardName];
			if (deleting || req.body.move) {
				numPagesAfterActions = Math.ceil((await Posts.getPages(boardName)) / 10);
			}

			if ((numPagesBeforeActions[boardName] && numPagesBeforeActions[boardName] !== numPagesAfterActions) || req.body.move) {

				//if number of pages changed, or doing a "move", rebuild all pages for simplicity and delete any pages that would no longer exist
				if (numPagesAfterActions < numPagesBeforeActions[boardName]) {
					for (let k = numPagesBeforeActions[boardName]; k > numPagesAfterActions; k--) {
						parallelPromises.push(remove(`${uploadDirectory}/html/${boardName}/${k}.html`));
						parallelPromises.push(remove(`${uploadDirectory}/json/${boardName}/${k}.json`));
					}
				}
				buildQueue.push({
					'task': 'buildBoardMultiple',
					'options': {
						'board': board,
						'startpage': 1,
						'endpage': numPagesAfterActions,
					}
				});

			} else {

				//build between pages
				const rebuildPageFirst = pageBounds[boardName].first;
				const rebuildPageLast = pageBounds[boardName].last;

				if (deleting) {

					if (boardThreadMap[boardName].directThreads.length === 0) {
						//only deleting posts from threads, so thread order wont change, thus we dont delete all pages after
						buildQueue.push({
							'task': 'buildBoardMultiple',
							'options': {
								'board': board,
								'startpage': rebuildPageFirst,
								'endpage': rebuildPageLast,
							}
						});
					} else {
						//deleting threads, so we delete all pages after
						buildQueue.push({
							'task': 'buildBoardMultiple',
							'options': {
								'board': board,
								'startpage': rebuildPageFirst,
								'endpage': numPagesAfterActions,
							}
						});
					}

				} else if (req.body.sticky) {

					//rebuild current and newer pages
					buildQueue.push({
						'task': 'buildBoardMultiple',
						'options': {
							'board': board,
							'startpage': 1,
							'endpage': rebuildPageLast,
						}
					});

				} else if (req.body.lock || req.body.bumplock || req.body.cyclic || req.body.unlink_file) {

					buildQueue.push({
						'task': 'buildBoardMultiple',
						'options': {
							'board': board,
							'startpage': rebuildPageFirst,
							'endpage': rebuildPageLast,
						}
					});

				} else if (req.body.spoiler || req.body.ban || req.body.global_ban) {

					buildQueue.push({
						'task': 'buildBoardMultiple',
						'options': {
							'board': board,
							'startpage': rebuildPageFirst,
							'endpage': numPagesAfterActions,
						}
					});
					//these actions dont affect the catalog tiles if no OPs selected, so dont bother rebuilding the catalog
					if (boardThreadMap[boardName].directThreads.length === 0) {
						catalogRebuild = false;
					}

				}

			}

			if (catalogRebuild) {

				buildQueue.push({
					'task': 'buildCatalog',
					'options': {
						'board': board,
					}
				});

			}

		}
	}

	if (parallelPromises.length > 0) {
		await Promise.all(parallelPromises);
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'messages': messages,
		redirect,
	});

};


code:
'use strict';

const { remove, pathExists } = require('fs-extra')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, moveUpload = require(__dirname+'/../../lib/file/moveupload.js')
	, mimeTypes = require(__dirname+'/../../lib/file/mimetypes.js')
	, deleteTempFiles = require(__dirname+'/../../lib/file/deletetempfiles.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, { Boards } = require(__dirname+'/../../db/');

module.exports = async (req, res) => {

	const { __, __n } = res.locals;
	const { checkRealMimeTypes } = config.get;
	const redirect = `/${req.params.board}/manage/assets.html`;

	for (let i = 0; i < res.locals.numFiles; i++) {
		if (!mimeTypes.allowed(req.files.file[i].mimetype, {
			image: true,
			animatedImage: true,
			video: false,
			audio: false,
			other: true
		})) {
			await deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'message': __('Invalid file type for %s. Mimetype %s not allowed.', req.files.file[i].name, req.files.file[i].mimetype),
				'redirect': redirect
			});
		}
		// check for any mismatching supposed mimetypes from the actual file mimetype
		if (checkRealMimeTypes) {
			if (!(await mimeTypes.realMimeCheck(req.files.file[i]))) {
				deleteTempFiles(req).catch(console.error);
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Bad request'),
					'message': __('Mime type mismatch for file "%s"', req.files.file[i].name),
					'redirect': redirect
				});
			}
		}
	}

	const filenames = [];
	for (let i = 0; i < res.locals.numFiles; i++) {
		const file = req.files.file[i];
		file.filename = file.sha256 + file.extension;

		//check if already exists
		const exists = await pathExists(`${uploadDirectory}/asset/${req.params.board}/${file.filename}`);

		if (exists) {
			await remove(file.tempFilePath);
			continue;
		}

		//add to list after checking it doesnt already exist
		filenames.push(file.filename);

		//then upload it
		await moveUpload(file, file.filename, `asset/${req.params.board}`);

		//and delete the temp file
		await remove(file.tempFilePath);

	}

	deleteTempFiles(req).catch(console.error);

	// no new assets
	if (filenames.length === 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'message': __n('Asset already exist', res.locals.numFiles),
			'redirect': redirect
		});
	}

	// add assets to the db
	await Boards.addAssets(req.params.board, filenames);

	// add assets to board in memory
	res.locals.board.assets = res.locals.board.assets.concat(filenames);

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __n('Uploaded %s new assets.', filenames.length),
		'redirect': redirect
	});

};


code:
'use strict';

const { CustomPages } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js')
	, { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js')
	, messageHandler = require(__dirname+'/../../lib/post/message.js');

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const message = prepareMarkdown(req.body.message, false);
	const { message: markdownMessage } = await messageHandler(message, null, null, res.locals.permissions);

	const post = {
		'board': req.params.board,
		'page': req.body.page,
		'title': req.body.title,
		'message': {
			'raw': message,
			'markdown': markdownMessage
		},
		'date': new Date(),
		'edited': null,
	};

	const insertedCustomPage = await CustomPages.insertOne(post);
	post._id = insertedCustomPage.insertedId;

	buildQueue.push({
		'task': 'buildCustomPage',
		'options': {
			'board': res.locals.board,
			'page': post.page,
			'customPage': post,
		}
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Added custom page'),
		'redirect': `/${req.params.board}/manage/custompages.html`
	});

};


code:
'use strict';

const { Filters } = require(__dirname+'/../../db/')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js');

module.exports = async (req, res) => {

	const { globalLimits } = config.get;

	const { __ } = res.locals;

	if (req.params.board) {
		const filterCount = await Filters.count(req.params.board);
		if (filterCount >= globalLimits.filters.max) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'message': __('Total number of filters would exceed global limit of %s', globalLimits.filters.max),
				'redirect': `/${req.params.board}/manage/filters.html`,
			});
		}
	}

	const filter = {
		'board': req.params.board ? req.params.board : null,
		'filters': req.body.filters.split(/\r?\n/).filter(n => n),
		'strictFiltering': req.body.strict_filtering ? true : false,
		'filterMode': req.body.filter_mode,
		'filterMessage': req.body.filter_message,
		'filterBanDuration': req.body.filter_ban_duration,
		'filterBanAppealable': req.body.filter_ban_appealable ? true : false,
	};

	await Filters.insertOne(filter);

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Added filter'),
		'redirect': req.params.board ? `/${req.params.board}/manage/filters.html` : '/globalmanage/filters.html'
	});

};


code:
'use strict';

const path = require('path')
	, { remove } = require('fs-extra')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, moveUpload = require(__dirname+'/../../lib/file/moveupload.js')
	, mimeTypes = require(__dirname+'/../../lib/file/mimetypes.js')
	, deleteTempFiles = require(__dirname+'/../../lib/file/deletetempfiles.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, { countryCodesSet } = require(__dirname+'/../../lib/misc/countries.js')
	, { Boards } = require(__dirname+'/../../db/')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js');

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const { checkRealMimeTypes } = config.get;
	const redirect = `/${req.params.board}/manage/assets.html`;

	// check all mime types before we try saving anything
	for (let i = 0; i < res.locals.numFiles; i++) {
		if (!mimeTypes.allowed(req.files.file[i].mimetype, {
			image: true,
			animatedImage: true, //gif flags? i guess lol
			video: false,
			audio: false,
			other: false
		})) {
			await deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'message': __('Invalid file type for %s. Mimetype %s not allowed.', req.files.file[i].name, req.files.file[i].mimetype),
				'redirect': redirect
			});
		}
	}

	// check for any mismatching supposed mimetypes from the actual file mimetype
	if (checkRealMimeTypes) {
		for (let i = 0; i < res.locals.numFiles; i++) {
			if (!(await mimeTypes.realMimeCheck(req.files.file[i]))) {
				deleteTempFiles(req).catch(console.error);
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Bad request'),
					'message': __('Mime type mismatch for file "%s"', req.files.file[i].name),
					'redirect': redirect
				});
			}
		}
	}

	const newFlags = {};
	for (let i = 0; i < res.locals.numFiles; i++) {
		const file = req.files.file[i];
		let noExt = path.parse(file.name).name;
		file.filename = noExt + file.sha256 + file.extension;

		//match case for real country flags
		if (noExt.length === 2 && countryCodesSet.has(noExt.toUpperCase())) {
			noExt = noExt.toUpperCase();
		}

		//add to list after checking it doesnt already exist
		newFlags[noExt] = file.filename;

		//then upload it
		await moveUpload(file, file.filename, `flag/${req.params.board}`);

		//and delete the temp file
		await remove(file.tempFilePath);

	}

	deleteTempFiles(req).catch(console.error);

	const updatedFlags = { ...res.locals.board.flags, ...newFlags };

	// add flags in db
	await Boards.setFlags(req.params.board, updatedFlags);

	await remove(`${uploadDirectory}/html/${req.params.board}/thread/`);
	buildQueue.push({
		'task': 'buildBoardMultiple',
		'options': {
			'board': res.locals.board,
			'startpage': 1,
			'endpage': Math.ceil(res.locals.board.settings.threadLimit/10),
		}
	});
	buildQueue.push({
		'task': 'buildCatalog',
		'options': {
			'board': res.locals.board,
		}
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Uploaded %s new flags.', res.locals.numFiles),
		'redirect': redirect
	});

};


code:
'use strict';

const { News } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js')
	, { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js')
	, messageHandler = require(__dirname+'/../../lib/post/message.js');

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const message = prepareMarkdown(req.body.message, false);
	const { message: markdownNews } = await messageHandler(message, null, null, res.locals.permissions);

	const post = {
		'title': req.body.title,
		'message': {
			'raw': message,
			'markdown': markdownNews
		},
		'date': new Date(),
		'edited': null,
	};

	await News.insertOne(post);

	buildQueue.push({
		'task': 'buildNews',
		'options': {}
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Added newspost'),
		'redirect': '/globalmanage/news.html'
	});

};


code:
'use strict';

const { Boards, Accounts } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, roleManager = require(__dirname+'/../../lib/permission/rolemanager.js');

module.exports = async (req, res) => {

	const { __ } = res.locals;

	await Promise.all([
		Accounts.addStaffBoard([req.body.username], res.locals.board._id),
		Boards.addStaff(res.locals.board._id, req.body.username, roleManager.roles.BOARD_STAFF)
	]);

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Added staff'),
		'redirect': `/${req.params.board}/manage/staff.html`,
	});

};


code:
'use strict';

const { Bans } = require(__dirname+'/../../db/');

module.exports = async (req, res) => {

	return Bans.appeal(res.locals.ip.cloak, req.body.checkedbans, req.body.message).then(r => r.modifiedCount);

};


code:
'use strict';

const { Bans } = require(__dirname+'/../../db/')
	, Socketio = require(__dirname+'/../../lib/misc/socketio.js')
	, config = require(__dirname+'/../../lib/misc/config.js');

module.exports = async (req, res) => {

	const { __, __n } = res.locals;
	const { defaultBanDuration } = config.get;
	const banDate = new Date();
	const banExpiry = new Date(banDate.getTime() + (req.body.ban_duration || defaultBanDuration)); //uses config default if missing or malformed
	const banReason = req.body.ban_reason || req.body.log_message || __('No reason specified');
	const allowAppeal = (req.body.no_appeal || req.body.ban_q || req.body.ban_h) ? false : true; //dont allow appeals for range bans

	const bans = [];

	if (req.body.ban || req.body.global_ban) {
		const banBoard = req.body.global_ban ? [null, req.params.board] : req.params.board;
		const ipPosts = res.locals.posts.reduce((acc, post) => {
			if (!acc[post.ip.cloak]) {
				acc[post.ip.cloak] = [];
			}
			acc[post.ip.cloak].push(post);
			if (req.body.ban_reason) {
				//send banmessage over websocket
				Socketio.emitRoom(`${post.board}-${post.thread || post.postId}`, 'markPost', {
					postId: post.postId,
					type: 'banmessage',
					banmessage: req.body.ban_reason,
				});
			}
			return acc;
		}, {});
		for (let ip in ipPosts) {
			//should we at some point filter these to not bother banning pruned ips?
			const thisIpPosts = ipPosts[ip];
			let banRange = 0;
			let banIp = {
				cloak: thisIpPosts[0].ip.cloak,
				raw: thisIpPosts[0].ip.raw,
				type: thisIpPosts[0].ip.type,
			};
			if (req.body.ban_h) {
				banRange = 2;
				banIp.cloak = thisIpPosts[0].ip.cloak
					.split('.')
					.slice(0,1)
					.join('.');
			} else if (req.body.ban_q) {
				banRange = 1;
				banIp.cloak = thisIpPosts[0].ip.cloak
					.split('.')
					.slice(0,2)
					.join('.');
			}
			bans.push({
				'global': req.body.global_ban != null,
				'range': banRange,
				'ip': banIp,
				'reason': banReason,
				'board': banBoard,
				'posts': req.body.preserve_post ? thisIpPosts : null,
				'issuer': req.session.user,
				'date': banDate,
				'expireAt': banExpiry,
				allowAppeal,
				'appeal': null,
				'showUser': !req.body.hide_name,
				'note': null,
				'seen': false,
			});
		}
	}

	if (req.body.report_ban || req.body.global_report_ban) {
		const banBoard = req.body.global_report_ban ? [null, req.params.board] : req.params.board;
		res.locals.posts.forEach(post => {
			let ips = [];
			if (req.body.report_ban) {
				const matches = post.reports.map(r => {
					if (req.body.checkedreports.includes(r.id.toString())) {
						return r.ip;
					}
				});
				ips = ips.concat(matches);
			}
			if (req.body.global_report_ban) {
				const matches = post.globalreports.map(r => {
					if (req.body.checkedreports.includes(r.id.toString())) {
						return r.ip;
					}
				});
				ips = ips.concat(matches);
			}
			ips = ips.filter(n => n);
			[...new Set(ips)].forEach(ip => {
				bans.push({
					'global': req.body.global_report_ban != null,
					'range': 0,
					'ip': ip,
					'reason': banReason,
					'board': banBoard,
					'posts': null,
					'issuer': req.session.user,
					'date': banDate,
					'expireAt': banExpiry,
					allowAppeal,
					'appeal': null,
					'showUser': !req.body.hide_name,
					'note': null,
					'seen': false
				});
			});
		});
	}

	const numBans = await Bans.insertMany(bans).then(result => result.insertedCount);

	const query = {
		message: __n('Added %s bans', numBans),
	};

	if ((req.body.ban || req.body.global_ban ) && req.body.ban_reason) {
		res.locals.actions.numBuild++;//there was a ban reason, so we may need to rebuild some pages, since banning is usually not a building action
		query['action'] = '$set';
		query['query'] = {
			'banmessage': req.body.ban_reason
		};
	}

	return query;

};


code:
'use strict';

const { Bypass } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, production = process.env.NODE_ENV === 'production';

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const { secureCookies, blockBypass } = config.get;
	const existingBypassId = req.signedCookies.bypassid || res.locals.pseudoIp;
	const bypass = await Bypass.getBypass(res.locals.anonymizer, existingBypassId, blockBypass.expireAfterUses);
	const bypassId = bypass.upsertedId || bypass.insertedId || existingBypassId;
	res.locals.blockBypass = true;

	res.cookie('bypassid', bypassId.toString(), {
		'maxAge': blockBypass.expireAfterTime,
		'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
		'sameSite': 'strict',
		'signed': true,
	});

	return dynamicResponse(req, res, 200, 'message', {
		'minimal': req.body.minimal,
		'title': __('Success'),
		'message': __('Completed block bypass, you may go back and make your post.'),
	});

};


code:
'use strict';

const { NumberInt } = require(__dirname+'/../../db/db.js');

module.exports = (locals) => {

	const { posts, __, __n } = locals;

	const filteredposts = posts.filter(post => {
		return !post.thread;
	});

	if (filteredposts.length === 0) {
		return {
			message: __('No threads selected to Bumplock'),
		};
	}

	return {
		message: __n('Toggled Bumplock for %s threads', filteredposts.length),
		action: '$bit',
		query: {
			'bumplocked': {
				'xor': NumberInt(1)
			},
		}
	};

};


code:
'use strict';

const { Boards, Posts } = require(__dirname+'/../../db/')
	, { debugLogs } = require(__dirname+'/../../configs/secrets.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js')
	, { remove } = require('fs-extra')
	, deletePosts = require(__dirname+'/deletepost.js')
	, { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js')
	, messageHandler = require(__dirname+'/../../lib/post/message.js')
	, { countryCodesSet } = require(__dirname+'/../../lib/misc/countries.js')
	, { trimSetting, numberSetting, booleanSetting, arraySetting } = require(__dirname+'/../../lib/input/setting.js')
	, { compareSettings } = require(__dirname+'/../../lib/input/settingsdiff.js')
	, settingChangeEntries = Object.entries({
		'userPostDelete': ['board', 'catalog', 'threads'],
		'userPostSpoiler': ['board', 'catalog', 'threads'],
		'userPostUnlink': ['board', 'catalog', 'threads'],
		'replyLimit': ['board', 'threads'],
		'archiveLinks': ['board', 'threads'],
		'reverseImageSearchLinks': ['board', 'threads'],
		'name': ['board', 'threads', 'catalog', 'other'],
		'description': ['board', 'threads', 'catalog', 'other'],
		'theme': ['board', 'threads', 'catalog', 'other'],
		'codetheme': ['board', 'threads', 'catalog', 'other'],
		'announcement.raw': ['board', 'threads', 'catalog', 'other'],
		'customCss': ['board', 'threads', 'catalog', 'other'],
		'language': ['board', 'threads', 'catalog', 'other'],
		'allowedFileTypes.other': ['board', 'threads', 'catalog'],
		'allowedFileTypes.image': ['board', 'threads', 'catalog'],
		'enableTegaki': ['board', 'threads', 'catalog'],
		'hideBanners': ['board', 'threads', 'catalog'],
		'enableWeb3': ['board', 'threads', 'catalog'],
	});

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const { globalLimits } = config.get;

	//oldsettings before changes
	const oldSettings = res.locals.board.settings;

	//array of promises we might need
	const promises = [];

	const announcement = req.body.announcement === null ? null : prepareMarkdown(req.body.announcement, false);
	let markdownAnnouncement = oldSettings.announcement.markdown;
	if (announcement !== oldSettings.announcement.raw) {
		({ message: markdownAnnouncement } = await messageHandler(announcement, req.params.board, null, res.locals.permissions));
	}

	if (req.body.countries) {
		req.body.countries = [...new Set(req.body.countries)] //prevents submitting multiple of same code, not like it matters, but meh
			.filter(code => countryCodesSet.has(code))
			.slice(0, countryCodesSet.size);
	}

	const newSettings = {
		'name': trimSetting(req.body.name, oldSettings.name),
		'description': trimSetting(req.body.description, oldSettings.description),
		'defaultName': trimSetting(req.body.default_name, oldSettings.defaultName),
		'language': trimSetting(req.body.language, oldSettings.language),
		'theme': req.body.theme || oldSettings.theme,
		'codeTheme': req.body.code_theme || oldSettings.codeTheme,
		'sfw': booleanSetting(req.body.sfw),
		'archiveLinks': booleanSetting(req.body.archive_links),
		'reverseImageSearchLinks': booleanSetting(req.body.reverse_image_search_links),
		'unlistedLocal': booleanSetting(req.body.unlisted_local),
		'unlistedWebring': booleanSetting(req.body.unlisted_webring),
		'early404': booleanSetting(req.body.early404),
		'ids': booleanSetting(req.body.ids),
		'geoFlags': booleanSetting(req.body.geo_flags),
		'customFlags': booleanSetting(req.body.custom_flags),
		'enableTegaki': booleanSetting(req.body.enable_tegaki),
		'enableWeb3': booleanSetting(req.body.enable_web3),
		'forceAnon': booleanSetting(req.body.force_anon),
		'sageOnlyEmail': booleanSetting(req.body.sage_only_email),
		'userPostDelete': booleanSetting(req.body.user_post_delete),
		'userPostSpoiler': booleanSetting(req.body.user_post_spoiler),
		'userPostUnlink': booleanSetting(req.body.user_post_unlink),
		'forceThreadMessage': booleanSetting(req.body.force_thread_message),
		'forceThreadFile': booleanSetting(req.body.force_thread_file),
		'forceReplyMessage': booleanSetting(req.body.force_reply_message),
		'forceReplyFile': booleanSetting(req.body.force_reply_file),
		'forceThreadSubject': booleanSetting(req.body.force_thread_subject),
		'disableReplySubject': booleanSetting(req.body.disable_reply_subject),
		'hideBanners': booleanSetting(req.body.hide_banners),
		'captchaMode': numberSetting(req.body.captcha_mode, oldSettings.captchaMode),
		'tphTrigger': numberSetting(req.body.tph_trigger, oldSettings.tphTrigger),
		'tphTriggerAction': numberSetting(req.body.tph_trigger_action, oldSettings.tphTriggerAction),
		'pphTrigger': numberSetting(req.body.pph_trigger, oldSettings.pphTrigger),
		'pphTriggerAction': numberSetting(req.body.pph_trigger_action, oldSettings.pphTriggerAction),
		'captchaReset': numberSetting(req.body.captcha_reset, oldSettings.captchaReset),
		'lockReset': numberSetting(req.body.lock_reset, oldSettings.lockReset),
		'threadLimit': numberSetting(req.body.thread_limit, oldSettings.threadLimit),
		'replyLimit': numberSetting(req.body.reply_limit, oldSettings.replyLimit),
		'bumpLimit': numberSetting(req.body.bump_limit, oldSettings.bumpLimit),
		'maxFiles': numberSetting(req.body.max_files, oldSettings.maxFiles),
		'minThreadMessageLength': numberSetting(req.body.min_thread_message_length, oldSettings.minThreadMessageLength),
		'minReplyMessageLength': numberSetting(req.body.min_reply_message_length, oldSettings.minReplyMessageLength),
		'maxThreadMessageLength': numberSetting(req.body.max_thread_message_length, oldSettings.maxThreadMessageLength),
		'maxReplyMessageLength': numberSetting(req.body.max_reply_message_length, oldSettings.maxReplyMessageLength),
		'lockMode': numberSetting(req.body.lock_mode, oldSettings.lockMode),
		'messageR9KMode': numberSetting(req.body.message_r9k_mode, oldSettings.messageR9KMode),
		'fileR9KMode': numberSetting(req.body.file_r9k_mode, oldSettings.fileR9KMode),
		'deleteProtectionAge': numberSetting(req.body.delete_protection_age, oldSettings.deleteProtectionAge),
		'deleteProtectionCount': numberSetting(req.body.delete_protection_count, oldSettings.deleteProtectionCount),
		'blockedCountries': req.body.countries || [],
		'disableAnonymizerFilePosting': booleanSetting(req.body.disable_anonymizer_file_posting),
		'customCss': globalLimits.customCss.enabled ? (req.body.custom_css !== null ? req.body.custom_css : oldSettings.customCss) : null,
		'announcement': {
			'raw': announcement !== null ? announcement : oldSettings.announcement.raw,
			'markdown': req.body.announcement !== null ? markdownAnnouncement : oldSettings.announcement.markdown,
		},
		'allowedFileTypes': {
			'animatedImage': booleanSetting(req.body.files_allow_animated_image),
			'image': booleanSetting(req.body.files_allow_image),
			'video': booleanSetting(req.body.files_allow_video),
			'audio': booleanSetting(req.body.files_allow_audio),
			'other': booleanSetting(req.body.files_allow_other),
		},
	};

	//settings changed in the db
	await Boards.updateOne(req.params.board, {
		'$set':  {
			'settings': newSettings,
			'tags': arraySetting(req.body.tags, res.locals.board.tags, 10),
		}
	});

	//update this in locals incase is used in later parts
	res.locals.board.settings = newSettings;

	//pages in new vs old settings
	const oldMaxPage = Math.ceil(oldSettings.threadLimit/10);
	const newMaxPage = Math.ceil(newSettings.threadLimit/10);

	const rebuildTasks = compareSettings(settingChangeEntries, oldSettings, newSettings, 4);

	if (rebuildTasks.size < 4) {
		//after that is stuff not direct equality comparisons calcluated by compareSettings()
		if (newSettings.captchaMode > oldSettings.captchaMode) {
			if (oldSettings.captchaMode === 0) {
				rebuildTasks.add('board')
					.add('catalog');
			}
			if (newSettings.captchaMode === 2) {
				rebuildTasks.add('threads');
			}
		} else if (newSettings.captchaMode < oldSettings.captchaMode) {
			if (oldSettings.captchaMode === 2) {
				rebuildTasks.add('threads');
			}
			if (newSettings.captchaMode === 0) {
				rebuildTasks.add('board')
					.add('catalog');
			}
		}
		//do rebuilding and pruning if max number of pages is changed and any threads are pruned
		if (newMaxPage < oldMaxPage) {
			//prune old threads
			const prunedThreads = await Posts.pruneThreads(res.locals.board);
			if (prunedThreads.length > 0) {
				await deletePosts(prunedThreads, req.params.board, res.locals);
				//remove board page html/json for pages > newMaxPage
				for (let i = newMaxPage+1; i <= oldMaxPage; i++) {
					promises.push(remove(`${uploadDirectory}/html/${req.params.board}/${i}.html`));
					promises.push(remove(`${uploadDirectory}/json/${req.params.board}/${i}.json`));
				}
				//rebuild all board pages for page nav numbers, and catalog
				rebuildTasks.add('board')
					.add('catalog');
			}
		}
	}

	if (rebuildTasks.has('threads')) {
		promises.push(remove(`${uploadDirectory}/html/${req.params.board}/thread/`));
	}
	if (rebuildTasks.has('board')) {
		buildQueue.push({
			'task': 'buildBoardMultiple',
			'options': {
				'board': res.locals.board,
				'startpage': 1,
				'endpage': newMaxPage
			}
		});
	}
	if (rebuildTasks.has('catalog')) {
		buildQueue.push({
			'task': 'buildCatalog',
			'options': {
				'board': res.locals.board,
			}
		});
	}
	if (rebuildTasks.has('other')) {
		promises.push(remove(`${uploadDirectory}/html/${req.params.board}/logs/`));
		promises.push(remove(`${uploadDirectory}/html/${req.params.board}/custompage/`));
		buildQueue.push({
			'task': 'buildModLogList',
			'options': {
				'board': res.locals.board,
			}
		});
		buildQueue.push({
			'task': 'buildBanners',
			'options': {
				'board': res.locals.board,
			}
		});
	}

	//updates board/settings.json
	buildQueue.push({
		'task': 'buildBoardSettings',
		'options': {
			'board': res.locals.board,
		}
	});

	//finish the promises in parallel e.g. removing files
	if (promises.length > 0) {
		await Promise.all(promises);
	}

	debugLogs && console.log(req.params.board, 'board settings changed');

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Updated settings.'),
		'redirect': `/${req.params.board}/manage/settings.html`
	});

};


code:
'use strict';

const { Boards } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js')
	, redis = require(__dirname+'/../../lib/redis/redis.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, Mongo = require(__dirname+'/../../db/db.js')
	, { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js')
	, messageHandler = require(__dirname+'/../../lib/post/message.js')
	, { trimSetting, numberSetting, booleanSetting, arraySetting } = require(__dirname+'/../../lib/input/setting.js')
	, { includeChildren, compareSettings } = require(__dirname+'/../../lib/input/settingsdiff.js')
	, { remove } = require('fs-extra')
	, template = require(__dirname+'/../../configs/template.js.example')
	, settingChangeEntries = Object.entries({
		//doesnt seem like it would be much different transforming this to be tasks: [settings] or this way, so this way it is
		'globalAnnouncement.raw': ['deletehtml', 'custompages'],
		'meta.siteName': ['deletehtml', 'scripts', 'custompages'],
		'meta.url': ['deletehtml', 'scripts', 'custompages'],
		'archiveLinksURL': ['deletehtml', 'custompages'],
		'ethereumLinksURL': ['deletehtml', 'custompages', 'scripts'],
		'reverseImageLinksURL': ['deletehtml', 'custompages'],
		'enableWebring': ['deletehtml', 'custompages'],
		'enableWeb3': ['deletehtml'],
		'thumbSize': ['deletehtml', 'css', 'scripts'],
		'previewReplies': ['deletehtml', 'custompages'],
		'stickyPreviewReplies': ['deletehtml', 'custompages'],
		'maxRecentNews': ['deletehtml', 'custompages'],
		'themes': ['scripts'],
		'codeThemes': ['scripts'],
		'globalLimits.postFiles.max': ['deletehtml', 'custompages'],
		'globalLimits.postFilesSize.max': ['deletehtml', 'custompages'],
		'language': ['deletehtml', 'css', 'scripts', 'custompages'],
		//these will make it easier to keep updated and include objects where any/all property change needs tasks
		//basically, it expands to all of globalLimits.fieldLength.* or frontendScriptDefault.*
		//it could be calculated in compareSettings with *, but im just precompiling it now. probably a tiny bit faster not doing it each time
		...includeChildren(template, 'captchaOptions', ['deletehtml', 'css', 'scripts', 'custompages']),
		...includeChildren(template, 'globalLimits.fieldLength', ['deletehtml', 'custompages']),
		...includeChildren(template, 'frontendScriptDefault', ['scripts']),
	});

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const promises = [];
	const oldSettings = config.get;

	const announcement = req.body.global_announcement === null ? null : prepareMarkdown(req.body.global_announcement, false);
	let markdownAnnouncement = oldSettings.globalAnnouncement.markdown;
	if (announcement !== oldSettings.globalAnnouncement.raw) {
		({ message: markdownAnnouncement } = await messageHandler(announcement, null, null, res.locals.permissions));
	}

	const newSettings = {
		allowedHosts: arraySetting(req.body.allowed_hosts, oldSettings.allowedHosts),
		countryCodeHeader: trimSetting(req.body.country_code_header, oldSettings.countryCodeHeader),
		ipHeader: trimSetting(req.body.ip_header, oldSettings.ipHeader),
		globalAnnouncement: {
			raw: trimSetting(announcement, oldSettings.globalAnnouncement.raw),
			markdown: trimSetting(markdownAnnouncement, oldSettings.globalAnnouncement.markdown),
		},
		meta: {
			siteName: trimSetting(req.body.meta_site_name, oldSettings.meta.siteName),
			url: trimSetting(req.body.meta_url, oldSettings.meta.url),
		},
		language: trimSetting(req.body.language, oldSettings.language),
		captchaOptions: {
			type: trimSetting(req.body.captcha_options_type, oldSettings.captchaOptions.type),
			generateLimit: numberSetting(req.body.captcha_options_generate_limit, oldSettings.captchaOptions.generateLimit),
			font: trimSetting(req.body.captcha_options_font, oldSettings.captchaOptions.font),
			grid: {
				size: numberSetting(req.body.captcha_options_grid_size, oldSettings.captchaOptions.grid.size),
				imageSize: numberSetting(req.body.captcha_options_grid_image_size, oldSettings.captchaOptions.grid.imageSize),
				iconYOffset: numberSetting(req.body.captcha_options_grid_icon_y_offset, oldSettings.captchaOptions.grid.iconYOffset),
				question: trimSetting(req.body.captcha_options_grid_question, oldSettings.captchaOptions.grid.question),
				trues: arraySetting(req.body.captcha_options_grid_trues, oldSettings.captchaOptions.grid.trues),
				falses: arraySetting(req.body.captcha_options_grid_falses, oldSettings.captchaOptions.grid.falses),
				edge: numberSetting(req.body.captcha_options_grid_edge, oldSettings.captchaOptions.grid.edge),
				noise: numberSetting(req.body.captcha_options_grid_noise, oldSettings.captchaOptions.grid.noise),
			},
			text: {
				line: booleanSetting(req.body.captcha_options_text_line, oldSettings.captchaOptions.text.line),
				wave: numberSetting(req.body.captcha_options_text_wave, oldSettings.captchaOptions.text.wave),
				paint: numberSetting(req.body.captcha_options_text_paint, oldSettings.captchaOptions.text.paint),
				noise: numberSetting(req.body.captcha_options_text_noise, oldSettings.captchaOptions.text.noise),
			},
			numDistorts: {
				min: numberSetting(req.body.captcha_options_num_distorts_min, oldSettings.captchaOptions.numDistorts.min),
				max: numberSetting(req.body.captcha_options_num_distorts_max, oldSettings.captchaOptions.numDistorts.max),
			},
			distortion: numberSetting(req.body.captcha_options_distortion, oldSettings.captchaOptions.distortion),
		},
		secureCookies: booleanSetting(req.body.secure_cookies),
		refererCheck: booleanSetting(req.body.referrer_check),
		dnsbl: {
			enabled: booleanSetting(req.body.dnsbl_enabled),
			blacklists: arraySetting(req.body.dnsbl_blacklists, oldSettings.dnsbl.blacklists),
			cacheTime: numberSetting(req.body.dnsbl_cache_time, oldSettings.dnsbl.cacheTime),
		},
		disableAnonymizerFilePosting: booleanSetting(req.body.disable_anonymizer_file_posting, oldSettings.disableAnonymizerFilePosting),
		statsCountAnonymizers: booleanSetting(req.body.stats_count_anonymizers, oldSettings.statsCountAnonymizers),
		floodTimers: {
			sameContentSameIp: numberSetting(req.body.flood_timers_same_content_same_ip, oldSettings.floodTimers.sameContentSameIp),
			sameContentAnyIp: numberSetting(req.body.flood_timers_same_content_any_ip, oldSettings.floodTimers.sameContentAnyIp),
			anyContentSameIp: numberSetting(req.body.flood_timers_any_content_same_ip, oldSettings.floodTimers.anyContentSameIp),
		},
		blockBypass: {
			enabled: booleanSetting(req.body.block_bypass_enabled, oldSettings.blockBypass.enabled),
			forceAnonymizers: booleanSetting(req.body.block_bypass_force_anonymizers, oldSettings.blockBypass.forceAnonymizers),
			expireAfterUses: numberSetting(req.body.block_bypass_expire_after_uses, oldSettings.blockBypass.expireAfterUses),
			expireAfterTime: numberSetting(req.body.block_bypass_expire_after_time, oldSettings.blockBypass.expireAfterTime),
			bypassDnsbl: booleanSetting(req.body.block_bypass_bypass_dnsbl, oldSettings.blockBypass.bypassDnsbl),
		},
		pruneImmediately: booleanSetting(req.body.prune_immediately, oldSettings.pruneImmediately),
		hashImages: booleanSetting(req.body.hash_images, oldSettings.hashImages),
		rateLimitCost: {
			captcha: numberSetting(req.body.rate_limit_cost_captcha, oldSettings.rateLimitCost.captcha),
			boardSettings: numberSetting(req.body.rate_limit_cost_board_settings, oldSettings.rateLimitCost.boardSettings),
			editPost: numberSetting(req.body.rate_limit_cost_edit_post, oldSettings.rateLimitCost.editPost),
		},
		inactiveAccountTime: numberSetting(req.body.inactive_account_time, oldSettings.inactiveAccountTime),
		inactiveAccountAction: numberSetting(req.body.inactive_account_action, oldSettings.inactiveAccountAction),
		abandonedBoardAction: numberSetting(req.body.abandoned_board_action, oldSettings.abandonedBoardAction),
		overboardLimit: numberSetting(req.body.overboard_limit, oldSettings.overboardLimit),
		overboardCatalogLimit: numberSetting(req.body.overboard_catalog_limit, oldSettings.overboardCatalogLimit),
		overboardReverseLinks: booleanSetting(req.body.overboard_reverse_links, oldSettings.overboardReverseLinks),
		hotThreadsLimit: numberSetting(req.body.hot_threads_limit, oldSettings.hotThreadsLimit),
		hotThreadsThreshold: numberSetting(req.body.hot_threads_threshold, oldSettings.hotThreadsThreshold),
		hotThreadsMaxAge: numberSetting(req.body.hot_threads_max_age, oldSettings.hotThreadsMaxAge),
		allowCustomOverboard: booleanSetting(req.body.allow_custom_overboard, oldSettings.allowCustomOverboard),
		archiveLinksURL: trimSetting(req.body.archive_links, oldSettings.archiveLinksURL),
		reverseImageLinksURL: trimSetting(req.body.reverse_links, oldSettings.reverseImageLinksURL),
		ethereumLinksURL: trimSetting(req.body.ethereum_links, oldSettings.ethereumLinksURL),
		cacheTemplates: booleanSetting(req.body.cache_templates, oldSettings.cacheTemplates),
		lockWait: numberSetting(req.body.lock_wait, oldSettings.lockWait),
		pruneModlogs: numberSetting(req.body.prune_modlogs, oldSettings.pruneModlogs),
		dontStoreRawIps: booleanSetting(req.body.dont_store_raw_ips, oldSettings.dontStoreRawIps),
		pruneIps: numberSetting(req.body.prune_ips, oldSettings.pruneIps),
		enableWebring: booleanSetting(req.body.enable_webring, oldSettings.enableWebring),
		enableWeb3: booleanSetting(req.body.enable_web3, oldSettings.enableWeb3),
		// ethereumNode: trimSetting(req.body.ethereum_node, oldSettings.ethereumNode),
		following: arraySetting(req.body.webring_following, oldSettings.following),
		blacklist: arraySetting(req.body.webring_blacklist, oldSettings.blacklist),
		logo: arraySetting(req.body.webring_logos, oldSettings.logo),
		proxy: {
			enabled: booleanSetting(req.body.proxy_enabled, oldSettings.proxy.enabled),
			address: trimSetting(req.body.proxy_address, oldSettings.proxy.address),
		},
		thumbExtension: trimSetting(req.body.thumb_extension, oldSettings.thumbExtension),
		highlightOptions: {
			languageSubset: arraySetting(req.body.highlight_options_language_subset, oldSettings.highlightOptions.languageSubset),
			threshold: numberSetting(req.body.highlight_options_threshold, oldSettings.highlightOptions.threshold),
		},
		themes: arraySetting(req.body.themes, oldSettings.themes),
		codeThemes: arraySetting(req.body.code_themes, oldSettings.codeThemes),
		frontendScriptDefault: {
			embedsEnabled: booleanSetting(req.body.frontend_script_default_embeds_enabled, oldSettings.frontendScriptDefault.embedsEnabled),
			heightUnlimit: booleanSetting(req.body.frontend_script_default_height_unlimit, oldSettings.frontendScriptDefault.heightUnlimit),
			hideRecursive: booleanSetting(req.body.frontend_script_default_hide_recursive, oldSettings.frontendScriptDefault.hideRecursive),
			crispImages: booleanSetting(req.body.frontend_script_default_crisp_images, oldSettings.frontendScriptDefault.crispImages),
			hideThumbnails: booleanSetting(req.body.frontend_script_default_hide_thumbnails, oldSettings.frontendScriptDefault.hideThumbnails),
			nonColorIds: booleanSetting(req.body.frontend_script_default_non_color_ids, oldSettings.frontendScriptDefault.nonColorIds),
			alwaysShowSpoilers: booleanSetting(req.body.frontend_script_default_always_show_spoilers, oldSettings.frontendScriptDefault.alwaysShowSpoilers),
			hidePostStubs: booleanSetting(req.body.frontend_script_default_hide_post_stubs, oldSettings.frontendScriptDefault.hidePostStubs),
			smoothScrolling: booleanSetting(req.body.frontend_script_default_smooth_scrolling, oldSettings.frontendScriptDefault.smoothScrolling),
			threadWatcher: booleanSetting(req.body.frontend_script_default_thread_watcher, oldSettings.frontendScriptDefault.threadWatcher),
			defaultVolume: numberSetting(req.body.frontend_script_default_volume, oldSettings.frontendScriptDefault.defaultVolume),
			loop: booleanSetting(req.body.frontend_script_default_loop, oldSettings.frontendScriptDefault.loop),
			imageLoadingBars: booleanSetting(req.body.frontend_script_default_image_loading_bars, oldSettings.frontendScriptDefault.imageLoadingBars),
			live: booleanSetting(req.body.frontend_script_default_live, oldSettings.frontendScriptDefault.live),
			scrollToPosts: booleanSetting(req.body.frontend_script_default_scroll_to_posts, oldSettings.frontendScriptDefault.scrollToPosts),
			localTime: booleanSetting(req.body.frontend_script_default_local_time, oldSettings.frontendScriptDefault.localTime),
			hour24Time: booleanSetting(req.body.frontend_script_default_hour_24_time, oldSettings.frontendScriptDefault.hour24Time),
			relativeTime: booleanSetting(req.body.frontend_script_default_relative_time, oldSettings.frontendScriptDefault.relativeTime),
			notificationsEnabled: booleanSetting(req.body.frontend_script_default_notifications_embed, oldSettings.frontendScriptDefault.notificationsEnabled),
			notificationsYousOnly: booleanSetting(req.body.frontend_script_default_notifications_yous_only, oldSettings.frontendScriptDefault.notificationsYousOnly),
			showYous: booleanSetting(req.body.frontend_script_default_show_yous, oldSettings.frontendScriptDefault.showYous),
			hideDeletedPostContent: booleanSetting(req.body.frontend_script_default_hide_deleted_post_content, oldSettings.frontendScriptDefault.hideDeletedPostContent),
			tegakiWidth: numberSetting(req.body.frontend_script_default_tegaki_width, oldSettings.frontendScriptDefault.tegakiWidth),
			tegakiHeight: numberSetting(req.body.frontend_script_default_tegaki_height, oldSettings.frontendScriptDefault.tegakiHeight),
		},
		animatedGifThumbnails: booleanSetting(req.body.animated_gif_thumbnails, oldSettings.animatedGifThumbnails),
		audioThumbnails: booleanSetting(req.body.audio_thumbnails, oldSettings.audioThumbnails),
		ffmpegGifThumbnails: booleanSetting(req.body.ffmpeg_gif_thumbnails, oldSettings.ffmpegGifThumbnails),
		thumbSize: numberSetting(req.body.thumb_size, oldSettings.thumbSize),
		videoThumbPercentage: numberSetting(req.body.video_thumb_percentage, oldSettings.videoThumbPercentage),
		otherMimeTypes: arraySetting(req.body.other_mime_types, oldSettings.otherMimeTypes),
		checkRealMimeTypes: booleanSetting(req.body.check_real_mime_types, oldSettings.checkRealMimeTypes),
		allowMimeNoMatch: booleanSetting(req.body.allow_mime_no_match, oldSettings.allowMimeNoMatch),
		defaultBanDuration: numberSetting(req.body.default_ban_duration, oldSettings.defaultBanDuration),
		quoteLimit: numberSetting(req.body.quote_limit, oldSettings.quoteLimit),
		previewReplies: numberSetting(req.body.preview_replies, oldSettings.previewReplies),
		stickyPreviewReplies: numberSetting(req.body.sticky_preview_replies, oldSettings.stickyPreviewReplies),
		early404Fraction: numberSetting(req.body.early_404_fraction, oldSettings.early404Fraction),
		early404Replies: numberSetting(req.body.early_404_replies, oldSettings.early404Replies),
		maxRecentNews: numberSetting(req.body.max_recent_news, oldSettings.maxRecentNews),
		filterFileNames: booleanSetting(req.body.filter_file_names, oldSettings.filterFileNames),
		spaceFileNameReplacement: req.body.space_file_name_replacement,
		uriDecodeFileNames: booleanSetting(req.body.uri_decode_file_names, oldSettings.uriDecodeFileNames),
		globalLimits:  {
			customCss: {
				enabled: booleanSetting(req.body.global_limits_custom_css_enabled, oldSettings.globalLimits.customCss.enabled),
				max: numberSetting(req.body.global_limits_custom_css_max, oldSettings.globalLimits.customCss.max),
				strict: booleanSetting(req.body.global_limits_custom_css_strict, oldSettings.globalLimits.customCss.strict),
				filters: arraySetting(req.body.global_limits_custom_css_filters, oldSettings.globalLimits.customCss.filters),
			},
			threadLimit: {
				min: numberSetting(req.body.global_limits_thread_limit_min, oldSettings.globalLimits.threadLimit.min),
				max: numberSetting(req.body.global_limits_thread_limit_max, oldSettings.globalLimits.threadLimit.max),
			},
			replyLimit: {
				min: numberSetting(req.body.global_limits_reply_limit_min, oldSettings.globalLimits.replyLimit.min),
				max: numberSetting(req.body.global_limits_reply_limit_max, oldSettings.globalLimits.replyLimit.max),
			},
			bumpLimit: {
				min: numberSetting(req.body.global_limits_bump_limit_min, oldSettings.globalLimits.bumpLimit.min),
				max: numberSetting(req.body.global_limits_bump_limit_max, oldSettings.globalLimits.bumpLimit.max),
			},
			postFiles: {
				max: numberSetting(req.body.global_limits_post_files_max, oldSettings.globalLimits.postFiles.max),
			},
			postFilesSize: {
				max: numberSetting(req.body.global_limits_post_files_size_max, oldSettings.globalLimits.postFilesSize.max),
				imageResolution: numberSetting(req.body.global_limits_post_files_size_image_resolution, oldSettings.globalLimits.postFilesSize.imageResolution),
				videoResolution: numberSetting(req.body.global_limits_post_files_size_video_resolution, oldSettings.globalLimits.postFilesSize.videoResolution),
			},
			bannerFiles: {
				width: numberSetting(req.body.global_limits_banner_files_width, oldSettings.globalLimits.bannerFiles.width),
				height: numberSetting(req.body.global_limits_banner_files_height, oldSettings.globalLimits.bannerFiles.height),
				max: numberSetting(req.body.global_limits_banner_files_max, oldSettings.globalLimits.bannerFiles.max),
				total: numberSetting(req.body.global_limits_banner_files_total, oldSettings.globalLimits.bannerFiles.total),
				forceAspectRatio: booleanSetting(req.body.global_limits_banner_files_force_aspect_ratio, oldSettings.globalLimits.bannerFiles.forceAspectRatio),
			},
			bannerFilesSize: {
				max: numberSetting(req.body.global_limits_banner_files_size_max, oldSettings.globalLimits.bannerFilesSize.max),
			},
			flagFiles: {
				max: numberSetting(req.body.global_limits_flag_files_max, oldSettings.globalLimits.flagFiles.max),
				total: numberSetting(req.body.global_limits_flag_files_total, oldSettings.globalLimits.flagFiles.total),
			},
			flagFilesSize: {
				max: numberSetting(req.body.global_limits_flag_files_size_max, oldSettings.globalLimits.flagFilesSize.max),
			},
			assetFiles: {
				max: numberSetting(req.body.global_limits_asset_files_max, oldSettings.globalLimits.assetFiles.max),
				total: numberSetting(req.body.global_limits_asset_files_total, oldSettings.globalLimits.assetFiles.total),
			},
			assetFilesSize: {
				max: numberSetting(req.body.global_limits_asset_files_size_max, oldSettings.globalLimits.assetFilesSize.max),
			},
			fieldLength: {
				name: numberSetting(req.body.global_limits_field_length_name, oldSettings.globalLimits.fieldLength.name),
				email: numberSetting(req.body.global_limits_field_length_email, oldSettings.globalLimits.fieldLength.email),
				subject: numberSetting(req.body.global_limits_field_length_subject, oldSettings.globalLimits.fieldLength.subject),
				postpassword: numberSetting(req.body.global_limits_field_length_postpassword, oldSettings.globalLimits.fieldLength.postpassword),
				message: numberSetting(req.body.global_limits_field_length_message, oldSettings.globalLimits.fieldLength.message),
				report_reason: numberSetting(req.body.global_limits_field_length_report_reason, oldSettings.globalLimits.fieldLength.report_reason),
				ban_reason: numberSetting(req.body.global_limits_field_length_ban_reason, oldSettings.globalLimits.fieldLength.ban_reason),
				log_message: numberSetting(req.body.global_limits_field_length_log_message, oldSettings.globalLimits.fieldLength.log_message),
				uri: numberSetting(req.body.global_limits_field_length_uri, oldSettings.globalLimits.fieldLength.uri),
				boardname: numberSetting(req.body.global_limits_field_length_boardname, oldSettings.globalLimits.fieldLength.boardname),
				description: numberSetting(req.body.global_limits_field_length_description, oldSettings.globalLimits.fieldLength.description),
			},
			multiInputs: {
				posts: {
					anon: numberSetting(req.body.global_limits_multi_input_posts_anon, oldSettings.globalLimits.multiInputs.posts.anon),
					staff: numberSetting(req.body.global_limits_multi_input_posts_staff, oldSettings.globalLimits.multiInputs.posts.staff),
				},
			},
			customPages: {
				max: numberSetting(req.body.global_limits_custom_pages_max, oldSettings.globalLimits.customPages.max),
				maxLength: numberSetting(req.body.global_limits_custom_pages_max_length, oldSettings.globalLimits.customPages.maxLength),
			},
			filters: {
				max: numberSetting(req.body.global_limits_filters_max, oldSettings.globalLimits.filters.max),
			},
		},
		boardDefaults: {
			language: trimSetting(req.body.board_defaults_language, oldSettings.boardDefaults.language),
			theme: trimSetting(req.body.board_defaults_theme, oldSettings.boardDefaults.theme),
			codeTheme: trimSetting(req.body.board_defaults_code_theme, oldSettings.boardDefaults.codeTheme),
			reverseImageSearchLinks: booleanSetting(req.body.board_defaults_reverse_image_search_links, oldSettings.boardDefaults.reverseImageSearchLinks),
			archiveLinks: booleanSetting(req.body.board_defaults_archive_links, oldSettings.boardDefaults.archiveLinks),
			sfw: booleanSetting(req.body.board_defaults_sfw, oldSettings.boardDefaults.sfw),
			lockMode: numberSetting(req.body.board_defaults_lock_mode, oldSettings.boardDefaults.lockMode),
			fileR9KMode: numberSetting(req.body.board_defaults_file_r9k_mode, oldSettings.boardDefaults.fileR9KMode),
			messageR9KMode: numberSetting(req.body.board_defaults_message_r9k_mode, oldSettings.boardDefaults.messageR9KMode),
			unlistedLocal: booleanSetting(req.body.board_defaults_unlisted_local, oldSettings.boardDefaults.unlistedLocal),
			unlistedWebring: booleanSetting(req.body.board_defaults_unlisted_webring, oldSettings.boardDefaults.unlistedWebring),
			captchaMode: numberSetting(req.body.board_defaults_captcha_mode, oldSettings.boardDefaults.captchaMode),
			tphTrigger: numberSetting(req.body.board_defaults_tph_trigger, oldSettings.boardDefaults.tphTrigger),
			pphTrigger: numberSetting(req.body.board_defaults_pph_trigger, oldSettings.boardDefaults.pphTrigger),
			tphTriggerAction: numberSetting(req.body.board_defaults_tph_trigger_action, oldSettings.boardDefaults.tphTriggerAction),
			pphTriggerAction: numberSetting(req.body.board_defaults_pph_trigger_action, oldSettings.boardDefaults.pphTriggerAction),
			captchaReset: numberSetting(req.body.board_defaults_captcha_reset, oldSettings.boardDefaults.captchaReset),
			lockReset: numberSetting(req.body.board_defaults_lock_reset, oldSettings.boardDefaults.lockReset),
			defaultName: trimSetting(req.body.board_defaults_default_name, oldSettings.boardDefaults.defaultName),
			forceAnon: booleanSetting(req.body.board_defaults_force_anon, oldSettings.boardDefaults.forceAnon),
			sageOnlyEmail: booleanSetting(req.body.board_defaults_sage_only_email, oldSettings.boardDefaults.sageOnlyEmail),
			early404: booleanSetting(req.body.board_defaults_early_404, oldSettings.boardDefaults.early404),
			ids: booleanSetting(req.body.board_defaults_ids, oldSettings.boardDefaults.ids),
			customFlags: booleanSetting(req.body.board_defaults_custom_flags, oldSettings.boardDefaults.customFlags),
			geoFlags: booleanSetting(req.body.board_defaults_geo_flags, oldSettings.boardDefaults.geoFlags),
			enableTegaki: booleanSetting(req.body.board_defaults_enable_tegaki, oldSettings.boardDefaults.enableTegaki),
			userPostDelete: booleanSetting(req.body.board_defaults_user_post_delete, oldSettings.boardDefaults.userPostDelete),
			userPostSpoiler: booleanSetting(req.body.board_defaults_user_post_spoiler, oldSettings.boardDefaults.userPostSpoiler),
			userPostUnlink: booleanSetting(req.body.board_defaults_user_post_unlink, oldSettings.boardDefaults.userPostUnlink),
			threadLimit: numberSetting(req.body.board_defaults_thread_limit, oldSettings.boardDefaults.threadLimit),
			replyLimit: numberSetting(req.body.board_defaults_reply_limit, oldSettings.boardDefaults.replyLimit),
			bumpLimit: numberSetting(req.body.board_defaults_bump_limit, oldSettings.boardDefaults.bumpLimit),
			maxFiles: numberSetting(req.body.board_defaults_max_files, oldSettings.boardDefaults.maxFiles),
			forceReplyMessage: booleanSetting(req.body.board_defaults_force_reply_message, oldSettings.boardDefaults.forceReplyMessage),
			forceReplyFile: booleanSetting(req.body.board_defaults_force_reply_file, oldSettings.boardDefaults.forceReplyFile),
			forceThreadMessage: booleanSetting(req.body.board_defaults_force_thread_message, oldSettings.boardDefaults.forceThreadMessage),
			forceThreadFile: booleanSetting(req.body.board_defaults_force_thread_file, oldSettings.boardDefaults.forceThreadFile),
			forceThreadSubject: booleanSetting(req.body.board_defaults_force_thread_subject, oldSettings.boardDefaults.forceThreadSubject),
			disableReplySubject: booleanSetting(req.body.board_defaults_disable_reply_subject, oldSettings.boardDefaults.disableReplySubject),
			hideBanners: booleanSetting(req.body.board_defaults_hide_banners, oldSettings.boardDefaults.hideBanners),
			minThreadMessageLength: numberSetting(req.body.board_defaults_min_thread_message_length, oldSettings.boardDefaults.minThreadMessageLength),
			minReplyMessageLength: numberSetting(req.body.board_defaults_min_reply_message_length, oldSettings.boardDefaults.minReplyMessageLength),
			maxThreadMessageLength: numberSetting(req.body.board_defaults_max_thread_message_length, oldSettings.boardDefaults.maxThreadMessageLength),
			maxReplyMessageLength: numberSetting(req.body.board_defaults_max_reply_message_length, oldSettings.boardDefaults.maxReplyMessageLength),
			disableAnonymizerFilePosting: booleanSetting(req.body.board_defaults_disable_anonymizer_file_posting, oldSettings.boardDefaults.disableAnonymizerFilePosting),
			deleteProtectionAge: numberSetting(req.body.board_defaults_delete_protection_age, oldSettings.boardDefaults.deleteProtectionAge),
			deleteProtectionCount: numberSetting(req.body.board_defaults_delete_protection_count, oldSettings.boardDefaults.deleteProtectionCount),
			customCSS: null,
			blockedCountries: [],
			announcement: {
				raw: null,
				markdown: null
			},
			allowedFileTypes: {
				animatedImage: booleanSetting(req.body.board_defaults_allowed_file_types_animated_image, oldSettings.boardDefaults.allowedFileTypes.animatedImage),
				image: booleanSetting(req.body.board_defaults_allowed_file_types_image, oldSettings.boardDefaults.allowedFileTypes.image),
				video: booleanSetting(req.body.board_defaults_allowed_file_types_video, oldSettings.boardDefaults.allowedFileTypes.video),
				audio: booleanSetting(req.body.board_defaults_allowed_file_types_audio, oldSettings.boardDefaults.allowedFileTypes.audio),
				other: booleanSetting(req.body.board_defaults_allowed_file_types_other, oldSettings.boardDefaults.allowedFileTypes.other)
			}
		},
	};

	await Mongo.setConfig(newSettings);

	//webring being disabled
	if (oldSettings.enableWebring === true && newSettings.enableWebring === false) {
		promises.push(Boards.db.deleteMany({ webring: true }));
		promises.push(remove(`${uploadDirectory}/json/webring.json`));
		redis.del('webringsites');
	}

	//finish the promises in parallel e.g. removing files
	if (promises.length > 0) {
		await Promise.all(promises);
	}

	//publish to redis so running processes get updated config
	redis.redisPublisher.publish('config', JSON.stringify(newSettings));

	//relevant tasks: deletehtml, css, scripts, custompages
	const gulpTasks = compareSettings(settingChangeEntries, oldSettings, newSettings, 4);

	if (gulpTasks.size > 0) {
		buildQueue.push({
			'task': 'gulp',
			'options': {
				'tasks': [...gulpTasks],
			}
		});
	}

	//updates /settings.json
	buildQueue.push({
		'task': 'buildGlobalSettings',
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Updated settings.'),
		'redirect': '/globalmanage/settings.html'
	});

};


code:
'use strict';

const bcrypt = require('bcrypt')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, redis = require(__dirname+'/../../lib/redis/redis.js')
	, doTwoFactor = require(__dirname+'/../../lib/misc/dotwofactor.js')
	, { Accounts } = require(__dirname+'/../../db/');

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const username = req.body.username.toLowerCase();
	const password = req.body.password;
	const newPassword = req.body.newpassword;

	//fetch an account
	const account = await Accounts.findOne(username);

	//if the account doesnt exist (or is web3 where password change would be impossible), reject
	if (!account || account.web3 == true) {
		return dynamicResponse(req, res, 403, 'message', {
			'title': __('Forbidden'),
			'message': __('Incorrect account credentials'),
			'redirect': '/changepassword.html'
		});
	}

	// bcrypt compare input to saved hash
	const passwordMatch = await bcrypt.compare(password, account.passwordHash);

	//if hashes matched
	if (passwordMatch === false) {
		return dynamicResponse(req, res, 403, 'message', {
			'title': __('Forbidden'),
			'message': __('Incorrect account credentials'),
			'redirect': '/changepassword.html'
		});
	}

	if (account.twofactor) {
		const delta = await doTwoFactor(username, account.twofactor, req.body.twofactor);
		if (delta === null) {
			return dynamicResponse(req, res, 403, 'message', {
				'title': __('Forbidden'),
				'message': __('Incorrect account credentials'),
				'redirect': '/changepassword.html'
			});
		}
	}

	//change the password
	await Promise.all([
		Accounts.changePassword(username, newPassword),
		redis.deletePattern(`sess:*:${username}`),
	]);

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Password updated successfully'),
		'redirect': '/login.html'
	});

};


code:
'use strict';

const { Boards, Accounts } = require(__dirname+'/../../db/')
	, { Binary } = require(__dirname+'/../../db/db.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, roleManager = require(__dirname+'/../../lib/permission/rolemanager.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, restrictedURIs = new Set(['captcha', 'forms', 'randombanner', 'all'])
	, { ensureDir } = require('fs-extra')
	, config = require(__dirname+'/../../lib/misc/config.js');

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const { boardDefaults } = config.get;

	const { name, description } = req.body
		, uri = req.body.uri.toLowerCase()
		, tags = (req.body.tags || '').split(/\r?\n/).filter(n => n)
		, owner = req.session.user;

	if (restrictedURIs.has(uri)) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad Request'),
			'message': __('URI "%s" is reserved', uri),
			'redirect': '/create.html'
		});
	}

	const board = await Boards.findOne(uri);

	// if board exists reject
	if (board != null) {
		return dynamicResponse(req, res, 409, 'message', {
			'title': __('Conflict'),
			'message': __('Board with this URI already exists'),
			'redirect': '/create.html'
		});
	}

	const newBoard = {
		'_id': uri,
		owner,
		tags,
		'banners': [],
		'sequence_value': 1,
		'pph': 0,
		'ppd': 0,
		'ips': 0,
		'lastPostTimestamp': null,
		'webring': false,
		'staff': {
			[owner]: {
				'permissions': Binary(roleManager.roles.BOARD_OWNER.array),
				'addedDate': new Date(),
			},
		},
		'flags': {},
		'assets': [],
		'settings': {
			name,
			description,
			...boardDefaults
		}
	};

	await Promise.all([
		Boards.insertOne(newBoard),
		Accounts.addOwnedBoard(owner, uri),
		ensureDir(`${uploadDirectory}/html/${uri}`),
		ensureDir(`${uploadDirectory}/json/${uri}`),
		ensureDir(`${uploadDirectory}/banner/${uri}`),
		ensureDir(`${uploadDirectory}/flag/${uri}`),
		ensureDir(`${uploadDirectory}/asset/${uri}`),
	]);

	return res.redirect(`/${req.body.uri}/index.html`);

};


code:
'use strict';

const { NumberInt } = require(__dirname+'/../../db/db.js');

module.exports = (locals) => {

	const { posts, __, __n } = locals;

	const filteredposts = posts.filter(post => {
		return !post.thread;
	});

	if (filteredposts.length === 0) {
		return {
			message: __('No threads selected to make Cyclical'),
		};
	}

	return {
		message: __n('Toggled Cyclical mode for %s threads', filteredposts.length),
		action: '$bit',
		query: {
			'cyclic': {
				'xor': NumberInt(1)
			},
		}
	};

};


code:
'use strict';

const { Accounts } = require(__dirname+'/../../db/')
	, redis = require(__dirname+'/../../lib/redis/redis.js');

module.exports = async (username) => {

	await Promise.all([
		Accounts.deleteOne(username),
		redis.deletePattern(`sess:*:${username}`),
	]);

};


code:
'use strict';

const { Accounts, Boards } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, deleteBoard = require(__dirname+'/../../models/forms/deleteboard.js')
	, cache = require(__dirname+'/../../lib/redis/redis.js');

module.exports = async (req, res) => {

	const { __, __n } = res.locals;
	const accountsWithBoards = await Accounts.getOwnedOrStaffBoards(req.body.checkedaccounts);
	const checkedDeleteOwnedBoards = new Set(req.body.delete_owned_boards||[]);
	// console.log('checkedDeleteOwnedBoards', checkedDeleteOwnedBoards);
	const deleteCacheBoards = new Set();
	const deleteBoards = new Set();
	if (accountsWithBoards.length > 0) {
		const bulkWrites = [];
		for (let i = 0; i < accountsWithBoards.length; i++) {
			const acc = accountsWithBoards[i];
			if (acc.staffBoards.length > 0) {
				//remove from staff of any boards they are mod on
				bulkWrites.push({
					'updateMany': {
						'filter': {
							'_id': {
								'$in': acc.staffBoards
							}
						},
						'update': {
							'$unset': {
								[`staff.${acc._id}`]: '',
							}
						}
					}
				});
				acc.staffBoards.forEach(sb => deleteCacheBoards.add(`board:${sb}`));
			}
			if (acc.ownedBoards.length > 0) {
				//remove as owner of any boards they own
				bulkWrites.push({
					'updateMany': {
						'filter': {
							'_id': {
								'$in': acc.ownedBoards
							}
						},
						'update': {
							'$set': {
								'owner': null,
							},
							'$unset': {
								[`staff.${acc._id}`]: '',
							},
						}
					}
				});
				acc.ownedBoards.forEach(ob => {
					deleteCacheBoards.add(`board:${ob}`);
					if (checkedDeleteOwnedBoards.has(acc._id)) {
						deleteBoards.add(ob);
					}
				});
			}
		}
		await Boards.db.bulkWrite(bulkWrites);
		//invalidate caches for any board they were owner/staff of
		cache.del([...deleteCacheBoards]);
	}

	const amount = await Accounts.deleteMany(req.body.checkedaccounts).then(res => res.deletedCount);
	if (deleteBoards.size > 0) {
		await Promise.all([...deleteBoards].map(async uri => {
			const _board = await Boards.findOne(uri);
			return deleteBoard(uri, _board);
		}));
	}

	//invalidate any of their active sessions
	await cache.del(req.body.checkedaccounts.map(username => `sess:*:${username}`));

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __n('Deleted %s accounts', amount),
		'redirect': '/globalmanage/accounts.html'
	});

};


code:
'use strict';

const { remove } = require('fs-extra')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, { Boards } = require(__dirname+'/../../db/');

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const redirect = `/${req.params.board}/manage/assets.html`;

	//delete file of all selected assets
	await Promise.all(req.body.checkedassets.map(async filename => {
		remove(`${uploadDirectory}/asset/${req.params.board}/${filename}`);
	}));

	//remove from db
	const amount = await Boards.removeAssets(req.params.board, req.body.checkedassets).then(result => result.modifiedCount);

	//update res locals assets in memory
	res.locals.board.assets = res.locals.board.assets.filter(asset => {
		return !req.body.checkedassets.includes(asset);
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Deleted %s assets', amount),
		'redirect': redirect
	});
};


code:
'use strict';

const { remove } = require('fs-extra')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, { Boards } = require(__dirname+'/../../db/')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js');

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const redirect = `/${req.params.board}/manage/assets.html`;

	//delete file of all selected banners
	await Promise.all(req.body.checkedbanners.map(filename => {
		remove(`${uploadDirectory}/banner/${req.params.board}/${filename}`);
	}));

	//remove from db
	await Boards.removeBanners(req.params.board, req.body.checkedbanners);

	//update res locals banners in memory
	const beforeBanners = res.locals.board.banners.length;
	res.locals.board.banners = res.locals.board.banners.filter(banner => {
		return !req.body.checkedbanners.includes(banner);
	});

	//rebuild public banners page
	buildQueue.push({
		'task': 'buildBanners',
		'options': {
			'board': res.locals.board,
		}
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Deleted %s banners', beforeBanners - res.locals.board.banners.length),
		'redirect': redirect
	});
};


code:
'use strict';

const { CustomPages, Accounts, Boards, Stats, Posts, Bans, Modlogs, Filters } = require(__dirname+'/../../db/')
	, deletePosts = require(__dirname+'/deletepost.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, i18n = require(__dirname+'/../../lib/locale/locale.js')
	, { remove } = require('fs-extra');

module.exports = async (uri, board) => {

	//i18n mumbo jumbo
	const deleteLocals = {};
	i18n.init(deleteLocals);
	const boardLanguage = board.settings.language;
	deleteLocals.setLocale(deleteLocals, boardLanguage);

	//delete board
	await Boards.deleteOne(uri);
	//get all posts (should probably project to get files for deletin and anything else necessary)
	const allPosts = await Posts.allBoardPosts(uri);
	if (allPosts.length > 0) {
		//delete posts and decrement images
		await deletePosts(allPosts, uri, deleteLocals, true);
	}
	await Promise.all([
		Accounts.removeOwnedBoard(board.owner, uri), //remove board from owner account
		Object.keys(board.staff).length > 0 ? Accounts.removeStaffBoard(Object.keys(board.staff), uri) : void 0, //remove staffboard from staff accounts
		Modlogs.deleteBoard(uri), //modlogs for the board
		Bans.deleteBoard(uri), //bans for the board
		Filters.deleteBoard(uri), //filters for the board
		Stats.deleteBoard(uri), //stats for the board
		CustomPages.deleteBoard(uri), //custom pages for the board
		remove(`${uploadDirectory}/html/${uri}/`), //html
		remove(`${uploadDirectory}/json/${uri}/`), //json
		remove(`${uploadDirectory}/banner/${uri}/`), //banners
		remove(`${uploadDirectory}/flag/${uri}/`), //flags
		remove(`${uploadDirectory}/asset/${uri}/`), //assets
	]);

};


code:
'use strict';

const uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, { remove } = require('fs-extra')
	, { CustomPages } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js');

module.exports = async (req, res) => {

	const { __, __n } = res.locals;
	const deletedCount = await CustomPages.deleteMany(req.body.checkedcustompages, req.params.board).then(res => res.deletedCount);

	if (deletedCount === 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad Request',
			'message': 'Invalid custom pages selected',
			'redirect': `/${req.params.board}/manage/custompages.html`
		});
	}

	await Promise.all(req.body.checkedcustompages.map(page => {
		return Promise.all([
			remove(`${uploadDirectory}/html/${req.params.board}/custompage/${page}.html`),
			remove(`${uploadDirectory}/json/${req.params.board}/custompage/${page}.json`),
		]);
	}));

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __n('Deleted %s custom pages', deletedCount),
		'redirect': `/${req.params.board}/manage/custompages.html`
	});

};


code:
'use strict';

const { Filters } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js');

module.exports = async (req, res) => {

	const { __, __n } = res.locals;

	const deletedFilters = await Filters.deleteMany(req.params.board, req.body.checkedfilters).then(result => result.deletedCount);

	if (deletedFilters === 0 || deletedFilters < req.body.checkedfilters.length) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'error': __n('Deleted %s filters', deletedFilters),
			'redirect': req.headers.referer || (req.params.board ? `/${req.params.board}/manage/filters.html` : '/globalmanage/filters.html')
		});
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __n('Deleted %s filters', deletedFilters),
		'redirect': req.params.board ? `/${req.params.board}/manage/filters.html` : '/globalmanage/filters.html'
	});

};


code:
'use strict';

const { remove } = require('fs-extra')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, { Boards } = require(__dirname+'/../../db/')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js');

module.exports = async (req, res) => {

	const { __ } = res.locals;
	const redirect = `/${req.params.board}/manage/assets.html`;

	const updatedFlags = res.locals.board.flags;

	//delete file of all selected flags
	await Promise.all(req.body.checkedflags.map(async flagName => {
		remove(`${uploadDirectory}/flag/${req.params.board}/${res.locals.board.flags[flagName]}`);
		delete res.locals.board.flags[flagName];
	}));

	//remove from db
	await Boards.setFlags(req.params.board, updatedFlags);

	await remove(`${uploadDirectory}/html/${req.params.board}/thread/`);
	buildQueue.push({
		'task': 'buildBoardMultiple',
		'options': {
			'board': res.locals.board,
			'startpage': 1,
			'endpage': Math.ceil(res.locals.board.settings.threadLimit/10),
		}
	});
	buildQueue.push({
		'task': 'buildCatalog',
		'options': {
			'board': res.locals.board,
		}
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Deleted flags'),
		'redirect': redirect
	});
};