code:
'use strict';

const YEAR = 31536000000
	, MONTH = 2629800000
	, WEEK = 604800000
	, DAY = 86400000
	, HOUR = 3600000
	, MINUTE = 60000
	, SECOND = 1000;

module.exports = {

	YEAR, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND,

	//ms until next hour
	'nextHour': () => {
		return 3600000 - new Date().getTime() % 3600000;
	},

	//string representing how long since date A to date B
	'relativeString': (now, relativeTo, locals) => {
		let difference = now.getTime() - relativeTo.getTime();
		let amount = 0;
		let unit = '';
		let isFuture = false;
		if (difference < 0) {
			difference = Math.abs(difference);
			isFuture = true;
		}
		if (difference < MINUTE) {
			return locals.__('Now');
		} else if (difference < MINUTE*59.5) {
			amount = Math.round(difference / MINUTE);
			unit = 'minute';
		} else if (difference < HOUR*23.5) {
			amount = Math.round(difference / HOUR);
			unit = 'hour';
		} else if (difference < DAY*6.5) {
			amount = Math.round(difference / DAY);
			unit = 'day';
		} else if (difference < WEEK*3.5) {
			amount = Math.round(difference / WEEK);
			unit = 'week';
		} else if (difference < MONTH*11.5) {
			amount = Math.round(difference / MONTH);
			unit = 'month';
		} else {
			amount = Math.round(difference / YEAR);
			unit = 'year';
		}
		return locals.__n(`%s ${unit} ${isFuture ? 'from now' :  'ago'}`, amount);
	},

	'relativeColor': (now, relativeTo) => {
		const difference = now.getTime() - relativeTo.getTime();
		const minutesAgo = Math.floor(difference / MINUTE);
		let r = 0
			, g = 0
			, b = 0;
		if (difference < MINUTE) {
			g = 0.7;
			b = 1;
		} else if (difference < HOUR) {
			r = (minutesAgo / 60) * 0.5;
			g = 1;
		} else if (difference < DAY) {
			r = 0.5 + (minutesAgo / 1440) * 0.5;
			g = 1;
		} else if (difference < WEEK) {
			g = 1 - (minutesAgo / 10080) * 0.5;
			r = 1;
		} else if (difference < MONTH) {
			g = 0.5 - (minutesAgo / 43830) * 0.5;
			r = 1;
		} else if (difference < YEAR) {
			r = 1 - (minutesAgo / 525960);
		} //else, leave it black for >1 year
		r = (Math.round(r*255*0.85).toString(16)).padStart(2, '0');
		g = (Math.round(g*255*0.85).toString(16)).padStart(2, '0');
		b = (Math.round(b*255).toString(16)).padStart(2, '0');
		return `#${r}${g}${b}`;
	},

	'durationString': (ms) => {
		const durString = new Date(ms).toISOString();
		if (ms < DAY) {
			return durString.substr(11,8).replace(/^00:/, '');
		} else {
			const hours = Math.floor(ms/HOUR);
			return `${hours}:${durString.substring(14,19)}`;
		}
	},

	'timeDiffString': (label, end) => {
		return (`${end[0] > 0 ? end[0]+'s' : ''}${Math.trunc(end[1]/1000000)}ms `).padStart(9) + label;
	},

};


code:
const ffmpeg = require('fluent-ffmpeg')
	, config = require(__dirname+'/../../misc/config.js')
	, uploadDirectory = require(__dirname+'/../uploaddirectory.js');

module.exports = (file) => {

	const { thumbSize } = config.get;
	return new Promise((resolve, reject) => {
		ffmpeg(`${uploadDirectory}/file/${file.filename}`)
			.on('end', () => {
				return resolve();
			})
			.on('error', function(err) {
				return reject(err);
			})
			.complexFilter([{
				filter: 'showwavespic',
				options: { split_channels: 1, s: `${thumbSize}x${thumbSize}` }
			}])
			.save(`${uploadDirectory}/file/thumb/${file.hash}${file.thumbextension}`);
	});

};

repo_name: Marzisoft/jschan

license: mit


code:
'use strict';

const { remove } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/uploaddirectory.js');

module.exports = async (filenames, folder) => {

	await Promise.all(filenames.map(async filename => {
		remove(`${uploadDirectory}/${folder}/${filename}`);
	}));

};


code:
'use strict';

const { stat, remove, readdir } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/uploaddirectory.js');

//takes directory name and timestamp to delete files older than
module.exports = async (directory, olderThan) => {
	const dir = `${uploadDirectory}/${directory}`;
	const files = await readdir(dir);
	if (files.length > 0) {
		return Promise.all(files.map(async file => {
			const filePath = `${dir}/${file}`;
			try {
				const stats = await stat(filePath);
				const expiry = new Date(olderThan).getTime();
				if (stats.ctime.getTime() < expiry) {
					await remove(filePath);
				}
			} catch (e) {
				console.error(e);
			}
		}));
	}
};


code:
'use strict';

const { remove } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/uploaddirectory.js');

module.exports = (files) => {

	//delete all the files and thumbs
	return Promise.all(files.map(async file => {
		return Promise.all([
			remove(`${uploadDirectory}/file/${file.filename}`),
			file.hasThumb ? remove(`${uploadDirectory}/file/thumb/${file.hash}${file.thumbextension}`) : void 0,
		]).catch(console.error);
	}));

};


code:
'use strict';

const { remove } = require('fs-extra');

module.exports = async (req) => {

	if (req.files != null) {
		let files = [];
		const keys = Object.keys(req.files);
		for (let i = 0; i < keys.length; i++) {
			const val = req.files[keys[i]];
			if (Array.isArray(val)) {
				files = files.concat(val);
			} else {
				files.push(val);
			}
		}
		return Promise.all(files.map(async file => {
			remove(file.tempFilePath);
		}));
	}

};


code:
'use strict';

const ffmpeg = require('fluent-ffmpeg')
	, uploadDirectory = require(__dirname+'/uploaddirectory.js');

module.exports = (filename, folder, temp) => {

	return new Promise((resolve, reject) => {
		ffmpeg.ffprobe(temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`, (err, metadata) => {
			if (err) {
				return reject(err);
			}
			return resolve(metadata);
		});
	});

};


code:
'use strict';

module.exports = (file) => {
	//handle gifs with multiple geometry and size
	if (Array.isArray(file.geometry)) {
		file.geometry = file.geometry[0];
	}
	if (Array.isArray(file.sizeString)) {
		file.sizeString = file.sizeString[0];
	}
	if (Array.isArray(file.geometryString)) {
		file.geometryString = file.geometryString[0];
	}
	return file;
};


code:
const gm = require('@fatchan/gm')
	, uploadDirectory = require(__dirname+'/../uploaddirectory.js');

module.exports = (filename, folder, temp) => {

	return new Promise((resolve, reject) => {
		const filePath = temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`;
		gm(`${filePath}[0]`) //0 for first frame of gifs, much faster
			.size(function (err, data) {
				if (err) {
					return reject(err);
				}
				return resolve(data);
			});
	});

};


code:
const gm = require('@fatchan/gm')
	, im = require('@fatchan/gm').subClass({ imageMagick: true })
	, ffmpeg = require('fluent-ffmpeg')
	, config = require(__dirname+'/../../misc/config.js')
	, uploadDirectory = require(__dirname+'/../uploaddirectory.js');

module.exports = (file) => {

	const { thumbSize, ffmpegGifThumbnails, animatedGifThumbnails } = config.get;

	//decide whether to animate gif thumbnail and update thumbextension
	let firstFrameOnly = true;
	if (file.hasThumb
		&& file.mimetype === 'image/gif'
		&& animatedGifThumbnails === true) {
		firstFrameOnly = false;
		file.thumbextension = '.gif';
	}

	//if enabled, make animated gif thumbs with ffmpeg
	if (ffmpegGifThumbnails && !firstFrameOnly) {
		const thumbSizeFilter = file.geometry.width > file.geometry.height ? `${thumbSize}:-1` : `-1:${thumbSize}`;
		const complexFilters = [
				/* this complex filter scales (resizes), and works some magic to preserve transparency,
					with an additional filter to reduce the aliasing on outlines of transparent parts. */
			`[0:v] scale=${thumbSizeFilter},split [a][b]; [a] palettegen=reserve_transparent=on:transparency_color=ffffff [p]; [b][p] paletteuse`
		];
		return new Promise((resolve, reject) => {
			ffmpeg(`${uploadDirectory}/file/${file.filename}`)
				.on('end', () => {
					return resolve();
				})
				.on('error', function(err) {
					return reject(err);
				})
				.outputOptions('-loop 0')
				.complexFilter(complexFilters)
				.save(`${uploadDirectory}/file/thumb/${file.hash}${file.thumbextension}`);
		});
	}

	//else we are using im/gm
	let thumbnailing;
	if (file.mimetype === 'image/webp') {
		//animated webp (which we cant tell apart from non-anim) need to be processed with imagemagick and cant make animated thumbs
		thumbnailing = im(`${uploadDirectory}/file/${file.filename}[0]`);
	} else {
		thumbnailing = gm(`${uploadDirectory}/file/${file.filename}${firstFrameOnly ? '[0]' : ''}`);
		if (!firstFrameOnly) {
			//try (and fail) to make gm thumbnailing less shit.
			thumbnailing.coalesce();
		}
	}

	return new Promise((resolve, reject) => {
		thumbnailing.resize(Math.min(thumbSize, file.geometry.width), Math.min(thumbSize, file.geometry.height))
			.write(`${uploadDirectory}/file/thumb/${file.hash}${file.thumbextension}`, function (err) {
				if (err) {
					return reject(err);
				}
				return resolve();
			});
	});

};


code:
'use strict';

const FileType = require('file-type')
	, config = require(__dirname+'/../misc/config.js')
	, { addCallback } = require(__dirname+'/../redis/redis.js')
	, image = new Set([
		'image/jpeg',
		'image/pjpeg',
		'image/png',
		'image/bmp',
	])
	, animatedImage = new Set([
		'image/gif',
		'image/webp',
		'image/apng',
	])
	, video = new Set([
		'video/mpeg',
		'video/quicktime',
		'video/mp4',
		'video/webm',
		'video/x-matroska',
		'video/ogg',
	])
	, audio = new Set([
		'audio/flac',
		'audio/mp3',
		'audio/mpeg',
		'audio/ogg',
		'audio/wave',
		'audio/wav',
	]);

let other;

const updateOtherMimes = () => {
	other = new Set(config.get.otherMimeTypes);
};

updateOtherMimes();
addCallback('config', updateOtherMimes);

module.exports = {

	allowed: (mimetype, options) => {
		return (options.image && image.has(mimetype)) ||
			(options.animatedImage && animatedImage.has(mimetype)) ||
			(options.video && video.has(mimetype)) ||
			(options.audio && audio.has(mimetype)) ||
			(options.other && other.has(mimetype));
	},

	realMimeCheck: async (file) => {
		const supposedMimeType = file.mimetype;
		const realMimeType = await FileType.fromFile(file.tempFilePath);
		if (realMimeType) {
			//note the correct file extension in case it is incorrect/missing
			file.extension = `.${realMimeType.ext}`;
			file.realMimetype = realMimeType.mime;
			return supposedMimeType === realMimeType.mime;
		}
		return config.get.allowMimeNoMatch;
	},

	getOther: () => other,

	image, animatedImage, video, audio

};


code:
'use strict';

const uploadDirectory = require(__dirname+'/uploaddirectory.js');

module.exports = (file, filename, folder) => {

	return new Promise((resolve, reject) => {
		file.mv(`${uploadDirectory}/${folder}/${filename}`, function (err) {
			if (err) {
				return reject(err);
			}
			return resolve();
		});
	});

};


code:
'use strict';

const path = require('path')
	, directory = path.join(__dirname+'/../../static');

module.exports = directory;


code:
'use strict';

const ffmpeg = require('fluent-ffmpeg')
	, config = require(__dirname+'/../../misc/config.js')
	, uploadDirectory = require(__dirname+'/../uploaddirectory.js');

module.exports = (file, geometry, timestamp) => {

	const { thumbSize } = config.get;

	let inputArgs = [
		timestamp === 0 ? '-t 0' : `-ss ${timestamp}`
	];
	let outputArgs = [
		`-vf scale=${geometry.width > geometry.height ? thumbSize + ':-2' : '-2:' + thumbSize}`,
		'-frames:v 1'
	];

	// workaround: FFmpeg native WebM decoder doesn't handle alpha.
	if (file.codec === 'vp8') { inputArgs.push('-c:v libvpx'); }
	if (file.codec === 'vp9') { inputArgs.push('-c:v libvpx-vp9'); }

	return new Promise((resolve, reject) => {
		const command = ffmpeg(`${uploadDirectory}/file/${file.filename}`)
			.on('end', () => {
				return resolve();
			})
			.on('error', function(err) {
				return reject(err);
			});
		command
			.inputOptions(inputArgs)
			.outputOptions(outputArgs)
			.output(`${uploadDirectory}/file/thumb/${file.hash}${file.thumbextension}`)
			.run();
	});
};


code:
'use strict';

const { Permissions } = require(__dirname+'/../permission/permissions.js')
	, actions = [
		{name:'unlink_file', global:true, passwords:true, build:true},
		{name:'delete_file', global:true, auth:Permissions.MANAGE_GLOBAL_GENERAL, passwords:false, build:true},
		{name:'spoiler', global:true, passwords:true, build:true},
		{name:'edit', global:true, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:true},
		{name:'delete', global:true, passwords:true, build:true},
		{name:'lock', global:false, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:true},
		{name:'sticky', global:false, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:true},
		{name:'cyclic', global:false, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:true},
		{name:'bumplock', global:false, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:true},
		{name:'report', global:false, passwords:false, build:false},
		{name:'global_report', global:true, passwords:false, build:false},
		{name:'move', global:false, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:true},
		{name:'delete_ip_board', global:true, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:true},
		{name:'delete_ip_thread', global:true, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:true},
		{name:'delete_ip_global', global:true, auth:Permissions.MANAGE_GLOBAL_GENERAL, passwords:false, build:true},
		{name:'dismiss', global:false, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:false},
		{name:'global_dismiss', global:true, auth:Permissions.MANAGE_GLOBAL_GENERAL, passwords:false, build:false},
		{name:'report_ban', global:false, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:false},
		{name:'global_report_ban', global:true, auth:Permissions.MANAGE_GLOBAL_GENERAL, passwords:false, build:false},
		{name:'ban', global:false, auth:Permissions.MANAGE_BOARD_GENERAL, passwords:false, build:false},
		{name:'global_ban', global:true, auth:Permissions.MANAGE_GLOBAL_GENERAL, passwords:false, build:false},
	];

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

	let numGlobal = 0
		, numPasswords = 0
		, numBuild = 0
		, hasPermission = true
		, validActions = [];

	for (let i = 0; i < actions.length; i++) {
		const action = actions[i];
		const bodyAction = req.body[action.name];
		if (bodyAction != null) {
			validActions.push(action.name);
			if (action.global) {
				numGlobal++;
			}
			if (action.auth && !res.locals.permissions.get(action.auth)) {
				hasPermission = false;
				//maybe break;?
			}
			if (action.passwords) {
				numPasswords++;
			}
			if (action.build) {
				numBuild++;
			}
		}
	}

	return { numGlobal, hasPermission, validActions, numPasswords, numBuild };

};


code:
'use strict';

const { isIP } = require('net')
	, { Permissions } = require(__dirname+'/../permission/permissions.js');

module.exports = (query, permissions) => {
	if (query && query.ip && typeof query.ip === 'string') {
		const decoded = decodeURIComponent(query.ip);
		//if is IP but no permission, return null
		if (isIP(decoded) && !permissions.get(Permissions.VIEW_RAW_IP)) {
			return null;
		}
		return decoded; //otherwise return ip/cloak query
	}
	return null; //else, no ip filter
};


code:
const decodeQueryIp = require('./decodequeryip.js');
const Permission = require('../permission/permission.js');
const ROOT = new Permission();
ROOT.setAll(Permission.allPermissions);
const NO_PERMISSION = new Permission();

describe('decode query ip', () => {

	const cases = [
		{ in: { query: null, permission: ROOT }, out: null },
		{ in: { query: {}, permission: ROOT }, out: null },
		{ in: { query: { ip: '10.0.0.1' }, permission: ROOT }, out: '10.0.0.1' },
		{ in: { query: { ip: '10.0.0.1' }, permission: NO_PERMISSION }, out: null },
		{ in: { query: { ip: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' }, permission: ROOT }, out: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' },
		{ in: { query: { ip: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' }, permission: NO_PERMISSION }, out: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' },
	];

	for (let i in cases) {
		test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => {
			expect(decodeQueryIp(cases[i].in.query, cases[i].in.permission)).toStrictEqual(cases[i].out);
		});
	}

});


code:
'use strict';

module.exports = (string) => {
	return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};


code:
const escapeRegExp = require('./escaperegexp.js');

describe('escape regular expression', () => {

	const cases = [
		{ in: '', out: '' },
		{ in: '/', out: '/' },
		{ in: '.*+?^${}()|[]\\', out: '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\' },
	];

	for (let i in cases) {
		test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => {
			expect(escapeRegExp(cases[i].in)).toStrictEqual(cases[i].out);
		});
	}

});


code:
'use strict';

module.exports = Object.seal(Object.freeze(Object.preventExtensions({

	BAN: 'Ban',
	GLOBAL_BAN: 'Global ban',

	BAN_REPORTER: 'Ban reporter',
	GLOBAL_BAN_REPORTER: 'Global ban reporter',

	DISMISS: 'Dismiss reports',
	GLOBAL_DISMISS: 'Dismiss global reports',

	DELETE: 'Delete',
	DELETE_BY_IP: 'Delete by IP',
	GLOBAL_DELETE_BY_IP: 'Global delete by IP',

	UNLINK_FILES: 'Unlink files',
	DELETE_FILES: 'Delete files',
	SPOILER_FILES: 'Spoiler files',

	EDIT: 'Edit',
	MOVE: 'Move',

	BUMPLOCK: 'Bumplock',
	LOCK: 'Lock',
	STICKY: 'Sticky',
	CYCLE: 'Cycle',

	EDIT_BAN: 'Edit Ban',
	SETTINGS: 'Settings',
	CREATE_BOARD: 'Create Board',
	DELETE_BOARD: 'Delete Board',

})));


code:
'use strict';

module.exports = (query, limit) => {
	query = query || {};
	const nopage = { ...query };
	delete nopage.page;
	const queryString = new URLSearchParams(nopage).toString();
	let page;
	if (query.page && Number.isSafeInteger(parseInt(query.page, 10))) {
		page = parseInt(query.page, 10);
		if (page <= 0) {
			page = 1;
		}
	} else {
		page = 1;
	}
	const offset = (page-1) * limit;
	return { queryString, page, offset };
};


code:
const pageQueryConverter = require('./pagequeryconverter.js');
const limit = 30;

describe('page query converter', () => {
	const cases = [
		{ in: null, out: { offset: 0, 'queryString': '', page: 1 } },
		{ in: { }, out: { offset: 0, 'queryString': '', page: 1 } },
		{ in: { page: [1, 2, 3] }, out: { offset: 0, 'queryString': '', page: 1 } },
		{ in: { page: 'test' }, out: { offset: 0, 'queryString': '', page: 1 } },
		{ in: { page: null }, out: { offset: 0, 'queryString': '', page: 1 } },
		{ in: { page: -1 }, out: { offset: 0, 'queryString': '', page: 1 } },
		{ in: { page: 0 }, out: { offset: 0, 'queryString': '', page: 1 } },
		{ in: { page: 1 }, out: { offset: 0, 'queryString': '', page: 1 } },
		{ in: { page: 5 }, out: { offset: limit*4, 'queryString': '', page: 5 } },
		{ in: { page: 10, other: 'test' }, out: { offset: limit*9, 'queryString': 'other=test', page: 10 } },
		{ in: { other: 'test' }, out: { offset: 0, 'queryString': 'other=test', page: 1 } },
	];
	for (let i in cases) {
		test(`should contain ${cases[i].out} for an input of ${cases[i].in}`, () => {
			expect(pageQueryConverter(cases[i].in, limit)).toStrictEqual(cases[i].out);
		});
	}
});


code:
'use strict';

module.exports = {

	alphaNumericRegex: /^[a-zA-Z0-9]+$/,

	//just whether it exists, for stuff like checkboxes where the value doesnt matter
	existsBody: (data) => {
		return data != null;
	},

	//check length of input, for strings or multi-select options
	lengthBody: (data, minlength=0, maxlength=Infinity) => {
		if (!data) {
			return minlength > 0;
		} else {
			return (data.length < minlength || data.length > maxlength);
		}
	},

	//checks if data is a number and within a range
	numberBody: (data, min=0, max=Infinity) => {
		return typeof data === 'number' && (min <= data && max >= data);
	},

	//same, but with old/new fallbacks for settings that can adjust a dependency at same time
	numberBodyVariable: (data, minOld, minNew, maxOld, maxNew) => {
		if (minNew == null) {
			minNew = minOld;
		}
		if (maxNew == null) {
			maxNew = maxOld;
		}
		let varMin = minNew;
		if (isNaN(varMin)) {
			varMin = minOld;
		}
		let varMax = maxNew;
		if (isNaN(varMax)) {
			varMax = maxOld;
		}
		return typeof data === 'number' && (varMin <= data && varMax >= data);
	},

	//check 2 number values, that one is less than the other, usually for setings with a min and max that they dont violate eachother
	minmaxBody: (minData, maxData) => {
		return typeof minData === 'number' && typeof maxData === 'number' && minData < maxData;
	},

	//check if value is included in a set or array, usually for blacklist or whitelist
	inArrayBody: (data, list) => {
		return data !== null && list[list.constructor.name === 'Array' ? 'includes' : 'has'](data);
	},

	//the opposite kinda, check if the data includes any of the values in the array
	arrayInBody: (filters, data) => {
		return data !== null && filters.some(filter => data.includes(filter));
	},

	//check the actual schema
	checkSchema: async (schema, permissions) => {
		const errors = [];
		//filter to checks with no permission or ones we dont have permission to skip.
		let filteredSchema = schema;
		if (permissions) {
			filteredSchema = filteredSchema.filter(c => c.permission == null || !permissions.get(c.permission));
		}
		for (let check of filteredSchema) {
			const result = await (typeof check.result === 'function' ? check.result() : check.result);
			const expected = (check.expected || false);
			if (result !== expected) {
				errors.push(check.error);
				if (check.blocking === true) {
					break; //errors that you want to stop and not bother checking the rest
				}
			}
		}
		return errors;
	},

};


code:
const { existsBody, lengthBody, numberBody, numberBodyVariable,
	minmaxBody, inArrayBody, arrayInBody, checkSchema } = require('./schema.js');

describe('schema checking (input handling after paramconverter)', () => {

	const existsBodyCases = [
		{ in: { body: { test: 1 }, expected: true }, out: 0 },
		{ in: { body: { test: null }, expected: false }, out: 0 },
		{ in: { body: {}, expected: false }, out: 0 },
		{ in: { body: { test: 1 }, expected: false }, out: 1 },
		{ in: { body: { test: '' }, expected: true }, out: 0 },
	];
	for (let i in existsBodyCases) {
		test(`existsBody should output ${existsBodyCases[i].out} for an input of ${existsBodyCases[i].in.body.test}`, async () => {
			const result = await checkSchema([
				{ result: existsBody(existsBodyCases[i].in.body.test), expected: existsBodyCases[i].in.expected, error: 'error' },
			]);
			expect(result.length).toBe(existsBodyCases[i].out);
		});
	}

	const lengthBodyCases = [
		{ in: { body: { test: null }, expected: false }, out: 0 },
		{ in: { body: { test: '' }, expected: false }, out: 0 },
		{ in: { body: { test: null }, min: 1, expected: false }, out: 1 },
		{ in: { body: { test: '' }, min: 1, expected: false }, out: 1 },
		{ in: { body: { test: 'hello' }, expected: false }, out: 0 },
		{ in: { body: { test: 'hello' }, min: 0, max: 10, expected: false }, out: 0 },
		{ in: { body: { test: 'hello' }, min: 10, expected: false }, out: 1 },
		{ in: { body: { test: 'hellohellohello' }, min: 10, expected: false }, out: 0 },
		{ in: { body: { test: 'hellohellohello' }, min: 0, max: 10, expected: false }, out: 1 },
		{ in: { body: { test: 'hellohellohello' }, max: 10, expected: false }, out: 1 },
	];
	for (let i in lengthBodyCases) {
		test(`lengthBody should output ${lengthBodyCases[i].out} for an input of ${lengthBodyCases[i].in.body.test}`, async () => {
			const result = await checkSchema([
				{ result: lengthBody(lengthBodyCases[i].in.body.test, lengthBodyCases[i].in.min, lengthBodyCases[i].in.max), expected: lengthBodyCases[i].in.expected, error: 'error' },
			]);
			expect(result.length).toBe(lengthBodyCases[i].out);
		});
	}

	const numberBodyCases = [
		{ in: { body: { test: null }, expected: false }, out: 0 },
		{ in: { body: { test: 1 }, expected: true }, out: 0 },
		{ in: { body: { test: 10 }, max: 10, expected: true }, out: 0 },
		{ in: { body: { test: 11 }, max: 10, expected: true }, out: 1 },
		{ in: { body: { test: 9 }, min: 10, expected: true }, out: 1 },
		{ in: { body: { test: 10 }, min: 10, expected: true }, out: 0 },
	];
	for (let i in numberBodyCases) {
		test(`numberBody should output ${numberBodyCases[i].out} for an input of ${numberBodyCases[i].in.body.test}`, async () => {
			const result = await checkSchema([
				{ result: numberBody(numberBodyCases[i].in.body.test, numberBodyCases[i].in.min, numberBodyCases[i].in.max), expected: numberBodyCases[i].in.expected, error: 'error' },
			]);
			expect(result.length).toBe(numberBodyCases[i].out);
		});
	}

	const numberBodyVariableCases = [
		{ in: { body: { test: 5 }, minOld: 0, minNew: 0, maxOld: 10, maxNew: 5, expected: true }, out: 0 },
		{ in: { body: { test: 5 }, minOld: 0, minNew: 6, maxOld: 10, maxNew: 10, expected: true }, out: 1 },
	];
	for (let i in numberBodyVariableCases) {
		test(`numberBodyVariable should output ${numberBodyVariableCases[i].out} for an input of ${numberBodyVariableCases[i].in.body.test}`, async () => {
			const { body, minOld, minNew, maxOld, maxNew } = numberBodyVariableCases[i].in;
			const result = await checkSchema([
				{ result: numberBodyVariable(body.test, minOld, minNew, maxOld, maxNew), expected: numberBodyVariableCases[i].in.expected, error: 'error' },
			]);
			expect(result.length).toBe(numberBodyVariableCases[i].out);
		});
	}

	const minmaxBodyCases = [
		{ in: { a: 0, b: 100, expected: true }, out: 0 },
		{ in: { a: 101, b: 100, expected: true }, out: 1 },
	];
	for (let i in minmaxBodyCases) {
		test(`minmaxBody should output ${minmaxBodyCases[i].out} for an input of ${minmaxBodyCases[i].in}`, async () => {
			const { a, b, expected } = minmaxBodyCases[i].in;
			const result = await checkSchema([
				{ result: minmaxBody(a, b), expected, error: 'error' },
			]);
			expect(result.length).toBe(minmaxBodyCases[i].out);
		});
	}

	const inArrayBodyCases = [
		{ in: { a: null, b: ['a', 'b', 'c'], expected: false }, out: 0 },
		{ in: { a: 'x', b: [], expected: false }, out: 0 },
		{ in: { a: 'x', b: ['a', 'b', 'c'], expected: false }, out: 0 },
		{ in: { a: 'a', b: ['a', 'b', 'c'], expected: true }, out: 0 },
		{ in: { a: null, b: new Set(['a', 'b', 'c']), expected: false }, out: 0 },
		{ in: { a: 'x', b: new Set(), expected: false }, out: 0 },
		{ in: { a: 'x', b: new Set(['a', 'b', 'c']), expected: false }, out: 0 },
		{ in: { a: 'a', b: new Set(['a', 'b', 'c']), expected: true }, out: 0 },
	];
	for (let i in inArrayBodyCases) {
		test(`inArrayBody should output ${inArrayBodyCases[i].out} for an input of ${inArrayBodyCases[i].in}`, async () => {
			const { a, b, expected } = inArrayBodyCases[i].in;
			const result = await checkSchema([
				{ result: inArrayBody(a, b), expected, error: 'error' },
			]);
			expect(result.length).toBe(inArrayBodyCases[i].out);
		});
	}

	const arrayInBodyCases = [
		{ in: { a: null, b: ['a', 'b', 'c'], expected: false }, out: 0 },
		{ in: { a: 'x', b: [], expected: false }, out: 0 },
		{ in: { a: 'x', b: ['a', 'b', 'c'], expected: false }, out: 0 },
		{ in: { a: 'a', b: ['a', 'b', 'c'], expected: true }, out: 0 },
	];
	for (let i in arrayInBodyCases) {
		test(`arrayInBody should output ${arrayInBodyCases[i].out} for an input of ${arrayInBodyCases[i].in}`, async () => {
			const { a, b, expected } = arrayInBodyCases[i].in;
			const result = await checkSchema([
				{ result: arrayInBody(b, a), expected, error: 'error' },
			]);
			expect(result.length).toBe(arrayInBodyCases[i].out);
		});
	}

});


code:
'use strict';

module.exports = {

	trimSetting: (setting, oldSetting) => {
		return typeof setting === 'string' ? setting.trim() : oldSetting;
	},

	numberSetting: (setting, oldSetting) => {
		return typeof setting === 'number' && setting !== oldSetting ? setting : oldSetting;
	},

	booleanSetting: (setting) => {
		return setting != null;
	},

	arraySetting: (setting, oldSetting, limit=false) => {
		if (typeof setting === 'string') {
			const split = setting
				.split(/\r?\n/)
				.filter(n => n);
			return split
				.slice(0, limit || split.length);
		}
		return oldSetting;
	},

};


code:
const { trimSetting, numberSetting, booleanSetting, arraySetting } = require('./setting.js');

describe('trimSetting, numberSetting, booleanSetting, arraySetting', () => {

	const trimCases = [
		{ in: '', out: '' },
		{ in: '   lol   ', out: 'lol' },
		{ in: 1, out: 'OLDSETTING' },
		{ in: null, out: 'OLDSETTING' },
	];
	for (let i in trimCases) {
		test(`trimSetting should output ${trimCases[i].out} for an input of ${trimCases[i].in}`, () => {
			expect(trimSetting(trimCases[i].in, 'OLDSETTING')).toStrictEqual(trimCases[i].out);
		});
	}

	const numberCases = [
		{ in: 3, out: 3 },
		{ in: undefined, out: 'OLDSETTING' },
		{ in: null, out: 'OLDSETTING' },
		{ in: [], out: 'OLDSETTING' },
		{ in: '', out: 'OLDSETTING' },
		{ in: 'string', out: 'OLDSETTING' },
	];
	for (let i in numberCases) {
		test(`numberSetting should output ${numberCases[i].out} for an input of ${numberCases[i].in}`, () => {
			expect(numberSetting(numberCases[i].in, 'OLDSETTING')).toStrictEqual(numberCases[i].out);
		});
	}

	const booleanCases = [
		{ in: null, out: false },
		{ in: undefined, out: false },
		{ in: '', out: true },
		{ in: 'test', out: true },
		{ in: 1, out: true },
		{ in: [], out: true },
		{ in: [1], out: true },
	];
	for (let i in booleanCases) {
		test(`booleanSetting should output ${booleanCases[i].out} for an input of ${booleanCases[i].in}`, () => {
			expect(booleanSetting(booleanCases[i].in)).toStrictEqual(booleanCases[i].out);
		});
	}

	const arrayCases = [
		{ in: undefined, out: 'OLDSETTING' },
		{ in: null, out: 'OLDSETTING' },
		{ in: '', out: [] },
		{ in: 'test', out: ['test'] },
		{ in: 1, out: 'OLDSETTING' },
		{ in: [], out: 'OLDSETTING' },
		{ in: '1', out: ['1'] },
		{ in: `1
2
3`, out: ['1', '2', '3'] },
		{ in: `  hello

123

xxx`, out: ['  hello  ', '123', 'xxx'] },
	];
	for (let i in arrayCases) {
		test(`arraySetting should output ${arrayCases[i].out} for an input of ${arrayCases[i].in}`, () => {
			expect(arraySetting(arrayCases[i].in, 'OLDSETTING', 10)).toStrictEqual(arrayCases[i].out);
		});
	}

});


code:
'use strict';

const { isDeepStrictEqual } = require('util');

function getDotProp(obj, prop) {
	return prop
		.split('.')
		.reduce((a, b) => {
			if (a && a[b]) {
				return a[b];
			}
			return null;
		}, obj);
}

function includeChildren(template, prop, tasks) {
	return Object.keys(getDotProp(template, prop) || {})
		.reduce((a, x) => {
			a[`${prop}.${x}`] = tasks;
			return a;
		}, {});
}

function compareSettings(entries, oldObject, newObject, maxSetSize) {
	const resultSet = new Set();
	entries.every(entry => {
		const oldValue = getDotProp(oldObject, entry[0]);
		const newValue = getDotProp(newObject, entry[0]);
		if (!isDeepStrictEqual(oldValue, newValue)) {
			entry[1].forEach(t => resultSet.add(t));
		}
		return resultSet.size <= maxSetSize;
	});
	return resultSet;
}

module.exports = {
	getDotProp,
	includeChildren,
	compareSettings,
};


code:
const { getDotProp, includeChildren, compareSettings } = require('./settingsdiff.js');

describe('getDotProp, includeChildren, compareSettings in settingsdiff', () => {

	const getDotPropCases = [
		{ in: { object: {a:{b:{c:1}}}, prop: 'a.b.c' }, out: 1 },
		{ in: { object: {a:null}, prop: 'a.b.c' }, out: null },
		{ in: { object: {}, prop: 'a.b.c' }, out: null },
	];
	for (let i in getDotPropCases) {
		test(`getDotProp should output ${getDotPropCases[i].out} for an input of ${getDotPropCases[i].in}`, () => {
			expect(getDotProp(getDotPropCases[i].in.object, getDotPropCases[i].in.prop)).toStrictEqual(getDotPropCases[i].out);
		});
	}

	const includeChildrenCases = [
		{ in: { object: {a:{b:1,c:2,d:3},a2:{b:'notme'}}, prop: 'a' }, out: {'a.b':['example'], 'a.c':['example'], 'a.d':['example']} },
		{ in: { object: null, prop: 'a' }, out: {} },
		{ in: { object: {a:null}, prop: 'a' }, out: {} },
	];
	for (let i in includeChildrenCases) {
		test(`includeChildren should output ${includeChildrenCases[i].out} for an input of ${includeChildrenCases[i].in}`, () => {
			expect(includeChildren(includeChildrenCases[i].in.object, includeChildrenCases[i].in.prop, ['example'])).toStrictEqual(includeChildrenCases[i].out);
		});
	}

	const compareSettingsCases = [
		{
			in: {
				entries: [['a.b.c',['1']]],
				old: {a:{b:{c:1,d:2}}},
				new: {a:{b:{c:1,d:2}}},
			},
			out: new Set(),
		},
		{
			in: {
				entries: [['a.b.c',['1']]],
				old: {a:{b:{c:1,d:2}}},
				new: {a:{b:{c:1,d:3}}},
			},
			out: new Set(),
		},
		{
			in: {
				entries: [['a.b.c',['1']]],
				old: {a:{b:{c:1,d:2}}},
				new: {a:{b:{c:2,d:2}}},
			},
			out: new Set(['1']),
		},
	];
	for (let i in compareSettingsCases) {
		test(`compareSettings should output ${compareSettingsCases[i].out} for an input of ${compareSettingsCases[i].in}`, () => {
			expect(compareSettings(compareSettingsCases[i].in.entries, compareSettingsCases[i].in.old, compareSettingsCases[i].in.new, 4)).toStrictEqual(compareSettingsCases[i].out);
		});
	}

});


code:
'use strict';

const i18n = require('i18n')
	, path = require('path')
	, { debugLogs } = require(__dirname+'/../../configs/secrets.js');

i18n.configure({
	directory: path.join(__dirname, '/../../locales'),
	defaultLocale: 'en-GB',
	retryInDefaultLocale: false,
	updateFiles: false, //holy FUCK why is that an option
	cookie: null,
	header: null,
	queryParameter: null,
});

debugLogs && console.log('Locales loaded:', i18n.getLocales());

module.exports = i18n;


code:
'use strict';

const { Bypass } = require(__dirname+'/../../../db/')
	, { ObjectId } = require(__dirname+'/../../../db/db.js')
	, config = require(__dirname+'/../../misc/config.js')
	, deleteTempFiles = require(__dirname+'/../../file/deletetempfiles.js')
	, dynamicResponse = require(__dirname+'/../../misc/dynamic.js')
	, production = process.env.NODE_ENV === 'production'
	, { Permissions } = require(__dirname+'/../../permission/permissions.js');

module.exports = {

	check: async (req, res, next) => {

		const { __, locale } = res.locals;
		const { secureCookies, blockBypass } = config.get;

		//bypass captcha permission
		if (res.locals.permissions &&
			res.locals.permissions.get(Permissions.BYPASS_CAPTCHA)) {
			res.locals.solvedCaptcha = true;
		}

		//check if blockbypass exists and right length
		const bypassId = req.signedCookies.bypassid;
		if (!res.locals.solvedCaptcha && (!bypassId || bypassId.length !== 24)) {
			deleteTempFiles(req).catch(console.error);
			return dynamicResponse(req, res, 403, 'message', {
				'title': __('Forbidden'),
				'message': __('Please complete a block bypass to continue'),
				'frame': `/bypass_minimal.html?language=${encodeURIComponent(locale)}`,
				'link': {
					'href': '/bypass.html',
					'text': __('Get block bypass'),
				},
			});
		}

		//try to get bypass from db
		let bypass;
		if (bypassId && bypassId.length === 24) {
			try {
				const bypassMongoId = ObjectId(bypassId);
				bypass = await Bypass.checkBypass(bypassMongoId, res.locals.anonymizer);
			} catch (err) {
				return next(err);
			}
		}

		//next if they have a valid bypass
		if (bypass != null) {
			res.locals.blockBypass = true;
			return next();
		}

		if (res.locals.solvedCaptcha) {
			//they dont have a valid bypass, but just solved board captcha, so give them a new one
			const newBypass = await Bypass.getBypass(res.locals.anonymizer, res.locals.pseudoIp, blockBypass.expireAfterUses);
			const newBypassId = newBypass.upsertedId || newBypass.insertedId || res.locals.pseudoIp;
			res.locals.blockBypass = true;
			res.cookie('bypassid', newBypassId.toString(), {
				'maxAge': blockBypass.expireAfterTime,
				'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
				'sameSite': 'strict',
				'signed': true
			});
			return next();
		}

		deleteTempFiles(req).catch(console.error);
		res.clearCookie('bypassid');
		return dynamicResponse(req, res, 403, 'message', {
			'title': __('Forbidden'),
			'message': __('Block bypass expired or exceeded max uses'),
			'frame': `/bypass_minimal.html?language=${encodeURIComponent(locale)}`,
			'link': {
				'href': '/bypass.html',
				'text': __('Get block bypass'),
			},
		});

	},

	middleware: async (req, res, next) => {
		const { blockBypass, secureCookies } = config.get;
		//skip bypass check if not enabled, and not forced for anonymizer or we arent an anonymizer
		if (!blockBypass.enabled &&
			(!blockBypass.forceAnonymizers || !res.locals.anonymizer)) {
			if (res.locals.anonymizer) {
				//dummy for anonymizers, wont work once bypasses are enabled. just allows them to keep same ip/userId for session
				res.cookie('bypassid', res.locals.pseudoIp, {
					'maxAge': blockBypass.expireAfterTime,
					'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
					'sameSite': 'strict',
					'signed': true
				});
			}
			return next();
		}
		return module.exports.check(req, res, next);
	},

};


code:
'use strict';

const { Ratelimits } = require(__dirname+'/../../../db/')
	, checkCaptcha = require(__dirname+'/../../captcha/captcha.js')
	, config = require(__dirname+'/../../misc/config.js')
	, remove = require('fs-extra').remove
	, dynamicResponse = require(__dirname+'/../../misc/dynamic.js')
	, deleteTempFiles = require(__dirname+'/../../file/deletetempfiles.js')
	, uploadDirectory = require(__dirname+'/../../file/uploaddirectory.js')
	, { Permissions } = require(__dirname+'/../../permission/permissions.js');

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

	//bypass captcha permission
	if (res.locals.permissions &&
		res.locals.permissions.get(Permissions.BYPASS_CAPTCHA)) {
		res.locals.solvedCaptcha = true;
		return next();
	}

	//skip captcha if disabled on board for posts only
	if (res.locals.board
		&& req.path === `/board/${res.locals.board._id}/post`) {
		if (res.locals.board.settings.captchaMode === 0 //if captcha disabled
			|| (res.locals.board.settings.captchaMode === 1 && req.body.thread)) { //or if enabled for threads, and not a thread
			return next(); //then skip checking captcha
		}
	}

	const captchaInput = req.body.captcha || req.body['g-recaptcha-response'] || req.body['h-captcha-response'];
	const captchaId = req.cookies.captchaid;
	try {
		await checkCaptcha(req, captchaInput, captchaId);
	} catch (err) {
		deleteTempFiles(req).catch(console.error);
		if (err instanceof Error) {
			return next(err);
		}
		const page = (req.body.minimal || req.path === '/blockbypass' ? 'bypass' : 'message');
		const { __ } = res.locals;
		return dynamicResponse(req, res, 403, page, {
			'title': __('Forbidden'),
			'message': __(err),
			'redirect': req.headers.referer,
		});
	}

	//it was correct, so mark as solved for other middleware
	res.locals.solvedCaptcha = true;

	const { captchaOptions } = config.get;
	if (!['google', 'hcaptcha', 'yandex']
		.includes(captchaOptions.type)) {
		//for builtin captchas, clear captchaid cookie, delete file and reset quota
		res.clearCookie('captchaid');
		await Promise.all([
			!res.locals.anonymizer && Ratelimits.resetQuota(res.locals.ip.cloak, 'captcha'),
			remove(`${uploadDirectory}/captcha/${captchaId}.jpg`)
		]);
	}

	//completed captcha successfully, continue
	return next();

};


code:
'use strict';

const { debugLogs } = require(__dirname+'/../../../configs/secrets.js')
	, dynamicResponse = require(__dirname+'/../../misc/dynamic.js')
	, { addCallback } = require(__dirname+'/../../redis/redis.js')
	, upload = require('@fatchan/express-fileupload')
	, fileHandlers = {}
	, fileSizeLimitFunction = (req, res) => {
		const { __ } = res.locals;
		return dynamicResponse(req, res, 413, 'message', {
			'title': __('Payload Too Large'),
			'message': __('Your upload was too large'),
			'redirect': req.headers.referer
		});
	}
	, missingExtensionLimitFunction = (req, res) => {
		const { __ } = res.locals;
		return dynamicResponse(req, res, 400, 'message', {
			'title': __('Bad Request'),
			'message': __('Missing file extensions'),
			'redirect': req.headers.referer
		});
	}
	, updateHandlers = () => {
		const { globalLimits, filterFileNames, spaceFileNameReplacement, uriDecodeFileNames } = require(__dirname+'/../../misc/config.js').get;
		['flag', 'banner', 'asset', 'post'].forEach(fileType => {
			const fileSizeLimit = globalLimits[`${fileType}FilesSize`];
			const fileNumLimit = globalLimits[`${fileType}Files`];
			const fileNumLimitFunction = (req, res) => {
				const { __ } = res.locals;
				const isPostform = req.path.endsWith('/post') || req.path.endsWith('/modpost');
				const message = (isPostform && res.locals.board)
					? __(`Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is %s`, res.locals.board.settings.maxFiles)
					: __('Max files per request is %s', fileNumLimit.max);
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Too many files'),
					'message': message,
					'redirect': req.headers.referer
				});
			};
			fileHandlers[fileType] = upload({
				defParamCharset: 'utf8',
				debug: debugLogs,
				createParentPath: true,
				safeFileNames: filterFileNames,
				spaceFileNameReplacement,
				preserveExtension: 4,
				uriDecodeFileNames,
				limits: {
					totalSize: fileSizeLimit.max,
					fileSize: fileSizeLimit.max,
					files: fileNumLimit.max,
				},
				limitHandler: fileSizeLimitFunction,
				numFilesLimitHandler: fileNumLimitFunction,
				extensionLimitHandler: missingExtensionLimitFunction,
				useTempFiles: true,
				tempFileDir: __dirname+'/../../../tmp/'
			});
		});
	};

updateHandlers();
addCallback('config', updateHandlers);

module.exports = {

	asset: (req, res, next) => {
		return fileHandlers.asset(req, res, next);
	},
	banner: (req, res, next) => {
		return fileHandlers.banner(req, res, next);
	},
	flag: (req, res, next) => {
		return fileHandlers.flag(req, res, next);
	},
	posts: (req, res, next) => {
		return fileHandlers.post(req, res, next);
	},

};


code:
'use strict';

const imageHash = require('imghash').hash
	, config = require(__dirname+'/../../misc/config.js');

module.exports = async (req, res, next) => {
	const { hashImages } = config.get;
	if (hashImages && res.locals.numFiles > 0) {
		const hashPromises = [];
		for (let i = 0; i < res.locals.numFiles; i++) {
			const mainType = req.files.file[i].mimetype.split('/')[0];
			if (mainType === 'image') {
				hashPromises.push(imageHash(req.files.file[i].tempFilePath, 8, 'hex').then(res => {
					req.files.file[i].phash = res;
				}));
			}
		}
		await Promise.all(hashPromises)
			.catch(() => { /* noop on fail, wont hurt. phashes just wont be set */ });
	}
	next();
};


code:
'use strict';

module.exports = (req, res, next) => {
	res.locals.numFiles = 0;
	if (req.files && req.files.file) {
		if (Array.isArray(req.files.file)) {
			res.locals.numFiles = req.files.file.filter(file => file.size > 0).length;
		} else {
			res.locals.numFiles = req.files.file.size > 0 ? 1 : 0;
			req.files.file = [req.files.file];
		}
	}
	next();
};


code:
'use strict';

const { ObjectId } = require('mongodb')
	, timeFieldRegex = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+mo)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?(?<MINUTE>[\d]+m)?(?<SECOND>[\d]+s)?$/
	, timeUtils = require(__dirname+'/../../converter/timeutils.js')
	, dynamicResponse = require(__dirname+'/../../misc/dynamic.js')
	, makeArrayIfSingle = require('../../converter/makearrayifsingle.js');

const defaultOptions = {
	timeFields: [],
	trimFields: [],
	allowedArrays: [], //lib/captcha already does this for captcha
	numberFields: [],
	numberArrays: [],
	objectIdParams: [],
	objectIdFields: [],
	objectIdArrays: [],
	processThreadIdParam: false,
	processDateParam: false,
	processMessageLength: false,
};

module.exports = (options) => {

	options = { ...defaultOptions, ...options };
	options.allowedArrays.push('captcha'); //this is the only one for now, otherwise i would .concat them all or something.

	return (req, res, next) => {

		const { __ } = res.locals;

		const { timeFields, trimFields, allowedArrays, processThreadIdParam,
			processDateParam, processMessageLength, numberFields, numberArrays,
			objectIdParams, objectIdFields, objectIdArrays } = options;

		if (req.body && req.body.message) {
			req.body.rawMessage = req.body.message;
		}

		/* check all body fields, body-parser prevents this array being too big, so no worry.
		   whitelist for fields that can be arrays, and convert singular of those fields to 1 length array */
		const bodyFields = Object.keys(req.body || {});
		for (let i = 0; i < bodyFields.length; i++) {
			const key = bodyFields[i];
			const val = req.body[key];
			if (!allowedArrays.includes(key) && Array.isArray(val)) {
				return dynamicResponse(req, res, 400, 'message', {
					'title': __('Bad request'),
					'message': __('Malformed input'),
				});
			} else if (allowedArrays.includes(key) && !Array.isArray(val)) {
				req.body[key] = makeArrayIfSingle(req.body[key]); //convert to arrays with single item for simpler case batch handling later
			}
		}

		//process trimFields to remove excess white space
		for (let i = 0; i < trimFields.length; i++) {
			const field = trimFields[i];
			if (req.body[field] != null) {
				//trimEnd() because trailing whitespace doesnt affect how a post appear and if it is all whitespace, trimEnd will get it all anyway
				req.body[field] = req.body[field].trimEnd();
			}
		}

		//convert numberFields into number
		for (let i = 0; i < numberFields.length; i++) {
			const field = numberFields[i];
			if (req.body[field] != null) {
				const num = parseInt(req.body[field], 10);
				if (Number.isSafeInteger(num)) {
					req.body[field] = num;
				} else {
					req.body[field] = null;
				}
			}
		}

		//convert timeFields duration string to time in ms
		for (let i = 0; i < timeFields.length; i++) {
			const field = timeFields[i];
			if (req.body[field] != null) {
				const matches = req.body[field].match(timeFieldRegex);
				if (matches && matches.groups) {
					const groups = matches.groups;
					let duration = 0;
					const groupKeys = Object.keys(groups);
					for (let i = 0; i < groupKeys.length; i++) {
						const key = groupKeys[i];
						if (!groups[key]) {
							continue;
						}
						const mult = +groups[key].replace(/\D+/, ''); //remove the unit
						if (Number.isSafeInteger(mult) //if the multiplier is safe int
							&& Number.isSafeInteger(mult*timeUtils[key]) //and multiplying it is safe int
							&& Number.isSafeInteger((mult*timeUtils[key])+duration)) { //and adding it to the total is safe
							duration += mult*timeUtils[key];
						}
					}
					req.body[field] = duration;
				} else {
					const num = parseInt(req.body[field], 10);
					if (Number.isSafeInteger(num)) {
						req.body[field] = num;
					} else {
						req.body[field] = null;
					}
				}
			}
		}

		//convert/map some fields to ObjectId or Number
		try {
			for (let i = 0; i < objectIdFields.length; i++) {
				const field = objectIdFields[i];
				if (req.body[field] != null) {
					req.body[field] = ObjectId(req.body[field]);
				}
			}
			for (let i = 0; i < objectIdParams.length; i++) {
				const field = objectIdParams[i];
				if (req.params[field] != null) {
					req.params[field] = ObjectId(req.params[field]);
				}
			}
			for (let i = 0; i < objectIdArrays.length; i++) {
				const field = objectIdArrays[i];
				if (req.body[field] != null) {
					req.body[field] = req.body[field]
						.filter(o => o) //objectid(null) doesnt throw, lol
						.map(ObjectId);
				}
			}
			for (let i = 0; i < numberArrays.length; i++) {
				const field = numberArrays[i];
				if (req.body[field] != null) {
					req.body[field] = req.body[field]
						.map(n => parseInt(n, 10))
						.filter(n => !isNaN(n) && Number.isSafeInteger(n));
				}
			}
		} catch (e) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': __('Bad request'),
				'message': __('Malformed input'),
			});
		}

		//thread id
		if (processThreadIdParam && req.params.id != null) {
			req.params.id = parseInt(req.params.id, 10);
			if (isNaN(req.params.id)) {
				req.params.id = null;
			}
		}

		//moglog date
		if (processDateParam && req.params.date != null) {
			if (req.params.date.match(/\d\d-\d\d-\d\d\d\d/)) {
				let [ month, day, year ] = req.params.date.split('-');
				month = month-1;
				const date = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
				if (isNaN(date) || date === 'Invalid Date') {
					res.locals.date = null;
				} else {
					res.locals.date = {
						date,
						month,
						day: date.getDate(),
						year:date.getFullYear(),
					};
				}
			} else {
				res.locals.date = null;
			}
		}

		/* normalise message length check for CRLF vs just LF, because String.length depending on browser wont count CRLF as
		   2 characters, so user gets "message too long" at the right length. Maybe will add another array for these in future */
		if (processMessageLength) {
			res.locals.messageLength = req.body.message ? req.body.message.replace(/\r\n/igm, '\n').length : 0;
		}

		next();

	};

};


code:
const paramConverter = require('./paramconverter.js');
const { ObjectId } = require('mongodb');
const { WEEK, DAY, HOUR } = require('./../../converter/timeutils.js');
const noop = () => {};
/*
const defaultOptions = {
	timeFields: [],
	trimFields: [],
	allowedArrays: [],
	numberFields: [],
	numberArrays: [],
	objectIdFields: [],
	objectIdArrays: [],
	processDateParam: false,
	processMessageLength: false,
};
*/

describe('paramconverter', () => {

	const bodyCases = [
		{
			in: { options: { trimFields: ['username', 'password'] }, body: { username: 'trimmed  ', password: 'trimmed    ' } },
			out: { username: 'trimmed', password: 'trimmed' },
		},
		{
			in: { options: { allowedArrays: ['array'] }, body: { array: 'lol' } },
			out: { array: ['lol'] },
		},
		{
			in: { options: { allowedArrays: ['array'] }, body: { array: ['lol'] } },
			out: { array: ['lol'] },
		},
		{
			in: { options: { allowedArrays: ['array'] }, body: { notarray: ['lol'] } },
			out: 'error',
		},
		{
			in: { options: { numberFields: ['num'] }, body: { num: '1' } },
			out: { num: 1 },
		},
		{
			in: { options: { numberFields: ['num'] }, body: { num: 'xxxx' } },
			out: { num: null },
		},
		{
			in: { options: { allowedArrays: ['num'], numberArrays: ['num'] }, body: { num: '1' } },
			out: { num: [1] },
		},
		{
			in: { options: { allowedArrays: ['num'], numberArrays: ['num'] }, body: { num: ['1', '2', '3'] } },
			out: { num: [1, 2, 3] },
		},
		{
			in: { options: { allowedArrays: ['num'], numberArrays: ['num'] }, body: { notnum: ['1', '2', '3'] } },
			out: 'error',
		},
		{
			in: { options: { allowedArrays: ['num'], numberArrays: ['num'] }, body: { num: ['xxx', '2', '3'] } },
			out: { num: [2, 3] },
		},
		{
			in: { options: { allowedArrays: ['num'], numberArrays: ['num'] }, body: { num: [] } },
			out: { num: [] },
		},
		{
			in: { options: { allowedArrays: ['num'], numberArrays: ['num'] }, body: { num: '' } },
			out: { num: [] },
		},
		{
			in: { options: { allowedArrays: ['num'], numberArrays: ['num'] }, body: { num: null } },
			out: { num: [] },
		},
		{
			in: { options: { timeFields: ['test'] }, body: { test: '1w2d3h' } },
			out: { test: WEEK+(2*DAY)+(3*HOUR) },
		},
		{
			in: { options: { timeFields: ['test'] }, body: { test: '20000' } },
			out: { test: 20000 },
		},
		{
			in: { options: { timeFields: ['test'] }, body: { test: 'xxxx' } },
			out: { test: null },
		},
		{
			in: { options: { objectIdFields: ['test'] }, body: { test: 'aaaaaaaaaaaaaaaaaaaaaaaa' } },
			out: { test: ObjectId('aaaaaaaaaaaaaaaaaaaaaaaa') },
		},
		{
			in: { options: { objectIdFields: ['test'] }, body: { test: 'x' } },
			out: 'error',
		},
		{
			in: { options: { objectIdFields: ['test'] }, body: { test: '0' } },
			out: 'error',
		},
		{
			in: { options: { objectIdFields: ['test'] }, body: { test: null } },
			out: { test: null },
		},
		{
			in: { options: { objectIdFields: ['test'] }, body: { test: '' } },
			out: 'error',
		},
		{
			in: { options: { objectIdFields: ['test'] }, body: { nottest: 'x' } },
			out: { nottest: 'x' },
		},
		{
			in: { options: { allowedArrays: ['test'], objectIdArrays: ['test'] }, body: { test: 'aaaaaaaaaaaaaaaaaaaaaaaa' } },
			out: { test: [ObjectId('aaaaaaaaaaaaaaaaaaaaaaaa')] },
		},
		{
			in: { options: { allowedArrays: ['test'], objectIdArrays: ['test'] }, body: { test: ['aaaaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaaaa'] } },
			out: { test: [ObjectId('aaaaaaaaaaaaaaaaaaaaaaaa'), ObjectId('aaaaaaaaaaaaaaaaaaaaaaaa')] },
		},
		{
			in: { options: { objectIdArrays: ['test'] }, body: { test: 'x' } },
			out: 'error',
		},
		{
			in: { options: { processMessageLength: true }, body: { message: 'asd' } },
			out: { message: 'asd', rawMessage: 'asd' },
		},
		{
			in: { options: { processMessageLength: true }, body: { message: 'asd\r\nasd\nasd' } },
			out: { message: 'asd\r\nasd\nasd', rawMessage: 'asd\r\nasd\nasd' },
		},
	];

	for (let i in bodyCases) {
		test(`${i} should output ${bodyCases[i].out} for an input of ${bodyCases[i].in}`, () => {
			const converter = paramConverter(bodyCases[i].in.options);
			if (bodyCases[i].out === 'error') {
				expect(() => {
					converter({ body: bodyCases[i].in.body }, {locals:{}}, noop);
				}).toThrow();
			} else {
				converter({ body: bodyCases[i].in.body }, {locals:{}}, noop);
				expect(bodyCases[i].in.body).toStrictEqual(bodyCases[i].out);
			}
		});
	}

/*
	objectIdParams: [],
	processThreadIdParam: false,
	processDateParam
*/
	const paramCases = [
		{
			in: { options: { objectIdParams: ['test'] }, params: { test: 'lol' } },
			out: 'error',
		},
		{
			in: { options: { objectIdParams: ['test'] }, params: { test: 'aaaaaaaaaaaaaaaaaaaaaaaa' } },
			out: { test: ObjectId('aaaaaaaaaaaaaaaaaaaaaaaa') },
		},
		{
			in: { options: { processThreadIdParam: true }, params: { id: '123' } },
			out: { id: 123 },
		},
		{
			in: { options: { processThreadIdParam: true }, params: { id: null } },
			out: { id: null },
		},
		{
			in: { options: { processThreadIdParam: true }, params: { id: '' } },
			out: { id: null },
		},
		{
			in: { options: { processThreadIdParam: true }, params: { id: 'x' } },
			out: { id: null },
		},
	];

	for (let i in paramCases) {
		test(`${i} should output ${paramCases[i].out} for an input of ${paramCases[i].in}`, () => {
			const converter = paramConverter(paramCases[i].in.options);
			if (paramCases[i].out === 'error') {
				expect(() => {
					converter({ params: paramCases[i].in.params }, {locals:{}}, noop);
				}).toThrow();
			} else {
				converter({ params: paramCases[i].in.params }, {locals:{}}, noop);
				expect(paramCases[i].in.params).toStrictEqual(paramCases[i].out);
			}
		});
	}

	const dateCases = [
		{
			in: { options: { processDateParam: true }, params: { date: '01-01-2069' } },
			out: { date: { date: new Date(Date.UTC(2069, 0, 1, 0, 0, 0, 0)), month: 0, day: 1, year: 2069 } },
		},
		{
			in: { options: { processDateParam: true }, params: { date: null } },
			out: {},
		},
		{
			in: { options: { processDateParam: true }, params: { date: '' } },
			out: { date: null },
		},
		{
			in: { options: { processDateParam: true }, params: { date: 'somebullshit' } },
			out: { date: null },
		},
	];

	for (let i in dateCases) {
		test(`${i} should output ${dateCases[i].out} for an input of ${dateCases[i].in}`, () => {
			const converter = paramConverter(dateCases[i].in.options);
			const locals = {locals:{}};
			converter({ params: dateCases[i].in.params }, locals, noop);
			expect(locals.locals).toStrictEqual(dateCases[i].out);
		});
	}

});


code:
'use strict';

const cache = require(__dirname+'/../../redis/redis.js')
	, { check: blockBypassCheck } = require(__dirname+'/../captcha/blockbypass.js')
	, dynamicResponse = require(__dirname+'/../../misc/dynamic.js')
	, deleteTempFiles = require(__dirname+'/../../file/deletetempfiles.js')
	, config = require(__dirname+'/../../misc/config.js')
	, { Permissions } = require(__dirname+'/../../permission/permissions.js')
	, { batch } = require('dnsbl');

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

	if (res.locals.permissions.get(Permissions.BYPASS_DNSBL)) {
		return next();
	}

	const { ipHeader, dnsbl, blockBypass } = config.get;

	if (dnsbl.enabled && dnsbl.blacklists.length > 0 //if dnsbl enabled and has more than 0 blacklists
		&& !res.locals.anonymizer) { //anonymizers cant be dnsbl'd
		const ip = req.headers[ipHeader] || req.connection.remoteAddress;
		let isBlacklisted = await cache.get(`blacklisted:${ip}`);
		if (isBlacklisted === null) { //not cached
			const dnsblResp = await batch(ip, dnsbl.blacklists);
			isBlacklisted = dnsblResp.some(r => r.listed === true);
			await cache.set(`blacklisted:${ip}`, isBlacklisted, Math.floor(dnsbl.cacheTime/1000));
		}
		if (isBlacklisted) {
			if (blockBypass.bypassDnsbl) {
				if (!res.locals.blockBypass) {
					return blockBypassCheck(req, res, next);
				}
				return next(); //already solved
			}
			//otherwise dnsbl cant be bypassed
			deleteTempFiles(req).catch(console.error);
			const { __ } = res.locals;
			return dynamicResponse(req, res, 403, 'message', {
				'title': __('Forbidden'),
				'message': __('Your request was blocked because your IP address is listed on a blacklist.'),
				'redirect': req.headers.referer || '/',
			});
		}
	}
	return next();

};


code:
'use strict';

const { isAnonymizer } = require(__dirname+'/../../misc/countries.js')
	, config = require(__dirname+'/../../misc/config.js');

module.exports = (req, res, next) => {
	const { countryCodeHeader } = config.get;
	const code = req.headers[countryCodeHeader] || 'XX';
	res.locals.anonymizer = isAnonymizer(code);
	res.locals.country = {
		code,
	};
	return next();
};


code:
'use strict';

module.exports = {
	IPV4: 0,
	IPV6: 1,
	BYPASS: 2,
	PRUNED: 3,
};


code:
'use strict';

const config = require(__dirname+'/../../misc/config.js')
	, { createCIDR, parse } = require('ip6addr')
	, hashIp = require(__dirname+'/../../misc/haship.js')
	, ipTypes = require(__dirname+'/iptypes.js')
	, { ObjectId } = require(__dirname+'/../../../db/db.js');

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

	//tor user ip uses bypass id (or objectid, which will become bypassid)
	if (res.locals.anonymizer) {
		const pseudoIp = req.signedCookies.bypassid || ObjectId().toString();
		res.locals.pseudoIp = pseudoIp;
		res.locals.ip = {
			raw: `${pseudoIp}.BP`,
			cloak: `${pseudoIp}.BP`,
			type: ipTypes.BYPASS,
		};
		return next();
	}

	//ip for normal user
	const { dontStoreRawIps, ipHeader } = config.get;
	const ip = req.headers[ipHeader] || req.connection.remoteAddress;
	try {
		const ipParsed = parse(ip);
		const ipKind = ipParsed.kind();
		const ipStr = ipParsed.toString({
			format: ipKind === 'ipv4' ? 'v4' : 'v6',
			zeroElide: false,
			zeroPad: false,
		});
		let qrange
			, hrange
			, type;
		if (ipKind === 'ipv4') {
			qrange = createCIDR(ipStr, 24).toString();
			hrange = createCIDR(ipStr, 16).toString();
			type = ipTypes.IPV4;
		} else {
			qrange = createCIDR(ipStr, 64).toString();
			hrange = createCIDR(ipStr, 48).toString();
			type = ipTypes.IPV6;
		}
		const cloak = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP${ipKind.charAt(3)}`;
		res.locals.ip = {
			raw: dontStoreRawIps === true ? cloak : ipStr,
			cloak,
			type,
		};
		next();
	} catch (e)  {
		//should never get here
		console.error('Ip parse failed', e);
		const { __ } = res.locals;
		return res.status(400).render('message', {
			'title': __('Bad request'),
			'message': __('Malformed IP'),
		});
	}

};


code:
'use strict';

const i18n = require(__dirname+'/../../locale/locale.js')
	, config = require(__dirname+'/../../misc/config.js');

module.exports = {

	setGlobalLanguage: (req, res, next) => {
		// global settings locale
		const { language } = config.get;
		res.locals.setLocale(res.locals, language);
		next();
	},

	setBoardLanguage: (req, res, next) => {
		// board settings locale
		const language = res.locals.board.settings.language;
		res.locals.setLocale(res.locals, language);
		next();
	},

	setQueryLanguage: (req, res, next) => {
		if (req.query.language
			&& typeof req.query.language === 'string'
			&& i18n.getLocales().includes(req.query.language)) {
			res.locals.setLocale(res.locals, req.query.language);
		}
		next();
	},

	//TODO set param language? set body language? surely not...

};


code:
'use strict';

const csrf = require('csurf')();

module.exports = csrf;


code:
'use strict';

const config = require(__dirname+'/../../misc/config.js')
	, dynamicResponse = require(__dirname+'/../../misc/dynamic.js')
	, { addCallback } = require(__dirname+'/../../redis/redis.js');

let refererCheck, allowedHosts, allowedHostSet;
const updateReferers = () => {
	({ refererCheck, allowedHosts } = config.get);
	allowedHostSet = new Set(allowedHosts);
};
updateReferers();
addCallback('config', updateReferers);

module.exports = (req, res, next) => {
	if (req.method !== 'POST') {
		return next();
	}
	let validReferer = false;
	try {
		const url = new URL(req.headers.referer);
		validReferer = allowedHostSet.has(url.hostname);
	} catch (e) {
		//referrer is invalid url
	}
	if (refererCheck === true && (!req.headers.referer || !validReferer)) {
		const { __ } = res.locals;
		return dynamicResponse(req, res, 403, 'message', {
			'title': __('Forbidden'),
			'message': __('Invalid or missing "Referer" header. Are you posting from the correct URL?'),
		});
	}
	next();
};


code:
'use strict';

module.exports = (req, res, next) => {
	res.locals.minimal = true;
	next();
};


code:
'use strict';

const Mongo = require(__dirname+'/../../../db/db.js')
	, { Posts } = require(__dirname+'/../../../db/')
	, { Permissions } = require(__dirname+'/../../permission/permissions.js')
	, config = require(__dirname+'/../../misc/config.js');

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

	if (res.locals.permissions.get(Permissions.BYPASS_SPAMCHECK)) {
		return false;
	}

	const { sameContentSameIp, sameContentAnyIp, anyContentSameIp } = config.get.floodTimers;

	if (sameContentSameIp === 0
		&& sameContentAnyIp === 0
		&& anyContentSameIp === 0) {
		return false;
	}

	const now = Date.now();
	const ors = [];
	const contentOr = [];
	if (res.locals.numFiles > 0) {
		contentOr.push({
			'files': {
				'$elemMatch': {
					'hash': { //any file hash will match, doesnt need to be all
						'$in': req.files.file.map(f => f.sha256)
					}
				}
			}
		});
	}
	if (req.body.message) {
		contentOr.push({
			'nomarkup':  req.body.message
		});
	}

	if (sameContentAnyIp) {
		//matching content from any IP
		const sameContentAnyIpMongoId = Mongo.ObjectId.createFromTime(Math.floor((now - sameContentAnyIp)/1000));
		ors.push({
			'_id': {
				'$gt': sameContentAnyIpMongoId
			},
			'$or': contentOr
		});
	}

	if (sameContentSameIp > 0) {
		//matching content from same IP
		const sameContentSameIpMongoId = Mongo.ObjectId.createFromTime(Math.floor((now - sameContentSameIp)/1000));
		ors.push({
			'_id': {
				'$gt': sameContentSameIpMongoId
			},
			'ip.cloak': res.locals.ip.cloak,
			'$or': contentOr
		});
	}

	if (anyContentSameIp > 0) {
		//any posts from same IP
		const anyContentSameIpMongoId = Mongo.ObjectId.createFromTime(Math.floor((now - anyContentSameIp)/1000));
		ors.push({
			'_id': {
				'$gt': anyContentSameIpMongoId
			},
			'ip.cloak': res.locals.ip.cloak
		});
	}

	let flood = await Posts.db.find({
		'$or': ors
	}).toArray();

	return flood.length > 0;

};


code:
'use strict';

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

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

	//bypass all bans, special permission
	if (res.locals.permissions.get(Permissions.BYPASS_BANS)) {
		return next();
	}

	//fetch bans
	const banBoard = res.locals.board ? res.locals.board._id : null; //if no board, global bans or "null" board.
	let bans = await Bans.find(res.locals.ip, banBoard);
	//board staff still bypass bans on their board by default
	if (res.locals.permissions.get(Permissions.MANAGE_BOARD_GENERAL)) {
		//filter bans to leave only global bans remaining
		bans = bans.filter(ban => ban.board !== res.locals.board);
	}
	if (bans && bans.length > 0) {
		const unseenBans = bans.filter(b => !b.seen).map(b => b._id);
		await Bans.markSeen(unseenBans); //mark bans as seen
		bans.forEach(ban => {
			ban.note = null;
			ban.ip.raw = null;
			ban.issuer = ban.showUser === true ? ban.issuer : null;
			ban.seen = true;
		});
		return dynamicResponse(req, res, 403, 'ban', {
			bans,
		});
	}

	next(); //no bans found

};


code:
'use strict';

const calcPerms = require(__dirname+'/../../permission/calcperms.js');

module.exports = (req, res, next) => {
	res.locals.permissions = calcPerms(req, res);
	next();
};


code:
'use strict';

//todo: refactor
const cache = {
	one: {}, all: {}, any: {},
};

module.exports = {

	one: (requiredPermission) => {
		return cache.one[requiredPermission] || (cache.one[requiredPermission] = function(req, res, next) {
			if (!res.locals.permissions.get(requiredPermission)) {
				const { __ } = res.locals;
				return res.status(403).render('message', {
					'title': __('Forbidden'),
					'message': __('No Permission'),
					'redirect': req.headers.referer || '/',
				});
			}
			next();
		});
	},

	all: (...requiredPermissions) => {
		//these caches working as intended with arrays?
		return cache.all[requiredPermissions] || (cache.all[requiredPermissions] = function(req, res, next) {
			if (!res.locals.permissions.hasAll(...requiredPermissions)) {
				const { __ } = res.locals;
				return res.status(403).render('message', {
					'title': __('Forbidden'),
					'message': __('No Permission'),
					'redirect': req.headers.referer || '/',
				});
			}
			next();
		});
	},

	any: (...requiredPermissions) => {
		//these caches working as intended with arrays?
		return cache.any[requiredPermissions] || (cache.any[requiredPermissions] = function(req, res, next) {
			if (!res.locals.permissions.hasAny(...requiredPermissions)) {
				const { __ } = res.locals;
				return res.status(403).render('message', {
					'title': __('Forbidden'),
					'message': __('No Permission'),
					'redirect': req.headers.referer || '/',
				});
			}
			next();
		});
	},

};


code:
'use strict';

module.exports = async (req, res, next) => {
	if (res.locals.user) {
		return next();
	}
	let goto;
	if (req.method === 'GET' && req.path) {
		//coming from a GET page isLoggedIn middleware check
		goto = encodeURIComponent(req.path);
	}
	if (req.params.board && req.params.id) {
		if (req.path.includes('/manage/thread/')) {
			return res.redirect(`/${req.params.board}/thread/${req.params.id}.html`);
		}
	}
	return res.redirect(`/login.html${goto ? '?goto='+goto : ''}`);
};


code:
'use strict';

const { Accounts } = require(__dirname+'/../../../db/')
	, { DAY } = require(__dirname+'/../../converter/timeutils.js')
	, cache = require(__dirname+'/../../redis/redis.js')
	, config = require(__dirname+'/../../misc/config.js');

module.exports = async (req, res, next) => {
	if (!res.locals) {
		res.locals = {};
	}
	if (req.session && req.session.user) {
		res.locals.user = await cache.get(`users:${req.session.user}`);
		if (!res.locals.user) {
			const account = await Accounts.findOne(req.session.user);
			if (!account) {
				req.session.destroy();
			} else {
				await Accounts.updateLastActiveDate(req.session.user);
				res.locals.user = {
					'username': account._id,
					'permissions': account.permissions.toString('base64'),
					'staffBoards': account.staffBoards,
					'ownedBoards': account.ownedBoards,
					/* For security, only storing a boolean used for checks,
						we dont need/want to store the twofactor secret in session */
					'twofactor': account.twofactor != null,
				};
				req.session.expires = new Date(Date.now() + (3 * DAY));
				cache.set(`users:${req.session.user}`, res.locals.user, 3600);
			}
		}
		const { forceAccountTwofactor } = config.get;
		//Note: not /forms/twofactor because req.path doesnt contain the mount point when called from a middleware
		if (!['/twofactor.html', '/twofactor'].includes(req.path)
			&& forceAccountTwofactor === true
			&& res.locals.user
			&& !res.locals.user.twofactor) {
			return res.redirect('/twofactor.html');
		}
	}
	next();
};


code:
'use strict';

const session = require('express-session')
	, uid = require('uid-safe').sync
	, redisStore = require('connect-redis')(session)
	, { cookieSecret } = require(__dirname+'/../../../configs/secrets.js')
	, config = require(__dirname+'/../../misc/config.js')
	, { redisClient } = require(__dirname+'/../../redis/redis.js')
	, production = process.env.NODE_ENV === 'production'
	, { DAY } = require(__dirname+'/../../converter/timeutils.js')
	, sessionMiddlewareCache = {};

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

	const { secureCookies } = config.get;
	const proto = req.headers['x-forwarded-proto'];
	const sessionMiddleware = sessionMiddlewareCache[proto] || (sessionMiddlewareCache[proto] = session({
		secret: cookieSecret,
		store: new redisStore({
			client: redisClient,
		}),
		resave: false,
		saveUninitialized: false,
		rolling: true,
		cookie: {
			httpOnly: true,
			secure: secureCookies && production && (proto === 'https'),
			sameSite: 'strict',
			maxAge: 3 * DAY,
		},
		genid: (req) => {
			//add user identifier to session id
			//https://github.com/expressjs/session/blob/master/index.js#L518
			let id = uid(24);
			if (req.path === '/login' && req.body.username) {
				id += `:${req.body.username}`;
			}
			return id;
		},
	}));
	return sessionMiddleware(req, res, next);

};


code:
'use strict';

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

module.exports = async (posts, deleting) => {

	//get a map of boards to threads affected
	const boardThreadMap = {};
	for (let i = 0; i < posts.length; i++) {
		const post = posts[i];
		if (!boardThreadMap[post.board]) {
			boardThreadMap[post.board] = {
				'directThreads': new Set(),
				'threads': new Set()
			};
		}
		if (!post.thread) {
			//a thread was directly selected on this board, not just posts. so we handle deletes differently
			boardThreadMap[post.board].directThreads.add(post.postId);
		}
		const threadId = post.thread || post.postId;
		boardThreadMap[post.board].threads.add(threadId);
	}

	const numPagesBeforeActions = {};
	const affectedBoardNames = Object.keys(boardThreadMap);
	//get number of pages for each before actions for deleting old pages and changing page nav numbers incase number of pages changes
	if (deleting) {
		await Promise.all(affectedBoardNames.map(async board => {
			numPagesBeforeActions[board] = Math.ceil((await Posts.getPages(board)) / 10);
		}));
	}

	return { boardThreadMap, numPagesBeforeActions, affectedBoardNames };

};


code:
'use strict';

module.exports = require('child_process')
	.execSync('git rev-parse --short HEAD')
	.toString()
	.trim();


code:
'use strict';

const redis = require(__dirname+'/../redis/redis.js')
	, Mongo = require(__dirname+'/../../db/db.js');

const load = async (message) => {
	module.exports.get = message || (await Mongo.getConfig());
};

redis.addCallback('config', load);

module.exports = {
	get: null,
	load,
};


code:
'use strict';

const countries = require('i18n-iso-countries')
	, countryNamesMap = countries.getNames('en')
	, extraCountryCodes = ['EU', 'XX', 'T1']
	, anonymizerCountryCodes = ['TOR', 'LOKI']
	, anonymizerCountryCodesSet = new Set(anonymizerCountryCodes)
	, countryCodes = Object.keys(countryNamesMap).concat(extraCountryCodes, anonymizerCountryCodes)
	, i18n = require(__dirname+'/../locale/locale.js')
	, extraCountryNames = Object.seal(Object.freeze(Object.preventExtensions({
		'EU': 'Europe',
		'XX': 'Unknown',
		'T1': 'Tor Exit Node',
		'TOR': 'Tor Hidden Service',
		'LOKI': 'Lokinet SNApp',
	})));

//Monkey patch until https://github.com/michaelwittig/node-i18n-iso-countries/issues/322
const alpha3s = countries.getAlpha3Codes();
alpha3s['TOR'] = 'TOR';
alpha3s['LOKI'] = 'LOKI';

i18n.getLocales()
	.forEach(locale => {
		const localeExtraCodesMap = { ...extraCountryNames };
		for (const code in localeExtraCodesMap) {
			localeExtraCodesMap[code] = i18n.__({
				phrase: localeExtraCodesMap[code],
				locale: locale,
			});
		}
		/* We are basically overwriting the existing locales in countries,
		but with translated extra codes added */
		const splitLocale = locale.split('-')[0]; //i18n-iso-countries doesnt have variants
		countries.registerLocale({
			locale: locale.toLowerCase(), //i18n-iso-countries toLowerCases these internally... ffs
			countries: {
				...countries.getNames(splitLocale, { select: 'official' }),
				...localeExtraCodesMap,
			},
		});
	});

module.exports = {
	countryCodes,
	countryCodesSet: new Set(countryCodes),
	getCountryNames: countries.getNames,
	getCountryName: countries.getName,
	isAnonymizer: (code) => {
		return anonymizerCountryCodesSet.has(code);
	},
};


code:
const OTPAuth = require('otpauth')
	, redis = require(__dirname+'/../redis/redis.js');

module.exports = async (username, totpSecret, userInput) => {

	const totp = new OTPAuth.TOTP({
		secret: totpSecret,
		algorithm: 'SHA256',
	});

	let delta = totp.validate({
		token: userInput,
		algorithm: 'SHA256',
		window: 1,
	});

	if (delta !== null) {
		const key = `twofactor_success:${username}:${userInput}`;
		const uses = await redis.incr(key);
		redis.expire(key, 30);
		if (uses && uses > 1) {
			return null;
		}
	}

	return delta;

};


code:
'use strict';

module.exports = (req, res, code, page, data) => {

	res.status(code);

	if (req.body.minimal) {
		data.minimal = true;
	}

	if (req.headers && req.headers['x-using-xhr'] != null) {
		//if sending header with js, and not a bypass_minimal page, show modal
		return res.json(data);
	} else {
		return res.render(page, data);
	}

};


code:
'use strict';

const { debugLogs } = require(__dirname+'/../../configs/secrets.js')
	, fontList = require('child_process')
		.execSync('fc-list -f "%{file}:%{family[0]} %{style[0]}\n"')
		.toString()
		.split('\n') //split by newlines, like here ^
		.filter(line => line) //filter empty lines
		.map(line => {
			//map to an object with path and name
			const [path, name] = line.split(':');
			return { path, name };
		})
		.sort((a, b) => {
			//alphabetical name sort
			return a.name.localeCompare(b.name);
		});

debugLogs && console.log(`${fontList.length} system fonts available`);

module.exports = {
	fontList,
	fontPaths: new Set(['default', ...fontList.map(f => f.path)]), //memoize paths
	DejaVuSans: fontList.find(f => f.name === 'DejaVu Sans Book'), //default for grid captchas
};


code:
'use strict';

const { ipHashSecret } = require(__dirname+'/../../configs/secrets.js')
	, { createHash } = require('crypto');

module.exports = (ip) => createHash('sha256').update(ipHashSecret + ip).digest('base64');


code:
'use strict';

const { redis: redisConfig } = require(__dirname+'/../../configs/secrets.js')
	, { Boards } = require(__dirname+'/../../db/')
	, roomRegex = /^(?<roomBoard>[a-z0-9]+)-(?<roomName>[a-z0-9-]+)$/i
	, calcPerms = require(__dirname+'/../permission/calcperms.js')
	, { Permissions } = require(__dirname+'/../permission/permissions.js')
	, socketIO = require('socket.io')
	, createAdapter = require('@socket.io/redis-adapter')
	, Redis = require('ioredis');

module.exports = {

	io: null, //null to begin with

	connect: (server, sessionMiddleware) => {
		//create redis adapter
		const io = socketIO(server)
			, pubClient = new Redis(redisConfig)
			, subClient = pubClient.duplicate();
		io.adapter(createAdapter(pubClient, subClient));
		//setup the middleware for sessions on socket
		const sessionRefresh = require(__dirname+'/../middleware/permission/sessionrefresh.js');
		io.use((socket, next) => {
			sessionMiddleware(socket.request, socket.request, next);
		});
		io.use((socket, next) => {
			sessionRefresh(socket.request, socket.request, next);
		});
		module.exports.io = io;
		//start accepting connections
		module.exports.startRooms();
	},

	startRooms: () => {
		module.exports.io.on('connection', socket => {
			socket.on('ping', cb => {
				if (typeof cb === 'function') {
					cb();
				}
			});
			socket.on('room', async (room) => {

				//check if a valid formatted room name
				const roomMatch = room.match(roomRegex);
				if (roomMatch && roomMatch.groups) {
					const { roomBoard, roomName } = roomMatch.groups;

					let hasPermission = true;

					//permission to manage/globalmanage based on MANAGE_GLOBAL/BOARD permissions
					if (room === 'globalmanage-recent-raw'
						|| room === 'globalmanage-recent-hashed') {
						socket.request.locals.board = null;
						socket.request.locals.permissions = calcPerms(socket.request, socket.request);
						hasPermission = socket.request.locals.permissions.get(Permissions.MANAGE_GLOBAL_GENERAL);
					} else {
						/* unlike normal endpoints, we cant get board from params and compare staffBoards and ownerBoards
						to roomBoard, so we need to put this here, in the room event and use the internal calcPerms after,
						instead of using a io.use( calcPermsMiddleware() after sessionRefresh() */
						socket.request.locals.board = await Boards.findOne(roomBoard);
						socket.request.locals.permissions = calcPerms(socket.request, socket.request);
						if (roomName === 'manage-recent-hashed'
							|| roomName === 'manage-recent-raw') {
							hasPermission = socket.request.locals.permissions.get(Permissions.MANAGE_BOARD_GENERAL);
						}
					}

					//if raw, must have room permission AND raw ip permission
					if (room.endsWith('-raw')) {
						hasPermission = hasPermission && socket.request.locals.permissions.get(Permissions.VIEW_RAW_IP);
					}

					//user has perms to join
					if (hasPermission === true) {
						socket.join(room);
						return socket.send('joined');
					}

				}

				//invalid room or no perms
				socket.disconnect(true);

			});
		});
	},

	emitRoom: (room, event, message) => {
		if (!module.exports.io) {
			return; //not initialized or in process that doesnt emit these events
		}
		module.exports.io.to(room).emit(event, message);
	},

};


code:
'use strict';

const { readdirSync } = require('fs-extra')
	, config = require(__dirname+'/config.js')
	, { addCallback } = require(__dirname+'/../redis/redis.js')
	, updateThemes = () => {
		const { themes, codeThemes } = config.get;
		module.exports.themes = themes.length > 0 ? themes : readdirSync(__dirname+'/../../gulp/res/css/themes/').filter(x => x.endsWith('.css')).map(x => x.substring(0,x.length-4));
		module.exports.codeThemes = codeThemes.length > 0 ? codeThemes : readdirSync(__dirname+'/../../node_modules/highlight.js/styles/').filter(x => x.endsWith('.css')).map(x => x.substring(0,x.length-4));
	};

updateThemes();
addCallback('config', updateThemes);


code:
'use strict';

const { Permissions } = require(__dirname+'/permissions.js')
	, Permission = require(__dirname+'/permission.js')
	, roleManager = require(__dirname+'/rolemanager.js');

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

	let calculatedPermissions;

	if (req.session && res.locals && res.locals.user) {

		//has a session and user, not anon, so their permissions from the db/user instead.
		const { user } = res.locals;
		calculatedPermissions = new Permission(user.permissions);

		//if they are on a board endpoint, also apply the board perms.
		let boardPermissions, boardRolePermission;
		if (res.locals.board != null) {
			if (res.locals.board.owner === user.username) {
				//they are board owner, give them board owner perms, in this board context
				calculatedPermissions.set(Permissions.MANAGE_BOARD_OWNER);
				boardPermissions = new Permission(res.locals.board.staff[user.username].permissions);
				boardRolePermission = roleManager.roles.BOARD_OWNER;
			} else if (res.locals.board.staff[user.username] != null) {
				//they are a board staff
				boardPermissions = new Permission(res.locals.board.staff[user.username].permissions);
				boardRolePermission = roleManager.roles.BOARD_STAFF;
			}
			if (boardPermissions) {
				//set their board level perms calculated from boardPermissions and boardRolePermission bits in _MANAGE_BOARD_BITS
				for (let bit of Permissions._MANAGE_BOARD_BITS) {
					const inheritOrGlobal = calculatedPermissions.get(bit)	//if they've been given the perm on their account (applies globally), or non-board role (including "global" BO/staffs)
						|| (boardPermissions.get(bit) 						//OR they have the bit on the board
							&& boardRolePermission.get(bit));				//and the "Board owner"/"Board staff" role doesn't have it unchecked globally
					calculatedPermissions.set(bit, inheritOrGlobal);		//set the inherited board permission bit
				}
			}

			//TODO: multi-board perms, note below comment maybe outdated, keeping for posterity
			//and note, in future since we might need multiple-boards permission checks, we will have to change this.
			//could even build it with a map for each board, based on their stored permissions in that board, maybe like:
			//res.locals.boardPermissions[board] = new Permission(res.locals.board.settings.staff[user.username].permissions);
			//and then the MANAGE_BOARD_OWNER inheritance could be removed, since it should be set immutable
			//inside the board perms instead. and the existing code would make it for "global" BOs to have the permissions.
			//so we would remove the "...permissions.set(Permissions.MANAGE_BOARD_OWNER)..." above

		}

		//give ROOT all permission, MANAGE_GLOBAL_BANS perms for VIEW_BOARD_GLOBAL_BANS, etc
		calculatedPermissions.applyInheritance();

	} else {
		//not logged in, gets default anon permission
		calculatedPermissions = new Permission(roleManager.roles.ANON.base64);
	}

	return calculatedPermissions;

};


code:
'use strict';

const { Permissions, Metadata } = require(__dirname+'/permissions.js')
	, BigBitfield = require('big-bitfield');

class Permission extends BigBitfield {

	constructor(data) {
		super(data);
	}

	// List of permission bits
	static allPermissions = Object.values(Permissions)
		.filter(v => typeof v === 'number');

	// Convert to a map of bit to metadata and state, for use in templates
	toJSON() {
		return Object.entries(Metadata)
			.reduce((acc, entry) => {
				acc[entry[0]] = {
					state: this.get(entry[0]),
					...entry[1],
				};
				return acc;
			}, {});
	}

	// Update permission based on body and another users permission
	handleBody(body, editorPermission, boardOnly=false) {
		const handlingBits = boardOnly ? Permissions._MANAGE_BOARD_BITS : Object.keys(Metadata);
		for (let bit of handlingBits) {
			// If perm has no "parents" bit list, or current user has at least one of the parent permissions, set each bit based on the form input
			const allowedParent = Metadata[bit].parents == null
				|| editorPermission.hasAny(...Metadata[bit].parents);
			if (allowedParent && !Metadata[bit].block) {
				this.set(parseInt(bit), (body[`permission_bit_${bit}`] != null));
			}
		}
	}

	applyInheritance() {
		if (this.get(Permissions.ROOT)) { //root gets all perms
			this.setAll(Permission.allPermissions);
		}
		if (this.get(Permissions.MANAGE_GLOBAL_BANS)) {
			this.set(Permissions.VIEW_BOARD_GLOBAL_BANS);
		}
	}

}

module.exports = Permission;


code:
const Permission = require('./permission.js');
const { Permissions } = require('./permissions.js');

describe('testing permissions', () => {

	const NO_PERMISSION = new Permission();

	const ANON = new Permission();
	ANON.setAll([
		Permissions.USE_MARKDOWN_PINKTEXT, Permissions.USE_MARKDOWN_GREENTEXT, Permissions.USE_MARKDOWN_BOLD,
		Permissions.USE_MARKDOWN_UNDERLINE, Permissions.USE_MARKDOWN_STRIKETHROUGH, Permissions.USE_MARKDOWN_TITLE,
		Permissions.USE_MARKDOWN_ITALIC, Permissions.USE_MARKDOWN_SPOILER, Permissions.USE_MARKDOWN_MONO,
		Permissions.USE_MARKDOWN_CODE, Permissions.USE_MARKDOWN_DETECTED, Permissions.USE_MARKDOWN_LINK,
		Permissions.USE_MARKDOWN_DICE, Permissions.USE_MARKDOWN_FORTUNE, Permissions.CREATE_BOARD,
		Permissions.CREATE_ACCOUNT
	]);

	test('test a permission they have = true', () => {
		expect(ANON.get(Permissions.CREATE_ACCOUNT)).toBe(true);
	});

	test('test a permission they dont have = false', () => {
		expect(ANON.get(Permissions.ROOT)).toBe(false);
	});

	const BOARD_STAFF = new Permission(ANON.base64);
	BOARD_STAFF.setAll([
		Permissions.MANAGE_BOARD_GENERAL, Permissions.MANAGE_BOARD_BANS, Permissions.MANAGE_BOARD_LOGS,
	]);
	const BOARD_OWNER = new Permission(BOARD_STAFF.base64);
	BOARD_OWNER.setAll([
		Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_BOARD_STAFF, Permissions.MANAGE_BOARD_CUSTOMISATION,
		Permissions.MANAGE_BOARD_SETTINGS, Permissions.USE_MARKDOWN_IMAGE
	]);

	test('BO has all board perms', () => {
		expect(Permissions._MANAGE_BOARD_BITS.every(b => BOARD_OWNER.get(b))).toBe(true);
	});

	const GLOBAL_STAFF = new Permission(BOARD_OWNER.base64);
	GLOBAL_STAFF.setAll([
		Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_GLOBAL_BANS, Permissions.MANAGE_GLOBAL_LOGS, Permissions.MANAGE_GLOBAL_NEWS,
		Permissions.MANAGE_GLOBAL_BOARDS, Permissions.MANAGE_GLOBAL_SETTINGS, Permissions.MANAGE_BOARD_OWNER, Permissions.BYPASS_FILTERS,
		Permissions.BYPASS_BANS, Permissions.BYPASS_SPAMCHECK, Permissions.BYPASS_RATELIMITS, Permissions.BYPASS_DNBSBL, Permissions.USE_MARKDOWN_IMAGE
	]);
	const ADMIN = new Permission(GLOBAL_STAFF.base64);
	ADMIN.setAll([
		Permissions.MANAGE_GLOBAL_ACCOUNTS, Permissions.MANAGE_GLOBAL_ROLES, Permissions.VIEW_RAW_IP,
	]);
	const ROOT = new Permission();
	ROOT.setAll(Permission.allPermissions);

	test('root has all permissions', () => {
		expect(Permission.allPermissions.every(p => ROOT.get(p))).toBe(true);
	});

	test('applyInheritance() gives ROOT all permissions as long as they have Permissions.ROOT', () => {
		NO_PERMISSION.set(Permissions.ROOT);
		NO_PERMISSION.applyInheritance();
		expect(Permission.allPermissions.every(b => NO_PERMISSION.get(b))).toBe(true);
	});

	test('handleBody() by somebody with editorPermission NOT having Permissions.ROOT cannot set Permissions.ROOT', () => {
		const TEST_PERMISSION = new Permission();
		TEST_PERMISSION.handleBody({
			'permission_bit_0': 0,
		}, ANON);
		expect(TEST_PERMISSION.get(0)).toBe(false);
	});

	test('handleBody() by somebody with editorPermission having Permissions.ROOT CAN set Permissions.ROOT', () => {
		const TEST_PERMISSION = new Permission();
		TEST_PERMISSION.handleBody({
			'permission_bit_0': 0,
		}, ROOT);
		expect(TEST_PERMISSION.get(0)).toBe(true);
	});

	test('handleBody() does not allow setting permission outside of _MANAGE_BOARD_BITS when boardOnly=true, even with permission', () => {
		const TEST_PERMISSION = new Permission();
		TEST_PERMISSION.handleBody({
			'permission_bit_0': 0,
		}, ROOT, true);
		expect(TEST_PERMISSION.get(0)).toBe(false);
	});

});


code:
'use strict';

const Permissions = Object.seal(Object.freeze(Object.preventExtensions({

	ROOT: 0,

	VIEW_RAW_IP: 1,

	CREATE_BOARD: 2,
	CREATE_ACCOUNT: 3,

	BYPASS_BANS: 4,
	BYPASS_SPAMCHECK: 5,
	BYPASS_RATELIMITS: 6,
	BYPASS_FILTERS: 7,
	BYPASS_CAPTCHA: 8,
	BYPASS_DNSBL: 9,
	BYPASS_ANONYMIZER_RESTRICTIONS: 10,

	MANAGE_GLOBAL_GENERAL: 11,
	MANAGE_GLOBAL_LOGS: 12,
	MANAGE_GLOBAL_NEWS: 13,
	MANAGE_GLOBAL_BOARDS: 14,
	MANAGE_GLOBAL_SETTINGS: 15,
	MANAGE_GLOBAL_ACCOUNTS: 16,
	MANAGE_GLOBAL_ROLES: 17,
	MANAGE_GLOBAL_BANS: 18,

	MANAGE_BOARD_OWNER: 20,
	MANAGE_BOARD_GENERAL: 21,
	MANAGE_BOARD_BANS: 22,
	MANAGE_BOARD_LOGS: 23,
	MANAGE_BOARD_SETTINGS: 24,
	MANAGE_BOARD_CUSTOMISATION: 25,
	MANAGE_BOARD_STAFF: 26,
	_MANAGE_BOARD_BITS: [20,21,22,23,24,25,26,49], //bits that can be set by a BO and partial bitfield will be stored in board staff object

	VIEW_BOARD_GLOBAL_BANS: 30,

	USE_MARKDOWN_PINKTEXT: 35,
	USE_MARKDOWN_GREENTEXT: 36,
	USE_MARKDOWN_BOLD: 37,
	USE_MARKDOWN_UNDERLINE: 38,
	USE_MARKDOWN_STRIKETHROUGH: 39,
	USE_MARKDOWN_TITLE: 40,
	USE_MARKDOWN_ITALIC: 41,
	USE_MARKDOWN_SPOILER: 42,
	USE_MARKDOWN_MONO: 43,
	USE_MARKDOWN_CODE: 44,
	USE_MARKDOWN_DETECTED: 45,
	USE_MARKDOWN_LINK: 46,
	USE_MARKDOWN_DICE: 47,
	USE_MARKDOWN_FORTUNE: 48,
	USE_MARKDOWN_IMAGE: 49,

})));

const Metadata = Object.seal(Object.freeze(Object.preventExtensions({

	[Permissions.ROOT]: { title: 'Root', label: 'Root', desc: 'Full control. Use with caution!', parents: [Permissions.ROOT] },

	[Permissions.VIEW_RAW_IP]: { title: 'Raw IPs', label: 'View Raw IPs', desc: 'Ability to see raw IPs in moderation interfaces.' },

	[Permissions.CREATE_BOARD]: { title: 'Create', label: 'Create Board', desc: 'Ability to create new boards.' },
	[Permissions.CREATE_ACCOUNT]: { label: 'Create Account', desc: 'Ability to register an account.' },

	[Permissions.BYPASS_BANS]: { title: 'Bypasses', label: 'Bypass Bans', desc: 'Bypass all bans.' },
	[Permissions.BYPASS_SPAMCHECK]: { label: 'Bypass Spamcheck', desc: 'Bypass the basic anti-flood spamcheck for too frequent similar posting.' },
	[Permissions.BYPASS_RATELIMITS]: { label: 'Bypass Ratelimits', desc: 'Bypass ratelimits for getting new captchas, editing posts, editing board settings, etc.' },
	[Permissions.BYPASS_FILTERS]: { label: 'Bypass Filters', desc: 'Bypass all post filters.' },
	[Permissions.BYPASS_CAPTCHA]: { label: 'Bypass Captcha', desc: 'Bypass captcha.' },
	[Permissions.BYPASS_DNSBL]: { label: 'Bypass DNSBL', desc: 'Bypass DNSBL.' },
	[Permissions.BYPASS_ANONYMIZER_RESTRICTIONS]: { label: 'Bypass Anonymizer Restrictions', desc: 'Bypass anonymizer restrictions e.g. disabled file posting.' },

	[Permissions.MANAGE_GLOBAL_GENERAL]: { title: 'Global Management',label: 'Global Staff', desc: 'General global staff permission. Access to recent posts and reports. Ability to submit global actions.' },
	[Permissions.MANAGE_GLOBAL_BANS]: { label: 'Global Bans', desc: 'Access global bans. Ability to unban, edit, or deny appeals.' },
	[Permissions.MANAGE_GLOBAL_LOGS]: { label: 'Global Logs', desc: 'Access global logs. Ability to search/filter' },
	[Permissions.MANAGE_GLOBAL_NEWS]: { label: 'News', desc: 'Access news posting. Ability to add, edit, or delete newsposts.' },
	[Permissions.MANAGE_GLOBAL_BOARDS]: { label: 'Boards', desc: 'Access the global board list. Ability to search/filter. Also grants the ability to transfer or delete any board.' },
	[Permissions.MANAGE_GLOBAL_SETTINGS]: { label: 'Global Settings', desc: 'Access global settings. Ability to change any settings.' },
	[Permissions.MANAGE_GLOBAL_ACCOUNTS]: { label: 'Accounts', desc: 'Access the accounts list. Ability to search/sort. Ability to edit permissions of any user.', parents: [Permissions.ROOT] },
	[Permissions.MANAGE_GLOBAL_ROLES]: { label: 'Roles', desc: 'Access roles list. Ability to edit roles', parents: [Permissions.ROOT] },

	[Permissions.MANAGE_BOARD_OWNER]: { title: 'Board Management', subtitle: 'Note: Setting board management permissions on an account/role level will grant them globally i.e for all boards.\nTo make somebody a normal board owner/staff, transfer them the board or give them the appropriate permissions in the board staff permission editing interface.', label: 'Board Owner', desc: 'Full control of the board, equivalent to the BO. Can delete and/or transfer the board. Use with caution!', parents: [Permissions.MANAGE_BOARD_OWNER] },
	[Permissions.MANAGE_BOARD_GENERAL]: { label: 'Board Staff', desc: 'General board staff permission. Access mod index, catalog, recent posts and reports. Ability to submit mod actions. Bypass board-specific bans and post filters.', block: true },
	[Permissions.MANAGE_BOARD_BANS]: { label: 'Bans', desc: 'Access board bans. Ability to unban, edit, or deny appeals.' },
	[Permissions.MANAGE_BOARD_LOGS]: { label: 'Logs', desc: 'Access board logs. Ability to search/filter.' },
	[Permissions.MANAGE_BOARD_SETTINGS]: { label: 'Settings', desc: 'Access board settings. Ability to change any settings. Settings page will show transfer/delete forms for those with "Board Owner" permission.' },
	[Permissions.MANAGE_BOARD_CUSTOMISATION]: { label: 'Customisation', desc: 'Access to board assets and custompages. Ability to upload, create, edit, delete.' },
	[Permissions.MANAGE_BOARD_STAFF]: { label: 'Staff', desc: 'Access to staff management, and ability to add or remove permissions from others. Use with caution!', parents: [Permissions.MANAGE_BOARD_OWNER] },
	[Permissions.VIEW_BOARD_GLOBAL_BANS]: { label: 'View Board Global Bans', desc: 'Ability to view global bans on board modlog pages if the banned post originated from that board.', parents: [Permissions.ROOT] },

	[Permissions.USE_MARKDOWN_PINKTEXT]: { title: 'Post styling', label: 'Pinktext', desc: 'Use pinktext' },
	[Permissions.USE_MARKDOWN_GREENTEXT]: { label: 'Greentext', desc: 'Use greentext' },
	[Permissions.USE_MARKDOWN_BOLD]: { label: 'Bold', desc: 'Use bold' },
	[Permissions.USE_MARKDOWN_UNDERLINE]: { label: 'Underline', desc: 'Use underline' },
	[Permissions.USE_MARKDOWN_STRIKETHROUGH]: { label: 'Strikethrough', desc: 'Use strikethrough' },
	[Permissions.USE_MARKDOWN_TITLE]: { label: 'Title', desc: 'Use titles' },
	[Permissions.USE_MARKDOWN_ITALIC]: { label: 'Italic', desc: 'Use italics' },
	[Permissions.USE_MARKDOWN_SPOILER]: { label: 'Spoiler', desc: 'Use spoilers' },
	[Permissions.USE_MARKDOWN_MONO]: { label: 'Inline Monospace', desc: 'Use inline monospace' },
	[Permissions.USE_MARKDOWN_CODE]: { label: 'Code Block', desc: 'Use code blocks' },
	[Permissions.USE_MARKDOWN_DETECTED]: { label: 'Detected', desc: 'Use detected' },
	[Permissions.USE_MARKDOWN_LINK]: { label: 'Links', desc: 'Make links clickable' },
	[Permissions.USE_MARKDOWN_DICE]: { label: 'Dice Roll', desc: 'Use dice rolls' },
	[Permissions.USE_MARKDOWN_FORTUNE]: { label: 'Fortune', desc: 'Use fortunes' },
	[Permissions.USE_MARKDOWN_IMAGE]: { label: 'Embed Images', desc: 'Embed images', parents: [Permissions.MANAGE_BOARD_OWNER] }, //TODO: add a new permission bit to manage each board inherited perm and use them as additional parents

})));

module.exports = {

	Permissions,

	Metadata,

};


code:
'use strict';

const { Roles } = require(__dirname+'/../../db/')
	, redis = require(__dirname+'/../redis/redis.js')
	, Permission = require(__dirname+'/permission.js');

const load = async () => {

	//todo: take a message argument from callback
	//maybe make it a separate func just for reloading single role?

	let roles = await Roles.find();
	roles = roles.reduce((acc, r) => {
		acc[r.name] = new Permission(r.permissions.toString('base64'));
		return acc;
	}, {});

	module.exports.roles = roles;

	module.exports.rolePermissionMap = {
		[roles.ANON.base64]: 'Regular User',
		// [roles.BOARD_STAFF_DEFAULTS.base64]: 'Board Staff Defaults',
		[roles.BOARD_STAFF.base64]: 'Board Staff',
		// [roles.BOARD_OWNER_DEFAULTS.base64]: 'Board Owner Defaults',
		[roles.BOARD_OWNER.base64]: 'Board Owner',
		[roles.GLOBAL_STAFF.base64]: 'Global Staff',
		[roles.ADMIN.base64]: 'Admin',
		[roles.ROOT.base64]: 'Root',
	};

	//Note: kinda redundant, might remove/change how this works
	module.exports.roleNameMap = {
		ANON: 'Regular User',
		BOARD_STAFF_DEFAULTS: 'Board Staff Defaults',
		BOARD_STAFF: 'Board Staff',
		BOARD_OWNER_DEFAULTS: 'Board Owner Defaults',
		BOARD_OWNER: 'Board Owner',
		GLOBAL_STAFF: 'Global Staff',
		ADMIN: 'Admin',
		ROOT: 'Root',
	};

};

redis.addCallback('roles', load);

module.exports = {
	roles: {},
	roleNameMap: {},
	rolePermissionMap: {},
	load,
};


code:
'use strict';

module.exports = (filters, combinedString, strictCombinedString) => {

	let filterHits = [];
	for (const filter of filters) {
		if (filter.filterMode === 0) { continue; } //skip "Do nothing" mode filters
		const string = filter.strictFiltering ? strictCombinedString : combinedString;
		const hitFilter = filter.filters.find(match => string.includes(match.toLowerCase()) );
		if (hitFilter) {
			//if either of these are hit, we can stop checking
			if (filter.filterMode === 1 || filter.filterMode === 2) {
				return [{ h: hitFilter, f: filter }];
			} else {
				filterHits.push({ h: hitFilter, f: filter });
			}
		}
	}

	return filterHits.length ? filterHits : false;

};


code:
const checkFilters = require('./checkfilters.js');
const getFilterStrings = require('./getfilterstrings.js');

const rs = () => Math.random().toString(26);

//non-message are just small random strings
const getDummyReq = (message) => ({
	body: {
		name: rs(),
		message: message,
		subject: rs(),
		email: rs(),
	},
	files: {
		file: new Array(3)
			.fill(0)
			.map(() => ({
				name: rs(),
				fname: rs(),
			}))
	}
});

const getDummyRes = () => ({
	locals: {
		numFiles: 3,
	}
});

const normalFilter = {
	_id: 'normal',
	board: 'test',
	filters: [ 'ace' ],
	strictFiltering: false,
	filterMode: 0,
	filterMessage: 'test',
	filterBanDuration: 10000,
	filterBanAppealable: false
};

const strictFilter = {
	_id: 'strict',
	board: 'test',
	filters: [ 'ace' ],
	strictFiltering: true,
	filterMode: 0,
	filterMessage: 'test',
	filterBanDuration: 10000,
	filterBanAppealable: false
};

describe('checkFilters() - basic filter matching', () => {
	const strings = [
		{ in: 'ace', out: true },
		{ in: 'áçé', out: false },
	];
	for (let s of strings) {
		const { combinedString, strictCombinedString } = getFilterStrings(getDummyReq(s.in), getDummyRes());
		test(`should not error and match ${s.out} for filter ${normalFilter._id}`, async () => {
			const res = await checkFilters([normalFilter], combinedString, strictCombinedString);
			if (s.out) {
				expect(true).not.toBe(false);
			} else {
				expect(res).toBe(false);
			}
		});
	}
});

describe('checkFilters() - basic filter not matching', () => {
	const strings = [
		{ in: 'zzz', out: false },
		{ in: '123', out: false },
	];
	for (let s of strings) {
		const { combinedString, strictCombinedString } = getFilterStrings(getDummyReq(s.in), getDummyRes());
		test(`should not error and match ${s.out} for filter ${normalFilter._id}`, async () => {
			const res = await checkFilters([normalFilter], combinedString, strictCombinedString);
			if (s.out) {
				expect(res).not.toBe(false);
			} else {
				expect(res).toBe(false);
			}
		});
	}
});

describe('checkFilters() - strict filter matching', () => {
	const strings = [
		{ in: 'ace', out: true },
		{ in: 'áçé', out: true },
	];
	for (let s of strings) {
		const { combinedString, strictCombinedString } = getFilterStrings(getDummyReq(s.in), getDummyRes());
		test(`should not error and match ${s.out} for filter ${normalFilter._id}`, async () => {
			const res = await checkFilters([strictFilter], combinedString, strictCombinedString);
			if (s.out) {
				expect(true).not.toBe(false);
			} else {
				expect(res).toBe(false);
			}
		});
	}
});

describe('checkFilters() - strict filter not matching', () => {
	const strings = [
		{ in: 'zzz', out: false },
		{ in: '123', out: false },
	];
	for (let s of strings) {
		const { combinedString, strictCombinedString } = getFilterStrings(getDummyReq(s.in), getDummyRes());
		test(`should not error and match ${s.out} for filter ${normalFilter._id}`, async () => {
			const res = await checkFilters([strictFilter], combinedString, strictCombinedString);
			if (s.out) {
				expect(res).not.toBe(false);
			} else {
				expect(res).toBe(false);
			}
		});
	}
});