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
	});
};

repo_name: Marzisoft/jschan

license: mit


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}/page/${page}.html`),
			remove(`${uploadDirectory}/json/${req.params.board}/page/${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
	});
};


code:
'use strict';

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

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

	const { __ } = res.locals;

	await News.deleteMany(req.body.checkednews);

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

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

};


code:
'use strict';

const uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, { remove } = require('fs-extra')
	, { Posts, Files } = require(__dirname+'/../../db/')
	, Socketio = require(__dirname+'/../../lib/misc/socketio.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, deleteQuotes = require(__dirname+'/../../lib/post/deletequotes.js')
	, { func: pruneFiles } = require(__dirname+'/../../schedules/tasks/prune.js');

module.exports = async (posts, board, locals, all=false) => {

	const { pruneImmediately } = config.get;

	//filter to threads
	const threads = posts.filter(x => x.thread == null);

	//emits not including the fetched posts from next block because those are based on threads being selected
	//and we dont need to send delete message for every reply in a thread when the OP gets deleted.
	const deleteEmits = posts.reduce((acc, post) => {
		acc.push({
			room: `${post.board}-${post.thread || post.postId}`,
			postId: post.postId,
		});
		return acc;
	}, []);

	//get posts from all threads
	let threadPosts = [];
	if (all === false && threads.length > 0) {
		if (board) {
			//if this is board-specific, we can use a single query
			const threadPostIds = threads.map(thread => thread.postId);
			threadPosts = await Posts.getMultipleThreadPosts(board, threadPostIds);
		} else {
			//otherwise we fetch posts from threads on different boards separarely
			//TODO: use big board:$or/postid:$in query so this can be tackled in a single db query
			await Promise.all(threads.map(async thread => {
				//for each thread, fetch all posts from the matching board and thread matching the threads postId
				const currentThreadPosts = await Posts.getThreadPosts(thread.board, thread.postId);
				threadPosts = threadPosts.concat(currentThreadPosts);
			}));
		}
	}

	//combine them all into one array, there may be duplicates but it shouldnt matter
	const allPosts = posts.concat(threadPosts);

	//get files for ref counting, backlinks for post re-markup, mongoids for deleting
	const { postFiles, postMongoIds } = allPosts.reduce((acc, post) => {
		if (post.files.length > 0) {
			acc.postFiles = acc.postFiles.concat(post.files);
		}
		if (post.backlinks.length > 0) {
			acc.postBacklinks = acc.postBacklinks.concat(post.backlinks);
		}
		acc.postMongoIds.push(post._id);
		return acc;
	}, { postFiles: [], postBacklinks: [], postMongoIds: [] });

	if (postFiles.length > 0) {
		const fileNames = postFiles.map(x => x.filename);//[...new Set(postFiles.map(x => x.filename))];
		await Files.decrement(fileNames);
		if (pruneImmediately) {
			await pruneFiles(fileNames);
		}
	}

	let bulkWrites = [];
	const backlinkRebuilds = new Set();
	if (all === false) { //no need to rebuild quotes when deleting all posts for a board
		const deleteThreadMap = {};
		for (let i = 0; i < threads.length; i++) {
			const thread = threads[i];
			//if exists, add to set, else make the set
			if (!deleteThreadMap[thread.board]) {
				deleteThreadMap[thread.board] = new Set();
			}
			deleteThreadMap[thread.board].add(thread.postId);
		}
		for (let j = 0; j < allPosts.length; j++) {
			const post = allPosts[j];
			backlinkRebuilds.delete(post._id); //make sure we dont try and remarkup this post since its getting deleted.
			if (post.thread != null && !deleteThreadMap[post.board] || !deleteThreadMap[post.board].has(post.thread)) {
				//get backlinks for posts to remarkup
				for (let i = 0; i < post.backlinks.length; i++) {
					backlinkRebuilds.add(post.backlinks[i]._id);
				}
				//remove dead backlinks to this post
				if (post.quotes.length > 0) {
					bulkWrites.push({
						'updateMany': {
							'filter': {
								'_id': {
									'$in': post.quotes.map(q => q._id)
								}
							},
							'update': {
								'$pull': {
									'backlinks': {
										'postId': post.postId
									}
								}
							}
						}
					});
				}
			}
		}
	}

	//deleting before remarkup so quotes are accurate
	const deletedPosts = await Posts.deleteMany(postMongoIds).then(result => result.deletedCount);
	//emit the deletes to thread sockets (not recent sockets [yet?])
	for (let i = 0; i < deleteEmits.length; i++) {
		Socketio.emitRoom(deleteEmits[i].room, 'markPost', { postId: deleteEmits[i].postId, type: 'delete' });
	}

	if (all === false) {
		//get posts that quoted deleted posts so we can remarkup them
		if (backlinkRebuilds.size > 0) {
			const remarkupPosts = await Posts.globalGetPosts([...backlinkRebuilds]);
			bulkWrites = bulkWrites.concat(deleteQuotes(allPosts, remarkupPosts));
		}
	}

	//bulkwrite it all
	if (bulkWrites.length > 0) {
		await Posts.db.bulkWrite(bulkWrites);
	}

	if (threads.length > 0) {
		//delete the html/json for threads
		await Promise.all(threads.map(thread => {
			remove(`${uploadDirectory}/html/${thread.board}/thread/${thread.postId}.html`);
			remove(`${uploadDirectory}/json/${thread.board}/thread/${thread.postId}.json`);
		}));
	}

	const { __n, __ } = locals;

	//hooray!
	return {
		action: deletedPosts > 0,
		//How in the fuck do I use __n with multiple plural modifiers?
		message: (threads.length > 0 && deletedPosts-threads.length > 0)
			? __(`Deleted %s thread${threads.length>1?'s':''} and %s post${(deletedPosts-threads.length)>1?'s':''}`, threads.length, deletedPosts-threads.length)
			: ((threads.length > 0)
				? __n('Deleted %s threads', threads.length)
				: __n('Deleted %s posts', deletedPosts))
	};

};


code:
'use strict';

const { Files } = require(__dirname+'/../../db/')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, { func: pruneFiles } = require(__dirname+'/../../schedules/tasks/prune.js')
	, deletePostFiles = require(__dirname+'/../../lib/file/deletepostfiles.js');

module.exports = async (locals, unlinkOnly) => {

	const { posts, __, __n } = locals;
	const { pruneImmediately } = config.get;

	//get filenames from all the posts
	let files = [];
	for (let i = 0; i < posts.length; i++) {
		const post = posts[i];
		if (post.files.length > 0) {
			files = files.concat(post.files.map(file => {
				return {
					filename: file.filename,
					hash: file.hash,
					thumbextension: file.thumbextension,
					hasThumb: file.hasThumb,
				};
			}));
		}
	}
	files = [...new Set(files)];

	if (files.length == 0) {
		return {
			message: __('No files found')
		};
	}

	if (files.length > 0) {
		const fileNames = files.map(x => x.filename);
		await Files.decrement(fileNames);
		if (pruneImmediately) {
			await pruneFiles(fileNames);
		}
	}

	if (unlinkOnly) {
		return {
			message: __n('Unlinked %s files', files.length),
			action:'$set',
			query: {
				'files': []
			}
		};
	} else {
		//delete all the files
		await deletePostFiles(files);
		return {
			message: __n('Deleted %s files from server', files.length),
			//NOTE: only deletes from selected posts. other posts with same image will 404
			action:'$set',
			query: {
				'files': []
			}
		};
	}

};


code:
'use strict';

const redis = require(__dirname+'/../../lib/redis/redis.js');

module.exports = async (sessionIds) => {

	await redis.del(sessionIds);

};


code:
'use strict';

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

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

	const { __ } = res.locals;
	//only a ROOT could do this, per the permission bypass in the controller
	const deletingBoardOwner = req.body.checkedstaff.some(s => s === res.locals.board.owner);

	await Promise.all([
		Accounts.removeStaffBoard(req.body.checkedstaff, res.locals.board._id),
		Boards.removeStaff(res.locals.board._id, req.body.checkedstaff),
		deletingBoardOwner ? Accounts.removeOwnedBoard(res.locals.board.owner, res.locals.board._id) : void 0,
		deletingBoardOwner ? Boards.setOwner(res.locals.board._id, null) : void 0,
	]);

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

};


code:
'use strict';

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

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

	return Bans.denyAppeal(res.locals.bansBoard, req.body.checkedbans).then(result => result.modifiedCount);

};


code:
'use strict';

module.exports = (locals) => {

	const { posts, __ } = locals;

	const filteredposts = posts.filter(post => {
		return post.globalreports.length > 0;
	});

	if (filteredposts.length === 0) {
		return {
			message: __('No global reports to dismiss'),
		};
	}

	return {
		message: __('Dismissed global reports'),
		action: '$set',
		query: {
			'globalreports': []
		}
	};

};


code:
'use strict';

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

	const { posts, __ } = res.locals;

	const filteredposts = posts.filter(post => {
		return (req.body.global_dismiss && post.globalreports.length > 0)
			|| (req.body.dismiss && post.reports.length > 0);
	});

	if (filteredposts.length === 0) {
		return {
			message: __('No reports to dismiss'),
		};
	}

	const ret = {
		message: __('Dismissed reports'),
		action: '$set',
		query: {}
	};
	ret.query[`${req.body.global_dismiss ? 'global' : ''}reports`] = [];

	return ret;

};


code:
'use strict';

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

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

	const { __ } = res.locals;
	let updatingPermissions;

	if (req.body.template) {
		updatingPermissions = new Permission(req.body.template);
	} else {
		updatingPermissions = new Permission(res.locals.editingAccount.permissions);
		updatingPermissions.handleBody(req.body, res.locals.permissions);
	}
	// updatingPermissions.applyInheritance();

	const updated = await Accounts.setAccountPermissions(req.body.username, updatingPermissions).then(r => r.matchedCount);

	if (updated === 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'errors': __('Account does not exist'),
			'redirect': req.headers.referer || '/globalmanage/accounts.html',
		});
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Edited account'),
		'redirect': `/globalmanage/editaccount/${req.body.username}.html`,
	});

};


code:
'use strict';

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

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

	//New ban expiry date is current date + ban_duration. Not based on the original ban issue date.
	const newExpireAt = new Date(Date.now() + req.body.ban_duration);
	return Bans.editDuration(res.locals.bansBoard, req.body.checkedbans, newExpireAt).then(result => result.modifiedCount);

};


code:
'use strict';

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

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

	//New ban note.
	return Bans.editNote(res.locals.bansBoard, req.body.checkedbans, req.body.ban_note).then(result => result.modifiedCount);

};


code:
'use strict';

const { CustomPages } = require(__dirname+'/../../db/')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, { remove } = require('fs-extra')
	, 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: markdownPage } = await messageHandler(message, null, null, res.locals.permissions);
	const editedDate = new Date();

	const oldPage = await CustomPages.findOneAndUpdate(req.body.page_id, req.params.board,
		req.body.page, req.body.title, message, markdownPage, editedDate).then(res => res.value);

	if (oldPage === null) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'error': __('Custom page does not exist'),
			'redirect': req.headers.referer || `/${req.params.board}/manage/custompages.html`
		});
	}

	await remove(`${uploadDirectory}/html/${req.params.board}/page/${oldPage.page}.html`);

	const newPage = {
		'_id': oldPage._id,
		'board': req.params.board,
		'page': req.body.page,
		'title': req.body.title,
		'message': {
			'raw': message,
			'markdown': markdownPage
		},
		'date': oldPage.date,
		'edited': editedDate,
	};

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

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Updated custom page'),
		'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 { __ } = res.locals;

	const updated = await Filters.updateOne(
		req.params.board,
		req.body.filter_id,
		req.body.filters.split(/\r?\n/).filter(n => n),
		req.body.strict_filtering ? true : false,
		req.body.filter_mode,
		req.body.filter_message,
		req.body.filter_ban_duration,
		req.body.filter_ban_appealable ? true : false,
		req.body.replace_text
	).then(r => r.matchedCount);

	if (updated === 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'error': __('Filter does not exist'),
			'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': __('Updated filter'),
		'redirect': req.params.board ? `/${req.params.board}/manage/filters.html` : '/globalmanage/filters.html'
	});

};


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 updated = await News.updateOne(req.body.news_id, req.body.title, message, markdownNews).then(r => r.matchedCount);

	if (updated === 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'errors': __('News post does not exist'),
			'redirect': req.headers.referer || '/globalmanage/news.html'
		});
	}

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

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

};


code:
'use strict';

const { Posts, Modlogs, Filters } = require(__dirname+'/../../db/')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, { createHash } = require('crypto')
	, Mongo = require(__dirname+'/../../db/db.js')
	, { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js')
	, messageHandler = require(__dirname+'/../../lib/post/message.js')
	, nameHandler = require(__dirname+'/../../lib/post/name.js')
	, getFilterStrings = require(__dirname+'/../../lib/post/getfilterstrings.js')
	, checkFilters = require(__dirname+'/../../lib/post/checkfilters.js')
	, filterActions = require(__dirname+'/../../lib/post/filteractions.js')
	, ModlogActions = require(__dirname+'/../../lib/input/modlogactions.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, Socketio = require(__dirname+'/../../lib/misc/socketio.js')
	, { buildThread } = require(__dirname+'/../../lib/build/tasks.js')
	, FIELDS_TO_REPLACE = ['email', 'subject', 'message'];

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

/*
todo: handle some more situations
- last activity date
- correct bump date when editing thread or last post in a thread
*/

	const { __ } = res.locals;
	const { globalLimits, previewReplies } = config.get;
	const { board, post } = res.locals;

	//filters
	if (!res.locals.permissions.get(Permissions.BYPASS_FILTERS)) {

		//only global filters are checked, because anybody who could edit bypasses board filters
		const globalFilters = await Filters.findForBoard(null);

		let hitFilters = false;
		let { combinedString, strictCombinedString } = getFilterStrings(req, res);

		hitFilters = checkFilters(globalFilters, combinedString, strictCombinedString);
		if (hitFilters) {
			//if block or ban matched, only it is returned
			if (hitFilters[0].f.filterMode === 1 || hitFilters[0].f.filterMode === 2) {
				return filterActions(req, res, true, hitFilters[0].h, hitFilters[0].f, null);
			} else {
				for (const o of hitFilters) {
					await filterActions(req, res, true, o.h, o.f, null);
				}

				for (const field of FIELDS_TO_REPLACE) {
					//check filters haven't pushed a field past its limit
					if (req.body[field] && (req.body[field].length > globalLimits.fieldLength[field])) {
						return dynamicResponse(req, res, 400, 'message', {
							'title': __('Bad request'),
							'message': __(`After applying filters, ${field} exceeds maximum length of %s`, globalLimits.fieldLength[field]),
							'redirect': null
						});
					}
				}
			}
		}
	}

	//message hash
	let messageHash = null;
	if (req.body.message && req.body.message.length > 0) {
		const noQuoteMessage = req.body.message.replace(/>>\d+/g, '').replace(/>>>\/\w+(\/\d*)?/gm, '').trim();
		messageHash = createHash('sha256').update(noQuoteMessage).digest('base64');
	}
	//new name, trip and cap
	const { name, tripcode, capcode } = await nameHandler(
		req.body.name,
		res.locals.permissions,
		board.settings,
		board.owner,
		board.staff,
		res.locals.user ? res.locals.user.username : null,
		res.locals.__
	);
	//new message and quotes
	const nomarkup = prepareMarkdown(req.body.message, false);
	const { message, quotes, crossquotes } = await messageHandler(nomarkup, req.body.board, post.thread, res.locals.permissions);

	//intersection/difference of quotes sets for linking and unlinking
	const oldQuoteIds = post.quotes.map(q => q._id.toString());
	const oldQuotesSet = new Set(oldQuoteIds);
	const newQuoteIds = quotes.map(q => q._id.toString());
	const newQuotesSet = new Set(newQuoteIds);

	const addedQuotesSet = new Set(newQuoteIds.filter(qid => !oldQuotesSet.has(qid)).map(Mongo.ObjectId));
	const removedQuotesSet = new Set(oldQuoteIds.filter(qid => !newQuotesSet.has(qid)).map(Mongo.ObjectId));

	//linking new added quotes
	if (addedQuotesSet.size > 0) {
		await Posts.db.updateMany({
			'_id': {
				'$in': [...addedQuotesSet]
			}
		}, {
			'$push': {
				'backlinks': { _id: post._id, postId: post.postId }
			}
		});
	}

	//unlinking removed quotes
	if (removedQuotesSet.size > 0) {
		await Posts.db.updateMany({
			'_id': {
				'$in': [...removedQuotesSet]
			}
		}, {
			'$pull': {
				'backlinks': {
					'postId': post.postId
				}
			}
		});
	}

	//update the post
	await Posts.db.updateOne({
		board: req.body.board,
		postId: post.postId
	}, {
		'$set': {
			edited: {
				username: req.body.hide_name ? null : req.session.user,
				date: new Date(),
			},
			nomarkup,
			message,
			'messagehash': messageHash || null,
			quotes,
			crossquotes,
			name,
			tripcode,
			capcode,
			email: req.body.email,
			subject: req.body.subject,
		}
	});

	//emit the edit over websocket so post gets updated live
	Socketio.emitRoom(`${board._id}-${post.thread || post.postId}`, 'markPost', {
		postId: post.postId,
		type: 'edit',
		name,
		message,
		tripcode,
		capcode,
		email: req.body.email,
		subject: req.body.subject,
		//existing post props
		_id: post._id,
		u: post.u,
		date: post.date,
		country: post.country,
		board: post.board,
		nomarkup: post.nomarkup,
		thread: post.thread,
		spoiler: post.spoiler,
		banmessage: post.banmessage,
		userId: post.userId,
		files: post.files,
		quotes: post.quotes,
		backlinks: post.backlinks,
		replyposts: post.replyposts,
		replyfiles: post.replyfiles,
		sticky: post.sticky,
		locked: post.locked,
		bumplocked: post.bumplocked,
		cyclic: post.cyclic,
		edited: {
			username: req.body.hide_name ? null : req.session.user,
			date: new Date(),
		}
	});

	//add the edit to the modlog
	await Modlogs.insertOne({
		board: board._id,
		showLinks: true,
		postLinks: [{
			postId: post.postId,
			thread: post.thread,
		}],
		actions: [ModlogActions.EDIT],
		public: true, //TODO: take an optional checkbox also controlled by a BO/global delegated perm
		date: new Date(),
		showUser: req.body.hide_name ? false : true,
		message: req.body.log_message || null,
		user: req.session.user,
		ip: {
			cloak: res.locals.ip.cloak,
			raw: res.locals.ip.raw,
		}
	});

	const buildOptions = {
		'threadId': post.thread || post.postId,
		'board': res.locals.board
	};

	//build thread immediately for redirect
	await buildThread(buildOptions);

	dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Post edited successfully'),
		'redirect': req.body.referer,
	});
	res.end();

	//rebuild the modlogs
	buildQueue.push({
		'task': 'buildModLog',
		'options': {
			'board': board,
		}
	});
	buildQueue.push({
		'task': 'buildModLogList',
		'options': {
			'board': board,
		}
	});

	//check if post is visible in preview posts
	let postInPreviewPosts = false;
	if (post.thread) {
		const threadPreviewPosts = await Posts.db.find({
			'thread': post.thread,
			'board': board._id
		},{
			'projection': {
				'postId': 1, //only get postId
			}
		}).sort({
			'postId': -1
		}).limit(previewReplies).toArray();
		postInPreviewPosts = threadPreviewPosts.some(p => p.postId <= post.postId);
	}

	if (post.thread === null || postInPreviewPosts) {
		const threadPage = await Posts.getThreadPage(board._id, post.thread || post.postId);
		//rebuild index page if its a thread or visible in preview posts
		buildQueue.push({
			'task': 'buildBoard',
			'options': {
				'board': res.locals.board,
				'page': threadPage
			}
		});
	}

	if (post.thread === null) {
		//rebuild catalog if its a thread to correct catalog tile
		buildQueue.push({
			'task': 'buildCatalog',
			'options': {
				'board': res.locals.board,
			}
		});
	}

};


code:
'use strict';

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

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

	const { __ } = res.locals;
	let rolePermissions = new Permission(res.locals.editingRole.permissions);
	rolePermissions.handleBody(req.body, res.locals.permissions);
	// rolePermissions.applyInheritance();

	const existingRoleName = roleManager.rolePermissionMap[rolePermissions.base64];
	if (existingRoleName) {
		return dynamicResponse(req, res, 409, 'message', {
			'title': __('Conflict'),
			'error': __('Another role already exists with the same permissions: "%s"', existingRoleName),
			'redirect': req.headers.referer || '/globalmanage/roles.html',
		});
	}

	const updated = await Roles.updateOne(req.body.roleid, rolePermissions).then(r => r.matchedCount);

	if (updated === 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'error': __('Role does not exist'),
			'redirect': req.headers.referer || '/globalmanage/roles.html',
		});
	}

	const oldPermissions = new Permission(res.locals.editingRole.permissions);
	await Accounts.setNewRolePermissions(oldPermissions, rolePermissions);

	redis.redisPublisher.publish('roles', null);

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Edited role'),
		'redirect': `/globalmanage/editrole/${req.body.roleid}.html`,
	});

};


code:
'use strict';

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

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

	const { __ } = res.locals;
	let updatingPermissions = new Permission(res.locals.board.staff[req.body.username].permissions);

	updatingPermissions = new Permission(res.locals.board.staff[req.body.username].permissions);
	updatingPermissions.handleBody(req.body, res.locals.permissions, true);
	// updatingPermissions.applyInheritance();

	const updated = await Boards.setStaffPermissions(req.params.board, req.body.username, updatingPermissions).then(r => r.matchedCount);

	if (updated === 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'errors': __('Staff does not exist'),
			'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
		});
	}

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

};


code:
'use strict';

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

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

	const { __, __n } = res.locals;

	let deletedCount = 0;
	switch (req.body.table) {
		case 'blockbypass':
			deletedCount = await Bypass.deleteAll().then(res => res.deletedCount || 0);
			break;
		case 'sessions':
			deletedCount = await redis.deletePattern('sess:*');
			break;
		default:
			throw 'invalid table'; //Should never get here
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __n('Deleted %s records.', deletedCount),
		'redirect': '/globalmanage/settings.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 Lock'),
		};
	}

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

};


code:
'use strict';

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

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

	const { __ } = res.locals;
	let username, password;
	if (res.locals.isWeb3) {
		username = req.body.address.toLowerCase();
		password = null;
	} else {
		username = req.body.username.toLowerCase();
		password = req.body.password;
	}

	let goto = req.body.goto;
	// we don't want to redirect the user to random sites
	if (goto == null || !/^\/[0-9a-zA-Z][0-9a-zA-Z._/-]*$/.test(goto)) {
		goto = '/account.html';
	}
	const failRedirect = `/login.html${goto ? '?goto='+encodeURIComponent(goto) : ''}`;

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

	//if the account doesnt exist, reject
	if (!account) {
		return dynamicResponse(req, res, 403, 'message', {
			'title': __('Forbidden'),
			'message': __('Incorrect login credentials'),
			'redirect': failRedirect
		});
	}

	if (!account.web3 || account.passwordHash) {
		// bcrypt compare input to saved hash
		const passwordMatch = await bcrypt.compare(password, account.passwordHash);
		//2fA (TOTP) validation
		const delta = await doTwoFactor(username, account.twofactor || '', req.body.twofactor || '');
		//if password was correct and 2fa valid (if enabled)
		if (passwordMatch === false
			|| (account.twofactor && delta === null)) {
			return dynamicResponse(req, res, 403, 'message', {
				'title': __('Forbidden'),
				'message': __('Incorrect login credentials'),
				'redirect': failRedirect
			});
		}
	}

	// add the account to the session and authenticate
	req.session.user = account._id;

	//successful login
	await Accounts.updateLastActiveDate(username);
	return res.redirect(goto);

};


code:
'use strict';

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

	//remove session
	req.session.destroy();
	return res.redirect('/');

};


code:
'use strict';

const { createHash, randomBytes } = require('crypto')
	, randomBytesAsync = require('util').promisify(randomBytes)
	, { remove, emptyDir, pathExists, stat: fsStat } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, Mongo = require(__dirname+'/../../db/db.js')
	, Socketio = require(__dirname+'/../../lib/misc/socketio.js')
	, { Stats, Posts, Boards, Files, Filters } = require(__dirname+'/../../db/')
	, cache = require(__dirname+'/../../lib/redis/redis.js')
	, nameHandler = require(__dirname+'/../../lib/post/name.js')
	, getFilterStrings = require(__dirname+'/../../lib/post/getfilterstrings.js')
	, checkFilters = require(__dirname+'/../../lib/post/checkfilters.js')
	, filterActions = require(__dirname+'/../../lib/post/filteractions.js')
	, { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js')
	, messageHandler = require(__dirname+'/../../lib/post/message.js')
	, moveUpload = require(__dirname+'/../../lib/file/moveupload.js')
	, mimeTypes = require(__dirname+'/../../lib/file/mimetypes.js')
	, imageThumbnail = require(__dirname+'/../../lib/file/image/imagethumbnail.js')
	, getDimensions = require(__dirname+'/../../lib/file/image/getdimensions.js')
	, videoThumbnail = require(__dirname+'/../../lib/file/video/videothumbnail.js')
	, audioThumbnail = require(__dirname+'/../../lib/file/audio/audiothumbnail.js')
	, ffprobe = require(__dirname+'/../../lib/file/ffprobe.js')
	, formatSize = require(__dirname+'/../../lib/converter/formatsize.js')
	, { getCountryName } = require(__dirname+'/../../lib/misc/countries.js')
	, deleteTempFiles = require(__dirname+'/../../lib/file/deletetempfiles.js')
	, fixGifs = require(__dirname+'/../../lib/file/image/fixgifs.js')
	, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, deletePosts = require(__dirname+'/deletepost.js')
	, spamCheck = require(__dirname+'/../../lib/middleware/misc/spamcheck.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, { postPasswordSecret } = require(__dirname+'/../../configs/secrets.js')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, { buildThread } = require(__dirname+'/../../lib/build/tasks.js')
	, FIELDS_TO_REPLACE = ['email', 'subject', 'message'];

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

	const { __ } = res.locals;
	const { checkRealMimeTypes, thumbSize, thumbExtension, videoThumbPercentage, audioThumbnails,
		dontStoreRawIps, globalLimits } = config.get;

	//spam/flood check
	const flood = await spamCheck(req, res);
	if (flood) {
		deleteTempFiles(req).catch(console.error);
		return dynamicResponse(req, res, 429, 'message', {
			'title': __('Flood detected'),
			'message': __('Please wait before making another post, or a post similar to another user'),
			'redirect': `/${req.params.board}${req.body.thread ? '/thread/' + req.body.thread + '.html' : ''}`
		});
	}

	// check if this is responding to an existing thread
	let redirect = `/${req.params.board}/`;
	let salt = null;
	let thread = null;
	const isStaffOrGlobal = res.locals.permissions.hasAny(Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_BOARD_GENERAL);
	const { blockedCountries, threadLimit, ids, userPostSpoiler,
		pphTrigger, tphTrigger, tphTriggerAction, pphTriggerAction,
		sageOnlyEmail, forceAnon, replyLimit, disableReplySubject,
		captchaMode, lockMode, allowedFileTypes, customFlags, geoFlags, fileR9KMode, messageR9KMode } = res.locals.board.settings;
	if (!isStaffOrGlobal
		// && !res.locals.permissions.get(Permissions.BYPASS_FILTERS) //TODO: new permission for "bypass blocks" or something
		&& res.locals.country
		&& blockedCountries.includes(res.locals.country.code)) {
		await deleteTempFiles(req).catch(console.error);
		return dynamicResponse(req, res, 403, 'message', {
			'title': __('Forbidden'),
			'message': __('Your country "%s" is not allowed to post on this board', getCountryName(res.locals.country.code, res.locals.locale)),
			'redirect': redirect
		});
	}
	if ((lockMode === 2 || (lockMode === 1 && !req.body.thread)) //if board lock, or thread lock and its a new thread
		&& !isStaffOrGlobal) { //and not staff
		await deleteTempFiles(req).catch(console.error);
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'message': __(lockMode === 1 ? 'Thread creation locked' : 'Board locked'),
			'redirect': redirect
		});
	}
	if (req.body.thread) {
		thread = await Posts.getPost(req.params.board, req.body.thread, true);
		if (!thread || thread.thread != null) {
			await deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'message': __('Thread does not exist'),
				'redirect': redirect
			});
		}
		salt = thread.salt;
		redirect += `thread/${req.body.thread}.html`;
		if (thread.locked && !isStaffOrGlobal) {
			await deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'message': __('Thread locked'),
				'redirect': redirect
			});
		}
		if (thread.replyposts >= replyLimit && !thread.cyclic) { //reply limit
			await deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'message': __('Thread reached reply limit'),
				'redirect': redirect
			});
		}
	}

	//filters
	if (!res.locals.permissions.get(Permissions.BYPASS_FILTERS)) {

		//deconstruct global filter settings to differnt names, else they would conflict with the respective board-level setting
		const [globalFilters, localFilters] = await Promise.all([
			Filters.findForBoard(null),
			Filters.findForBoard(res.locals.board._id)
		]);

		let hitFilters = false
			, globalFilter = false;
		let { combinedString, strictCombinedString } = getFilterStrings(req, res);

		//compare to global filters
		hitFilters = checkFilters(globalFilters, combinedString, strictCombinedString);
		if (hitFilters) {
			globalFilter = true;
		}
		//if none matched, check local filters
		if (!hitFilters) {
			hitFilters = checkFilters(localFilters, combinedString, strictCombinedString);
		}

		if (hitFilters) {
			//if block or ban matched, only it is returned
			if (hitFilters[0].f.filterMode === 1 || hitFilters[0].f.filterMode === 2) {
				await deleteTempFiles(req).catch(console.error);
				return filterActions(req, res, globalFilter, hitFilters[0].h, hitFilters[0].f, redirect);
			} else {
				for (const o of hitFilters) {
					await filterActions(req, res, globalFilter, o.h, o.f, redirect);
				}

				for (const field of FIELDS_TO_REPLACE) {
					//check filters haven't pushed a field past its limit
					if (req.body[field] && (req.body[field].length > globalLimits.fieldLength[field])) {
						await deleteTempFiles(req).catch(console.error);
						return dynamicResponse(req, res, 400, 'message', {
							'title': __('Bad request'),
							'message': __(`After applying filters, ${field} exceeds maximum length of %s`, globalLimits.fieldLength[field]),
							'redirect': redirect
						});
					}
				}
			}
		}
	}

	//for r9k messages. usually i wouldnt process these if its not enabled e.g. flags and IDs but in this case I think its necessary
	let messageHash = null;
	if (req.body.message && req.body.message.length > 0) {
		const noQuoteMessage = req.body.message.replace(/>>\d+/g, '').replace(/>>>\/\w+(\/\d*)?/gm, '').trim();
		messageHash = createHash('sha256').update(noQuoteMessage).digest('base64');
		if ((req.body.thread && messageR9KMode === 1) || messageR9KMode === 2) {
			const postWithExistingMessage = await Posts.checkExistingMessage(res.locals.board._id, (messageR9KMode === 2 ? null : req.body.thread), messageHash);
			if (postWithExistingMessage != null) {
				await deleteTempFiles(req).catch(console.error);
				return dynamicResponse(req, res, 409, 'message', {
					'title': __('Conflict'),
					'message': __(`Messages must be unique ${messageR9KMode === 1 ? 'in this thread' : 'on this board'}. Your message is not unique.`),
					'redirect': redirect
				});
			}
		}
	}

	let files = [];
	// if we got a file
	if (res.locals.numFiles > 0) {

		//unique files check
		if ((req.body.thread && fileR9KMode === 1) || fileR9KMode === 2) {
			const filesHashes = req.files.file.map(f => f.sha256);
			const postWithExistingFiles = await Posts.checkExistingFiles(res.locals.board._id, (fileR9KMode === 2 ? null : req.body.thread), filesHashes);
			if (postWithExistingFiles != null) {
				await deleteTempFiles(req).catch(console.error);
				const conflictingFiles = req.files.file
					.filter(f => postWithExistingFiles.files.some(fx => fx.hash === f.sha256))
					.map(f => f.name)
					.join(', ');
				const r9kFilesMessage = __(`Uploaded files must be unique ${fileR9KMode === 1 ? 'in this thread' : 'on this board'}.`)
					+ '\n'
					+ conflictingFiles.length > 1 //slightly gross but __mf() is more so
					? __('At least the following file is not unique: %s', conflictingFiles)
					: __('At least the following files are not unique: %s', conflictingFiles);
				return dynamicResponse(req, res, 409, 'message', {
					'title': 'Conflict',
					'message': r9kFilesMessage,
					'redirect': redirect,
				});
			}
		}

		//basic mime type check
		for (let i = 0; i < res.locals.numFiles; i++) {
			if (!mimeTypes.allowed(req.files.file[i].mimetype, allowedFileTypes)) {
				await deleteTempFiles(req).catch(console.error);
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Bad request'),
					'message': __('Mime type "%s" for "%s" not allowed', req.files.file[i].mimetype, req.files.file[i].name),
					'redirect': redirect
				});
			}
		}

		//validate mime type properly
		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': req.files.file[i].realMimetype
							? __('Mime type "%s" invalid for file "%s"', req.files.file[i].realMimetype, req.files.file[i].name)
							: __('Mime type invalid for file "%s"', req.files.file[i].name),
						'redirect': redirect
					});
				}
			}
		}

		//upload, create thumbnails, get metadata, etc.
		for (let i = 0; i < res.locals.numFiles; i++) {
			const file = req.files.file[i];
			file.filename = file.sha256 + file.extension;

			//get metadata
			let processedFile = {
				filename: file.filename,
				spoiler: (!isStaffOrGlobal || userPostSpoiler) && req.body.spoiler && req.body.spoiler.includes(file.sha256),
				hash: file.sha256,
				originalFilename: req.body.strip_filename && req.body.strip_filename.includes(file.sha256) ? file.filename : file.name,
				mimetype: file.mimetype,
				size: file.size,
				extension: file.extension,
			};

			//phash
			if (file.phash) {
				processedFile.phash = file.phash;
			}

			//type and subtype
			let [type, subtype] = processedFile.mimetype.split('/');
			//check if already exists
			const existsFull = await pathExists(`${uploadDirectory}/file/${processedFile.filename}`);
			processedFile.sizeString = formatSize(processedFile.size);
			const saveFull = async () => {
				await Files.increment(processedFile);
				req.files.file[i].inced = true;
				if (!existsFull) {
					await moveUpload(file, processedFile.filename, 'file');
				}
			};
			if (mimeTypes.getOther().has(processedFile.mimetype)) {
				//"other" mimes from config, overrides main type to avoid codec issues in browser or ffmpeg for unsupported filetypes
				processedFile.hasThumb = false;
				processedFile.attachment = true;
				await saveFull();
			} else {
				const existsThumb = await pathExists(`${uploadDirectory}/file/thumb/${processedFile.hash}${processedFile.thumbextension}`);
				try {
					switch (type) {
						case 'image': {
							processedFile.thumbextension = thumbExtension;
							const imageDimensions = await getDimensions(req.files.file[i].tempFilePath, null, true);
							if (Math.floor(imageDimensions.width*imageDimensions.height) > globalLimits.postFilesSize.imageResolution) {
								await deleteTempFiles(req).catch(console.error);
								return dynamicResponse(req, res, 400, 'message', {
									'title': 'Bad request',
									'message': `File "${req.files.file[i].name}" image resolution is too high. Width*Height must not exceed ${globalLimits.postFilesSize.imageResolution}.`,
									'redirect': redirect
								});
							}
							if (thumbExtension === '.jpg' && subtype === 'png') {
								//avoid transparency issues for jpg thumbnails on pngs (the most common case -- for anything else, use webp thumbExtension)
								processedFile.thumbextension = '.png';
							}
							processedFile.geometry = imageDimensions;
							processedFile.geometryString = `${imageDimensions.width}x${imageDimensions.height}`;
							const lteThumbSize = (processedFile.geometry.height <= thumbSize
								&& processedFile.geometry.width <= thumbSize);
							processedFile.hasThumb = !(mimeTypes.allowed(file.mimetype, {image: true})
								&& subtype !== 'png'
								&& lteThumbSize);
							await saveFull();
							if (!existsThumb) {
								await imageThumbnail(processedFile);
							}
							processedFile = fixGifs(processedFile);
							break;
						}
						case 'audio':
						case 'video': {
							//video metadata
							const audioVideoData = await ffprobe(req.files.file[i].tempFilePath, null, true);
							processedFile.duration = audioVideoData.format.duration;
							processedFile.durationString = timeUtils.durationString(audioVideoData.format.duration*1000);
							const videoStreams = audioVideoData.streams.filter(stream => stream.width != null); //filter to only video streams or something with a resolution
							if (videoStreams.length > 0) {
								processedFile.thumbextension = thumbExtension;
								processedFile.codec = videoStreams[0].codec_name;
								processedFile.geometry = {width: videoStreams[0].coded_width, height: videoStreams[0].coded_height};
								if (Math.floor(processedFile.geometry.width*processedFile.geometry.height) > globalLimits.postFilesSize.videoResolution) {
									await deleteTempFiles(req).catch(console.error);
									return dynamicResponse(req, res, 400, 'message', {
										'title': 'Bad request',
										'message': `File "${req.files.file[i].name}" video resolution is too high. Width*Height must not exceed ${globalLimits.postFilesSize.videoResolution}.`,
										'redirect': redirect
									});
								}
								processedFile.geometryString = `${processedFile.geometry.width}x${processedFile.geometry.height}`;
								processedFile.hasThumb = true;
								await saveFull();
								if (!existsThumb) {
									const numFrames = videoStreams[0].nb_frames;
									const timestamp = ((numFrames === 'N/A' && subtype !== 'webm') || numFrames <= 1) ? 0 : processedFile.duration * videoThumbPercentage / 100;
									try {
										await videoThumbnail(processedFile, processedFile.geometry, timestamp);
									} catch (err) {
										//No keyframe after timestamp probably. ignore, we'll retry
										console.warn(err); //printing log because this error can actually be useful and we dont wanna mask it
									}
									let videoThumbStat = null;
									try {
										videoThumbStat = await fsStat(`${uploadDirectory}/file/thumb/${processedFile.hash}${processedFile.thumbextension}`);
									} catch (err) { /*ENOENT probably, ignore*/ }
									if (!videoThumbStat || videoThumbStat.code === 'ENOENT' || videoThumbStat.size === 0) {
										//create thumb again at 0 timestamp and lets hope it exists this time
										await videoThumbnail(processedFile, processedFile.geometry, 0);
									}
								}
							} else {
								//audio file, or video with only audio streams
								type = 'audio';
								processedFile.mimetype = `audio/${subtype}`;
								processedFile.thumbextension = '.png';
								processedFile.hasThumb = audioThumbnails;
								processedFile.geometry = { thumbwidth: thumbSize, thumbheight: thumbSize };
								await saveFull();
								if (processedFile.hasThumb && !existsThumb) {
									await audioThumbnail(processedFile);
								}
							}
							break;
						}
						default:
							throw new Error(__('invalid file mime type: %s', processedFile.mimetype));
					}
				} catch (e) {
					console.error(e);
					await deleteTempFiles(req).catch(console.error);
					return dynamicResponse(req, res, 400, 'message', {
						'title': __('Bad request'),
						'message': __('The server failed to process "%s". Possible unsupported or corrupt file.', req.files.file[i].name),
						'redirect': redirect
					});
				}
			}

			if (processedFile.hasThumb === true && processedFile.geometry && processedFile.geometry.width != null) {
				if (processedFile.geometry.width < thumbSize && processedFile.geometry.height < thumbSize) {
					//dont scale up thumbnail for smaller images
					processedFile.geometry.thumbwidth = processedFile.geometry.width;
					processedFile.geometry.thumbheight = processedFile.geometry.height;
				} else {
					const ratio = Math.min(thumbSize/processedFile.geometry.width, thumbSize/processedFile.geometry.height);
					processedFile.geometry.thumbwidth = Math.floor(Math.min(processedFile.geometry.width*ratio, thumbSize));
					processedFile.geometry.thumbheight = Math.floor(Math.min(processedFile.geometry.height*ratio, thumbSize));
				}
			}

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

			files.push(processedFile);
		}
	}
	// because express middleware is autistic i need to do this
	deleteTempFiles(req).catch(console.error);

	let userId = null;
	if (!salt) {
		//thread salt for IDs
		salt = (await randomBytesAsync(128)).toString('base64');
	}
	if (ids === true) {
		const fullUserIdHash = createHash('sha256').update(salt + res.locals.ip.raw).digest('hex');
		userId = fullUserIdHash.substring(fullUserIdHash.length-6);
	}
	let country = null;
	if (geoFlags === true) {
		country = {
			code: res.locals.country.code,
			name: getCountryName(res.locals.country.code, res.locals.locale),
		};
	}
	if (customFlags === true) {
		if (req.body.customflag && res.locals.board.flags[req.body.customflag] != null) {
			//if customflags allowed, and its a valid selection
			country = {
				name: req.body.customflag,
				code: req.body.customflag,
				src: res.locals.board.flags[req.body.customflag],
				custom: true, //this will help
			};
		}
	}
	let password = null;
	if (req.body.postpassword) {
		password = createHash('sha256').update(postPasswordSecret + req.body.postpassword).digest('base64');
	}

	//spoiler files only if board settings allow
	const spoiler = (!isStaffOrGlobal || userPostSpoiler) && req.body.spoiler_all ? true : false;

	//forceanon and sageonlyemail only allow sage email
	let email = (isStaffOrGlobal || (!forceAnon && !sageOnlyEmail) || req.body.email === 'sage') ? req.body.email : null;
	//disablereplysubject
	let subject = (!isStaffOrGlobal && req.body.thread && disableReplySubject) ? null : req.body.subject;

	//get name, trip and cap
	const { name, tripcode, capcode } = await nameHandler(
		req.body.name,
		res.locals.permissions,
		res.locals.board.settings,
		res.locals.board.owner,
		res.locals.board.staff,
		res.locals.user ? res.locals.user.username : null,
		__ //i18n translation local
	);
	//get message, quotes and crossquote array
	const nomarkup = prepareMarkdown(req.body.message, true);
	const { message, quotes, crossquotes } = await messageHandler(nomarkup, req.params.board, req.body.thread, res.locals.permissions);

	//web3 sig
	let signature = null
		, address = null;
	if (res.locals.recoveredAddress) {
		signature = req.body.signature;
		address = res.locals.recoveredAddress;
	}

	//build post data for db. for some reason all the property names are lower case :^)
	const now = Date.now();
	const data = {
		'date': new Date(now),
		'u': now,
		name,
		country,
		'board': req.params.board,
		tripcode,
		capcode,
		subject,
		'message': message || null,
		'messagehash': messageHash || null,
		'nomarkup': nomarkup || null,
		'thread': req.body.thread || null,
		password,
		email,
		spoiler,
		signature,
		address,
		'banmessage': null,
		userId,
		'ip': res.locals.ip,
		files,
		'reports': [],
		'globalreports': [],
		quotes, //posts this post replies to
		crossquotes, //quotes to other threads in same board
		'backlinks': [], //posts replying to this post
	};

	if (!req.body.thread) {
		//if this is a thread, add thread specific properties
		Object.assign(data, {
			'replyposts': 0,
			'replyfiles': 0,
			//NOTE: sticky is a number, 0 = not sticky, higher numbers are a priority and will be sorted in descending order
			'sticky': Mongo.NumberInt(0),
			//NOTE: these are numbers because we XOR them for toggling in action handler
			'locked': Mongo.NumberInt(0),
			'bumplocked': Mongo.NumberInt(0),
			'cyclic': Mongo.NumberInt(0),
			'salt': salt
		});
	}

	let threadPage = null;
	if (data.thread) {
		threadPage = await Posts.getThreadPage(req.params.board, data.thread);
	}

	const { postId, postMongoId } = await Posts.insertOne(res.locals.board, data, thread, res.locals.anonymizer);

	let enableCaptcha = false; //make this returned from some function, refactor and move the next section to another file
	const tphTriggerActive = (tphTriggerAction > 0 && tphTrigger > 0);
	if (pphTriggerAction || tphTriggerActive) { //if a trigger is enabled
		const triggerUpdate = {
			'$set': {},
		};
		//and a setting needs to be updated
		const pphTriggerUpdate = (pphTriggerAction < 3 && captchaMode < pphTriggerAction)
			|| (pphTriggerAction === 3 && lockMode < 1)
			|| (pphTriggerAction === 4 && lockMode < 2);
		const tphTriggerUpdate = (tphTriggerAction < 3 && captchaMode < tphTriggerAction)
			|| (tphTriggerAction === 3 && lockMode < 1)
			|| (tphTriggerAction === 4 && lockMode < 2);
		if (tphTriggerUpdate || pphTriggerUpdate) {
			const hourPosts = await Stats.getHourPosts(res.locals.board._id);
			const calcTriggerMode = (update, trigger, triggerAction, stat) => { //todo: move this somewhere else
				if (trigger > 0 && stat >= trigger) {
					//update in memory for other stuff done e.g. rebuilds
					if (triggerAction < 3) {
						res.locals.board.settings.captchaMode = triggerAction;
						update['$set']['settings.captchaMode'] = triggerAction;
						enableCaptcha = true; //todo make this also returned after moving/refactoring this
					} else {
						res.locals.board.settings.lockMode = triggerAction-2;
						update['$set']['settings.lockMode'] = triggerAction-2;
					}
					return true;
				}
				return false;
			};
			const updatedPphTrigger = pphTriggerUpdate && calcTriggerMode(triggerUpdate, pphTrigger, pphTriggerAction, hourPosts.pph);
			const updatedTphTrigger = tphTriggerUpdate && calcTriggerMode(triggerUpdate, tphTrigger, tphTriggerAction, hourPosts.tph);
			if (updatedPphTrigger || updatedTphTrigger) {
				//set it in the db
				await Boards.updateOne(res.locals.board._id, triggerUpdate);
				await cache.sadd('triggered', res.locals.board._id);
			}
		}
	}

	//for cyclic threads, delete posts beyond bump limit
	if (thread && thread.cyclic && thread.replyposts >= replyLimit) {
		const cyclicOverflowPosts = await Posts.db.find({
			'thread': data.thread,
			'board': req.params.board
		}).sort({
			'postId': -1,
		}).skip(replyLimit).toArray();
		if (cyclicOverflowPosts.length > 0) {
			await deletePosts(cyclicOverflowPosts, req.params.board, res.locals);
			const fileCount = cyclicOverflowPosts.reduce((acc, post) => {
				return acc + (post.files ? post.files.length : 0);
			}, 0);
			//reduce amount counted in post by number of posts deleted
			await Posts.db.updateOne({
				'postId': thread.postId,
				'board': res.locals.board._id
			}, {
				'$inc': { //negative increment
					'replyposts': -cyclicOverflowPosts.length,
					'replyfiles': -fileCount
				}
			});
		}
	}

	const successRedirect = `/${req.params.board}/${req.path.endsWith('/modpost') ? 'manage/' : ''}thread/${req.body.thread || postId}.html#${postId}`;

	const buildOptions = {
		'threadId': data.thread || postId,
		'board': res.locals.board
	};

	//let frontend script know if captcha is still enabled
	res.set('x-captcha-enabled', captchaMode > 0);

	if (req.headers['x-using-live'] != null && data.thread) {
		//defer build and post will come live
		res.json({
			'postId': postId,
			'redirect': successRedirect
		});
		buildQueue.push({
			'task': 'buildThread',
			'options': buildOptions
		});
	} else {
		//build immediately and refresh when built
		await buildThread(buildOptions);
		if (req.headers['x-using-xhr'] != null) {
			res.json({
				'postId': postId,
				'redirect': successRedirect
			});
		} else {
			res.redirect(successRedirect);
		}
	}

	const projectedPost = {
		'_id': postMongoId,
		'u': data.u,
		'date': data.date,
		'name': data.name,
		'country': data.country,
		'board': req.params.board,
		'tripcode': data.tripcode,
		'capcode': data.capcode,
		'subject': data.subject,
		'message': data.message,
		'nomarkup': data.nomarkup,
		'thread': data.thread,
		'postId': postId,
		'email': data.email,
		'spoiler': data.spoiler,
		'banmessage': null,
		'userId': data.userId,
		'files': data.files,
		'reports': [],
		'globalreports': [],
		'quotes': data.quotes,
		'backlinks': [],
		'replyposts': 0,
		'replyfiles': 0,
		'sticky': data.sticky,
		'locked': data.locked,
		'bumplocked': data.bumplocked,
		'cyclic': data.cyclic,
		'signature': data.signature,
		'address': data.address,
	};
	if (data.thread) {
		//dont emit thread to this socket, because the room only exists when the thread is open
		Socketio.emitRoom(`${res.locals.board._id}-${data.thread}`, 'newPost', projectedPost);
	}
	const { raw, cloak, type } = data.ip;
	//but emit it to manage pages because they need to get all posts through socket including thread
	Socketio.emitRoom('globalmanage-recent-hashed', 'newPost', { ...projectedPost, ip: { cloak, raw: null, type } });
	Socketio.emitRoom(`${res.locals.board._id}-manage-recent-hashed`, 'newPost', { ...projectedPost, ip: { cloak, raw: null, type } });
	if (!dontStoreRawIps) {
        //no need to emit to these rooms if raw IPs are not stored
		Socketio.emitRoom('globalmanage-recent-raw', 'newPost', { ...projectedPost, ip: { cloak, raw, type } });
		Socketio.emitRoom(`${res.locals.board._id}-manage-recent-raw`, 'newPost', { ...projectedPost, ip: { cloak, raw, type } });
	}

	//now add other pages to be built in background
	if (enableCaptcha) {
		if (res.locals.board.settings.captchaMode == 2) {
			//only delete threads if all posts require threads, otherwise just build board pages for thread captcha
			await emptyDir(`${uploadDirectory}/html/${req.params.board}/thread/`); //not deleting json cos it doesnt need to be
		}
		const endPage = Math.ceil(threadLimit/10);
		buildQueue.push({
			'task': 'buildBoardMultiple',
			'options': {
				'board': res.locals.board,
				'startpage': 1,
				'endpage': endPage
			}
		});
	} else if (data.thread) {
		//refersh pages
		if (data.email === 'sage' || thread.bumplocked) {
			//refresh the page that the thread is on
			buildQueue.push({
				'task': 'buildBoard',
				'options': {
					'board': res.locals.board,
					'page': threadPage
				}
			});
		} else {
			//if not saged, it will bump so we should refresh any pages above it as well
			buildQueue.push({
				'task': 'buildBoardMultiple',
				'options': {
					'board': res.locals.board,
					'startpage': 1,
					'endpage': threadPage
				}
			});
		}
	} else if (!data.thread) {
		//new thread, prunes any old threads before rebuilds
		const prunedThreads = await Posts.pruneThreads(res.locals.board);
		if (prunedThreads.length > 0) {
			await deletePosts(prunedThreads, req.params.board, res.locals);
		}
		if (!enableCaptcha) {
			const endPage = Math.ceil(threadLimit/10);
			buildQueue.push({
				'task': 'buildBoardMultiple',
				'options': {
					'board': res.locals.board,
					'startpage': 1,
					'endpage': endPage
				}
			});
		}
	}

	//always rebuild catalog for post counts and ordering
	buildQueue.push({
		'task': 'buildCatalog',
		'options': {
			'board': res.locals.board,
		}
	});

};


code:
'use strict';

const uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, { remove } = require('fs-extra')
	, { Posts } = require(__dirname+'/../../db/')
	, Socketio = require(__dirname+'/../../lib/misc/socketio.js')
	, { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js')
	, messageHandler = require(__dirname+'/../../lib/post/message.js')
	, { createHash } = require('crypto');

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

	const { __ } = res.locals;
	const { threads, postIds, postMongoIds } = res.locals.posts
		.sort((a, b) => {
			return a.date - b.date; //could do postId, doesn't really matter.
		}).reduce((acc, p) => {
			acc.postIds.push(p.postId);
			acc.postMongoIds.push(p._id);
			if (p.thread === null) {
				acc.threads.push(p);
			}
			return acc;
		}, { threads: [], postIds: [], postMongoIds: [] });

	//maybe should filter these? because it will include threads from which child posts are already fetched in the action handler, unlike the deleteposts model
	const moveEmits = res.locals.posts.reduce((acc, post) => {
		acc.push({
			room: `${post.board}-${post.thread || post.postId}`,
			postId: post.postId,
		});
		return acc;
	}, []);

	const backlinkRebuilds = new Set();
	const bulkWrites = [];

	//remove backlinks from selected posts that link to unselected posts
	bulkWrites.push({
		'updateMany': {
			'filter': {
				'_id': {
					'$in': postMongoIds
				}
			},
			'update': {
				'$pull': {
					'backlinks': {
						'postId': {
							'$nin': postIds
						}
					}
				}
			}
		}
	});

	for (let j = 0; j < res.locals.posts.length; j++) {
		const post = res.locals.posts[j];
//note: needs debugging
//		if (post.crossquotes.filter(c => c.thread === req.body.move_to_thread).length > 0) {
			//a crossquote is in the thread we move to, so need to remarkup and add backlinks to those posts
		backlinkRebuilds.add(post._id);
//		}
		//get backlinks for posts to remarkup
		for (let i = 0; i < post.backlinks.length; i++) {
			backlinkRebuilds.add(post.backlinks[i]._id);
		}
		//remove dead backlinks to this post
		if (post.quotes.length > 0) {
			backlinkRebuilds.add(post._id);
			bulkWrites.push({
				'updateMany': {
					'filter': {
						'_id': {
							'$in': post.quotes.map(q => q._id)
						}
					},
					'update': {
						'$pull': {
							'backlinks': {
								'postId': post.postId
							}
						}
					}
				}
			});
		}
	}

	//increase file/reply count in thread we are moving the posts to
	if (!res.locals.destinationBoard) {
		//recalculateThreadMetadata will handle cross board moves
		const { replyposts, replyfiles } = res.locals.posts.reduce((acc, p) => {
			acc.replyposts += 1;
			acc.replyfiles += p.files.length;
			return acc;
		}, { replyposts: 0, replyfiles: 0 });
		bulkWrites.push({
			'updateOne': {
				'filter': {
					'postId': req.body.move_to_thread,
					'board': req.params.board,
				},
				'update': {
					'$inc': {
						'replyposts': replyposts,
						'replyfiles': replyfiles,
					}
				}
			}
		});
	}

	const destinationBoard = res.locals.destinationBoard ? res.locals.destinationBoard._id : req.params.board;
	const crossBoard = destinationBoard !== req.params.board;
	let destinationThreadId = res.locals.destinationThread ? res.locals.destinationThread.postId : (crossBoard ? null : postIds[0])
		, movedPosts = 0;
	({ destinationThreadId, movedPosts } = await Posts.move(postMongoIds, crossBoard, destinationThreadId, destinationBoard));

	//emit markPost moves
	for (let i = 0; i < moveEmits.length; i++) {
		Socketio.emitRoom(moveEmits[i].room, 'markPost', { postId: moveEmits[i].postId, type: 'move' });
	}

	//no destination thread specified (making new thread from posts), need to fetch OP as destinationThread for remarkup/salt
	if (!res.locals.destinationThread) {
		res.locals.destinationThread = await Posts.getPost(destinationBoard, destinationThreadId);
	}

	//get posts that quoted moved posts so we can remarkup them
	if (backlinkRebuilds.size > 0) {
		const remarkupPosts = await Posts.globalGetPosts([...backlinkRebuilds]);
		await Promise.all(remarkupPosts.map(async post => { //doing these all at once
			const postUpdate = {};
			//update post message and/or id
			if (post.userId) {
				let userId = createHash('sha256').update(res.locals.destinationThread.salt + post.ip.raw).digest('hex');
				userId = userId.substring(userId.length-6);
				postUpdate.userId = userId;
			}
			if (post.nomarkup && post.nomarkup.length > 0) {
				const nomarkup = prepareMarkdown(post.nomarkup, false);
				const { message, quotes, crossquotes } = await messageHandler(nomarkup, post.board, post.thread, null);
				bulkWrites.push({
					'updateMany': {
						'filter': {
							'_id': {
								'$in': quotes.map(q => q._id)
							}
						},
						'update': {
							'$push': {
								'backlinks': { _id: post._id, postId: post.postId }
							}
						}
					}
				});
				postUpdate.quotes = quotes;
				postUpdate.crossquotes = crossquotes;
				postUpdate.message = message;
			}
			if (Object.keys(postUpdate).length > 0) {
				bulkWrites.push({
					'updateOne': {
						'filter': {
							'_id': post._id
						},
						'update': {
							'$set': postUpdate
						}
					}
				});
			}
		}));
	}

	//bulkwrite it all
	if (bulkWrites.length > 0) {
		await Posts.db.bulkWrite(bulkWrites);
	}

	//delete html/json for no longer existing threads, because op was moved
	if (threads.length > 0) {
		await Promise.all(threads.map(thread => {
			return Promise.all([
				remove(`${uploadDirectory}/html/${thread.board}/thread/${thread.postId}.html`),
				remove(`${uploadDirectory}/json/${thread.board}/thread/${thread.postId}.json`)
			]);
		}));
	}

	return {
		message: __('Moved posts'),
		action: movedPosts > 0,
	};

};


code:
'use strict';

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

	res.clearCookie('captchaid');
	return res.redirect('/captcha.html');

};


code:
'use strict';

const { 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;
	let original, username, password;
	if (res.locals.isWeb3) {
		original = req.body.address;
		username = req.body.address.toLowerCase();
		password = null;
	} else {
		original = req.body.username;
		username = original.toLowerCase(); //lowercase to prevent duplicates with mixed case
		password = req.body.password;
	}

	const account = await Accounts.findOne(username);

	// if the account exists reject
	if (account != null) {
		return dynamicResponse(req, res, 409, 'message', {
			'title': __('Conflict'),
			'message': __('Account with that username already exists'),
			'redirect': '/register.html'
		});
	}

	await Accounts.insertOne(original, username, password, roleManager.roles.ANON, res.locals.isWeb3);

	if (res.locals.isWeb3) {
		req.session.user = username;
		await Accounts.updateLastActiveDate(username);
		let goto = req.body.goto;
		if (goto == null || !/^\/[0-9a-zA-Z][0-9a-zA-Z._/-]*$/.test(goto)) {
			goto = '/account.html';
		}
		return res.redirect(goto);
	}

	return res.redirect('/login.html');

};


code:
'use strict';

const { Bans } = require(__dirname+'/../../db/')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js');

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

	const showGlobal = res.locals.permissions.get(Permissions.VIEW_BOARD_GLOBAL_BANS);
	const bansBoard = req.params && req.params.board
		? (showGlobal
			? req.params.board
			: { '$eq': req.params.board })
		: null;
	return Bans.removeMany(bansBoard, req.body.checkedbans).then(result => result.deletedCount);

};


code:
'use strict';

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

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

	const { __n } = res.locals;

	const report = {
		'id': ObjectId(),
		'reason': req.body.report_reason,
		'date': new Date(),
		'ip': {
			'cloak': res.locals.ip.cloak,
			'raw': res.locals.ip.raw,
			'type': res.locals.ip.type,
		}
	};

	const ret = {
		message: __n('Reported %s posts', res.locals.posts.length),
		action: '$push',
		query: {}
	};
	const query = {
		'$each': [report],
		'$slice': -5 //limit number of  reports
	};
	if (req.body.global_report) {
		ret.query['globalreports'] = query;
	}
	if (req.body.report) {
		ret.query['reports'] = query;
	}

	return ret;

};


code:
'use strict';

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

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

	const { __ } = res.locals;
	const moderatesBoard = res.locals.user.staffBoards.includes(req.body.board);
	const ownsBoard = res.locals.user.ownedBoards.includes(req.body.board);
	if (!ownsBoard && !moderatesBoard) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad request'),
			'message': __('You do not own or moderate that board'),
			'redirect': '/account.html'
		});
	}

	if (ownsBoard) {
		await Promise.all([
			Accounts.removeOwnedBoard(res.locals.user.username, req.body.board),
			Boards.setOwner(req.body.board, null),
			Boards.removeStaff(req.body.board, [res.locals.user.username]),
		]);
	} else if (moderatesBoard) {
		await Promise.all([
			Boards.removeStaff(req.body.board, [res.locals.user.username]),
			Accounts.removeStaffBoard([res.locals.user.username], req.body.board),
		]);
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __(`Resigned from ${ownsBoard ? 'owner' : 'staff'} position on /%s/`, req.body.board),
		'redirect': '/account.html'
	});

};


code:
'use strict';

module.exports = (locals) => {

	const { __, __n, posts } = locals;

	// filter to ones not spoilered
	const filteredPosts = posts.filter(post => {
		return !post.spoiler && post.files.length > 0;
	});

	if (filteredPosts.length === 0) {
		return {
			message: __('No files to spoiler'),
		};
	}

	return {
		message: __n('Spoilered %s posts', filteredPosts.length),
		action: '$set',
		query: {
			'spoiler': true
		}
	};

};


code:
'use strict';

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

module.exports = (locals, sticky) => {

	const { posts, __, __n } = locals;

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

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

	const stickyValue = NumberInt(sticky);

	return {
		message: __n('Set Sticky level for %s threads to %s', 'Set Sticky level for %s threads to %s', filteredposts.length, sticky),
		action: '$set',
		query: {
			'sticky': stickyValue,
		}
	};

};


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;
	const newOwner = res.locals.newOwner;

	//remove current owner
	await Promise.all([
		Accounts.removeOwnedBoard(res.locals.board.owner, req.params.board),
		Boards.removeStaff(req.params.board, [res.locals.board.owner]),
	]);

	//set new owner in locals
	res.locals.board.owner = newOwner._id;

	if (res.locals.board.staff[newOwner._id] != null) {
		//if already a staff, just change their permission instead of removing+adding back
		await Promise.all([
			Boards.setStaffPermissions(req.params.board, newOwner._id, roleManager.roles.BOARD_OWNER, true),
			Accounts.removeStaffBoard([newOwner._id], req.params.board),
			Accounts.addOwnedBoard(newOwner._id, req.params.board),
		]);
	} else {
		//otherwise add them as a new staff+owner
		await Promise.all([
			Boards.addStaff(req.params.board, newOwner._id, roleManager.roles.BOARD_OWNER, true),
			Accounts.addOwnedBoard(newOwner._id, req.params.board),
		]);
	}

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

};


code:
'use strict';

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

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

	const { __ } = res.locals;
	const username = res.locals.user.username.toLowerCase();

	// Get the temporary secret from redis and check it exists
	const tempSecret = await redis.get(`twofactor_tempsecret:${username}`);
	if (!tempSecret || !username) {
		return dynamicResponse(req, res, 403, 'message', {
			'title': __('Forbidden'),
			'message': __('2FA QR code expired, try again'),
			'redirect': '/twofactor.html',
		});
	}

	// Validate totp
	const delta = await doTwoFactor(username, tempSecret, req.body.twofactor);

	// Check if code was valid
	if (delta === null) {
		return dynamicResponse(req, res, 403, 'message', {
			'title': __('Forbidden'),
			'message': __('Incorrect 2FA code'),
			'redirect': '/twofactor.html',
		});
	}
	redis.del(`twofactor_tempsecret:${username}`);

	// Successfully enabled 2FA
	await Accounts.updateTwofactor(username, tempSecret);

	// Logout all sessions, 2FA now required
	await Promise.all([
		req.session.destroy(),
		redis.del(`users:${username}`),
		redis.deletePattern(`sess:*:${username}`),
	]);

	return dynamicResponse(req, res, 200, 'message', {
		'title': __('Success'),
		'message': __('Two factor authentication enabled successfully'),
		'redirect': '/login.html',
	});

};


code:
'use strict';

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

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

	const nReturned = await Bans.upgrade(res.locals.bansBoard, req.body.checkedbans, req.body.upgrade)
		.then(explain => {
			if (explain && explain.stages) {
				return explain.stages[0].nReturned;
			}
			return 0;
		});

	return nReturned;

};


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')
	, getDimensions = require(__dirname+'/../../lib/file/image/getdimensions.js')
	, deleteTempFiles = require(__dirname+'/../../lib/file/deletetempfiles.js')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, { Boards } = require(__dirname+'/../../db/')
	, buildQueue = require(__dirname+'/../../lib/build/queue.js');

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

	const { __, __n } = res.locals;
	const { globalLimits, 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, {
				//banners can be static image or animated (gif, apng, etc)
			image: true,
			animatedImage: true,
			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) {
			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
				});
			}
		}

		//300x100 check
		const imageDimensions = await getDimensions(req.files.file[i].tempFilePath, null, true);
		let geometry = imageDimensions;
		if (Array.isArray(geometry)) {
			geometry = geometry[0];
		}
		if (geometry.width > globalLimits.bannerFiles.width
			|| geometry.height > globalLimits.bannerFiles.height
			|| (globalLimits.bannerFiles.forceAspectRatio === true
				&& (geometry.width/geometry.height !== 3))) {
			await deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'message': __(`Invalid file "%s". Max banner dimensions are %sx%s${globalLimits.bannerFiles.forceAspectRatio === true ? ' and must be a 3:1 aspect ratio' : '' }.`, req.files.file[i].name, globalLimits.bannerFiles.width, globalLimits.bannerFiles.height),
				'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}/banner/${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, `banner/${req.params.board}`);

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

	}

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

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

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

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

	if (filenames.length > 0) {
		//add public banners page to build queue
		buildQueue.push({
			'task': 'buildBanners',
			'options': {
				'board': res.locals.board,
			}
		});
	}

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

};


code:
'use strict';

const { Posts, Boards } = require(__dirname+'/../../db/')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
	, Permission = require(__dirname+'/../../lib/permission/permission.js')
	, roleManager = require(__dirname+'/../../lib/permission/rolemanager.js');

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

	const { forceActionTwofactor } = config.get;
	let boardReportCountMap = {}; //map of board to open report count
	let globalReportCount = 0; //number of open global reports
	let boardPermissions; //map of board perms
	let userBoards = [];

	let boardReportCounts;
	try {
		userBoards = res.locals.user.ownedBoards.concat(res.locals.user.staffBoards);
		([boardReportCounts, boardPermissions, globalReportCount] = await Promise.all([
			userBoards.length > 0 ? Posts.getBoardReportCounts(userBoards) : [],
			res.locals.user.staffBoards.length > 0 ? Boards.getStaffPerms(res.locals.user.staffBoards, res.locals.user.username) : null,
			res.locals.permissions.get(Permissions.MANAGE_GLOBAL_GENERAL) ? Posts.getGlobalReportsCount() : 0,
		]));
	} catch (err) {
		return next(err);
	}

	if (boardReportCounts && boardReportCounts.length > 0) {
		//make the aggregate array from mongodb to a map
		boardReportCountMap = boardReportCounts.reduce((acc, val) => {
			acc[val._id] = val.count;
			return acc;
		}, boardReportCountMap);
	}

	if (boardPermissions) {
		//calcperms isnt multi-board (but neither is this, really) its alright until then
		boardPermissions = boardPermissions.reduce((acc, bs) => {
			acc[bs._id] = new Permission(bs.staff[res.locals.user.username].permissions.toString('base64'));
			const boardRolePermission = bs.owner === res.locals.user.username
				? roleManager.roles.BOARD_OWNER
				: roleManager.roles.BOARD_STAFF;
			for (let bit of Permissions._MANAGE_BOARD_BITS) {
				const inheritOrGlobal = res.locals.permissions.get(bit)
					|| (acc[bs._id].get(bit)
						&& boardRolePermission.get(bit));
				acc[bs._id].set(bit, inheritOrGlobal);
			}
			return acc;
		}, {});
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('account', {
			csrf: req.csrfToken(),
			user: res.locals.user,
			permissions: res.locals.permissions,
			boardPermissions,
			boardReportCountMap,
			globalReportCount,
			forceActionTwofactor,
		});

};


code:
'use strict';

const { buildBanners } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html, json;
	try {
		({ html, json } = await buildBanners({ board: res.locals.board }));
	} catch (err) {
		return next(err);
	}

	if (req.path.endsWith('.json')) {
		return res.set('Cache-Control', 'max-age=0').json(json);
	} else {
		return res.set('Cache-Control', 'max-age=0').send(html);
	}

};


code:
'use strict';

const { buildBypass } = require(__dirname+'/../../lib/build/tasks.js');

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

	if (res.locals.minimal === true) {
		return res
			.set('Cache-Control', 'public, max-age=60')
			.render('bypass', {
				minimal: true
			});
	}

	let html;
	try {
		html = await buildBypass(res.locals.minimal);
	} catch (err) {
		return next(err);
	}

	return res.set('Cache-Control', 'max-age=0').send(html);

};


code:
'use strict';

const Posts = require(__dirname+'/../../db/posts.js')
	, { buildBoard } = require(__dirname+'/../../lib/build/tasks.js');

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

	const page = req.params.page === 'index' ? 1 : Number(req.params.page);
	let html, json;
	try {
		const maxPage = Math.min(Math.ceil((await Posts.getPages(req.params.board)) / 10), Math.ceil(res.locals.board.settings.threadLimit/10)) || 1;
		if (page > maxPage) {
			return next();
		}
		({ html, json } = await buildBoard({
			board: res.locals.board,
			page,
			maxPage
		}));
	} catch (err) {
		return next(err);
	}

	if (req.path.endsWith('.json')) {
		return res.set('Cache-Control', 'max-age=0').json(json);
	} else {
		return res.set('Cache-Control', 'max-age=0').send(html);
	}

};


code:
'use strict';

const config = require(__dirname+'/../../lib/misc/config.js')
	, { Boards } = require(__dirname+'/../../db/')
	, cache = require(__dirname+'/../../lib/redis/redis.js')
	, { relativeColor, relativeString } = require(__dirname+'/../../lib/converter/timeutils.js')
	, pageQueryConverter = require(__dirname+'/../../lib/input/pagequeryconverter.js')
	, limit = 30;

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

	const { meta, enableWebring } = config.get;
	const { page, offset, queryString } = pageQueryConverter(req.query, limit);
	const direction = req.query.direction && req.query.direction === 'asc' ? 1 : -1;
	const search = (typeof req.query.search === 'string' ? req.query.search : null);
	const localFirst = req.query.local_first != null;
	const sortType = req.query.sort && req.query.sort === 'activity' ? 'activity' : 'popularity';
	let sort = {};

	const siteNames = enableWebring === true ? [meta.siteName, ...(await Boards.webringSites())] : [meta.siteName];
	const siteNamesSet = new Set(siteNames);

	let selectedSites = req.query.sites ? (typeof req.query.sites === 'string' ? [req.query.sites] : req.query.sites) : [];
	let validSelectedSites = selectedSites.filter(ss => siteNamesSet.has(ss));
	if (validSelectedSites.length === 0) {
		validSelectedSites = siteNames;
	}
	const validSelectedSitesSet = new Set(validSelectedSites);

	const cacheQuery = new URLSearchParams({ direction, sort: sortType, search, page, localFirst, selectedSites: [...validSelectedSitesSet] });
	cacheQuery.sort();
	const cacheQueryString = cacheQuery.toString();
	const cached = await cache.get(`boardlist:${cacheQueryString}`);
	const { shown, notShown } = siteNames.reduce((acc, sn) => {
		if (validSelectedSitesSet.has(sn)) {
			acc.shown.push(sn);
		} else {
			acc.notShown.push(sn);
		}
		return acc;
	}, { shown: [], notShown: [] });
	validSelectedSites = validSelectedSites.map(x => x === meta.siteName ? null : x);

	if (localFirst && validSelectedSitesSet.has(meta.siteName)) {
		//meh.
		sort['webring'] = 1;
	}
	let boards, maxPage;
	if (cached) {
		({ boards, maxPage } = cached);
	} else {

		if (sortType === 'activity') {
			sort['lastPostTimestamp'] = direction;
		} else {
			sort['ips'] = direction;
			sort['pph'] = direction;
			sort['sequence_value'] = direction;
		}

		let filter = {};
		if (req.query.search && search) {
			filter = {
				'search': search
			};
		}

		try {
			[ boards, maxPage ] = await Promise.all([
				Boards.boardSort(offset, limit, sort, filter, false, validSelectedSites),
				Boards.count(filter, false, validSelectedSites),
			]);
			maxPage = Math.ceil(maxPage/limit);
		} catch (err) {
			return next(err);
		}
		const now = new Date();
		for (let i = 0; i < boards.length; i++) {
			if (boards[i].lastPostTimestamp) {
				const lastPostDate = new Date(boards[i].lastPostTimestamp);
				boards[i].lastPostTimestamp = {
					text: relativeString(now, lastPostDate, res.locals),
					color: relativeColor(now, lastPostDate)
				};
			}
		}
		cache.set(`boardlist:${cacheQueryString}`, { boards, maxPage }, 60);
	}

	res
		.set('Cache-Control', 'public, max-age=60');

	if (req.path === '/boards.json') {
		res.json({
			boards,
			page,
			maxPage,
		});
	} else {
		res.render('boardlist', {
			boards,
			shown,
			notShown,
			page,
			maxPage,
			query: req.query,
			localFirst,
			search,
			queryString,
		});
	}

};


code:
'use strict';

const { buildBoardSettings } = require(__dirname+'/../../lib/build/tasks.js');

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

	let json;
	try {
		json = await buildBoardSettings({ board: res.locals.board });
	} catch (err) {
		return next(err);
	}

	return res.json(json);

};


code:
'use strict';

const { Captchas, Ratelimits } = require(__dirname+'/../../db/')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, production = process.env.NODE_ENV === 'production';

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

	const { secureCookies, rateLimitCost, captchaOptions } = config.get;
	if (!['text', 'grid', 'grid2'].includes(captchaOptions.type)) {
		return next(); //only grid and text captcha continue
	}

	const generateCaptcha = require(__dirname+`/../../lib/captcha/generators/${captchaOptions.type}.js`);

	if (!production && req.cookies['captchaid'] != null) {
		return res.redirect(`/captcha/${req.cookies['captchaid']}.jpg`);
	}

	let captchaId;
	let maxAge = 5*60*1000;
	try {
		if (!res.locals.anonymizer) {
			const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'captcha', rateLimitCost.captcha);
			if (ratelimit > 100) {
				return res.status(429).redirect('/file/ratelimit.png');
			}
		}
		const captchaCount = await Captchas.db.estimatedDocumentCount();
		if (captchaCount >= captchaOptions.generateLimit) {
			const captchaSample = await Captchas.randomSample();
			const randomCaptcha = captchaSample[0];
			captchaId = randomCaptcha._id;
			maxAge = Math.abs((randomCaptcha.expireAt.getTime()+maxAge) - Date.now()); //abs in case mongo hasn't pruned, and will not be too big since it can't be too far away from pruning anyway
		} else {
			const { captcha, solution } = await generateCaptcha(captchaOptions);
			captchaId = await Captchas.insertOne(solution).then(r => r.insertedId);
			//captcha.write doesn't like to be util.promisify'd
			await (new Promise((resolve, reject) => {
				captcha.write(`${uploadDirectory}/captcha/${captchaId}.jpg`, (err) => {
					if (err) {
						return reject(err);
					}
					resolve();
				});
			}));
		}
	} catch (err) {
		return next(err);
	}

	return res
		.cookie('captchaid', captchaId.toString(), {
			'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
			'sameSite': 'strict',
			maxAge,
		})
		.redirect(`/captcha/${captchaId}.jpg`);

};


code:
'use strict';

const { buildCaptcha } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html;
	try {
		html = await buildCaptcha();
	} catch (err) {
		return next(err);
	}

	return res.set('Cache-Control', 'max-age=0').send(html);

};


code:
'use strict';

const { buildCatalog } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html, json;
	try {
		({ html, json } = await buildCatalog({ board: res.locals.board }));
	} catch (err) {
		return next(err);
	}

	if (req.path.endsWith('.json')) {
		return res.set('Cache-Control', 'max-age=0').json(json);
	} else {
		return res.set('Cache-Control', 'max-age=0').send(html);
	}

};


code:
'use strict';

const { buildChangePassword } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html;
	try {
		html = await buildChangePassword();
	} catch (err) {
		return next(err);
	}

	return res.set('Cache-Control', 'max-age=0').send(html);

};


code:
'use strict';

//const { buildCreate } = require(__dirname+'/../../lib/build/tasks.js');

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

	return res.render('create');

/*
	let html;
	try {
		html = await buildCreate();
	} catch (err) {
		return next(err);
	}

	return res.send(html);
*/

};


code:
'use strict';

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

	res.json({
		token: req.csrfToken(),
	});

};


code:
'use strict';

const { buildCustomPage } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html, json;
	try {
		({ html, json } = await buildCustomPage({ ...req.params, board: res.locals.board }));
	} catch (err) {
		return next(err);
	}

	if (!html) {
		return next();
	}

	if (req.path.endsWith('.json')) {
		return res.set('Cache-Control', 'max-age=0').json(json);
	} else {
		return res.set('Cache-Control', 'max-age=0').send(html);
	}

};


code:
'use strict';

const { Accounts } = require(__dirname+'/../../../db/')
	, config = require(__dirname+'/../../../lib/misc/config.js')
	, roleManager = require(__dirname+'/../../../lib/permission/rolemanager.js')
	, pageQueryConverter = require(__dirname+'/../../../lib/input/pagequeryconverter.js')
	, limit = 20;

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

	const { forceActionTwofactor } = config.get;
	const { page, offset, queryString } = pageQueryConverter(req.query, limit);

	let filter = {};
	const username = (typeof req.query.username === 'string' ? req.query.username : null);
	if (username) {
		filter['_id'] = username;
	}
	const uri = (typeof req.query.uri === 'string' ? req.query.uri  : null);
	if (uri) {
		filter['$or'] = [
			{
				'ownedBoards': uri
			},
			{
				'staffBoards': uri
			},
		];
	}

	let accounts, maxPage;
	try {
		[accounts, maxPage] = await Promise.all([
			Accounts.find(filter, offset, limit),
			Accounts.count(filter),
		]);
		maxPage = Math.ceil(maxPage/limit);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('globalmanageaccounts', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			user: res.locals.user,
			queryString,
			username,
			uri,
			accounts,
			page,
			maxPage,
			rolePermissionMap: roleManager.rolePermissionMap,
			roleNameMap: roleManager.roleNameMap,
			forceActionTwofactor,
		});

};


code:
'use strict';

const { Bans } = require(__dirname+'/../../../db/')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js');

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

	let bans;
	try {
		bans = await Bans.getGlobalBans();
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('globalmanagebans', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP),
			bans,
		});

};