Merge pull request #31 from fatchan/globalactions

file deduplication, global manage action rebuilds, floating postform, mov files, temp file deletion, more aids
merge-requests/208/head
Tom 5 years ago committed by GitHub
commit 221fdf0a00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 51
      build.js
  2. 281
      controllers/forms.js
  3. 2
      db/boards.js
  4. 11
      db/posts.js
  5. 86
      gulp/res/css/style.css
  6. BIN
      gulp/res/img/deleted.png
  7. 31
      helpers/actionchecker.js
  8. 4
      helpers/captchaverify.js
  9. 23
      helpers/files/deletetempfiles.js
  10. 0
      helpers/files/formatsize.js
  11. 4
      helpers/files/imageidentify.js
  12. 0
      helpers/files/imagethumbnail.js
  13. 1
      helpers/files/mimetypes.js
  14. 4
      helpers/files/videoidentify.js
  15. 5
      helpers/files/videothumbnail.js
  16. 3
      helpers/haspermsmiddleware.js
  17. 8
      helpers/id-contrast.js
  18. 48
      helpers/paramconverter.js
  19. 12
      helpers/quotes.js
  20. 220
      models/forms/actionhandler.js
  21. 0
      models/forms/banposter.js
  22. 4
      models/forms/deletebanners.js
  23. 8
      models/forms/deletepost.js
  24. 40
      models/forms/deletepostsfiles.js
  25. 0
      models/forms/dismissreport.js
  26. 74
      models/forms/makepost.js
  27. 0
      models/forms/reportpost.js
  28. 0
      models/forms/spoilerpost.js
  29. 59
      models/forms/uploadbanners.js
  30. 29
      models/pages/banners.js
  31. 517
      package-lock.json
  32. 17
      package.json
  33. 43
      server.js
  34. 9
      views/includes/actionfooter.pug
  35. 4
      views/includes/actionfooter_globalmanage.pug
  36. 4
      views/includes/actionfooter_manage.pug
  37. 2
      views/includes/footer.pug
  38. 81
      views/includes/postform.pug
  39. 46
      views/mixins/catalogtile.pug
  40. 32
      views/mixins/post.pug
  41. 2
      views/pages/catalog.pug
  42. 2
      views/pages/manage.pug
  43. 1
      views/pages/thread.pug
  44. 4
      wipe.js

@ -5,9 +5,43 @@ const Posts = require(__dirname+'/db/posts.js')
, uploadDirectory = require(__dirname+'/helpers/uploadDirectory.js')
, render = require(__dirname+'/helpers/render.js');
function addBacklinks(thread, preview) { //preview means this is not the full thread
const postMap = new Map()
postMap.set(thread.postId, thread)
for (let i = 0; i < thread.replies.length; i++) {
const reply = thread.replies[i];
postMap.set(reply.postId, reply);
}
for (let i = 0; i < thread.replies.length; i++) {
const reply = thread.replies[i];
if (!reply.quotes) continue;
for (let j = 0; j < reply.quotes.length; j++) {
const quote = reply.quotes[j];
if (postMap.has(quote)) {
const post = postMap.get(quote)
if (!post.backlinks) {
post.backlinks = [];
}
post.backlinks.push(reply.postId);
} else if (!preview) {
/*
quote was valid on post creation, but points to postID that has been deleted
or possibly removed from cyclical thread (whenever i implement those)
could re-markdown the post here to remove the quotes (or convert to greentext)
*/
}
}
}
}
module.exports = {
buildCatalog: async (board) => {
//console.log('building catalog', `${board._id}/catalog.html`);
if (!board._id) {
board = await Boards.findOne(board);
}
const threads = await Posts.getCatalog(board._id);
return render(`${board._id}/catalog.html`, 'catalog.pug', {
board,
@ -22,8 +56,11 @@ module.exports = {
}
const thread = await Posts.getThread(board._id, threadId)
if (!thread) {
return; //this thread may have been an OP that was deleted during a rebuild
return; //this thread may have been an OP that was deleted
}
addBacklinks(thread, false);
return render(`${board._id}/thread/${threadId}.html`, 'thread.pug', {
board,
thread
@ -36,6 +73,12 @@ module.exports = {
if (!maxPage) {
maxPage = Math.ceil((await Posts.getPages(board._id)) / 10);
}
for (let k = 0; k < threads.length; k++) {
const thread = threads[k];
addBacklinks(thread, true);
}
return render(`${board._id}/${page === 1 ? 'index' : page}.html`, 'board.pug', {
board,
threads,
@ -56,6 +99,12 @@ module.exports = {
}
const difference = endpage-startpage + 1; //+1 because for single pagemust be > 0
const threads = await Posts.getRecent(board._id, startpage, difference*10);
for (let k = 0; k < threads.length; k++) {
const thread = threads[k];
addBacklinks(thread, true);
}
const buildArray = [];
for (let i = startpage; i <= endpage; i++) {
//console.log('multi building board page', `${board._id}/${i === 1 ? 'index' : i}.html`);

@ -4,30 +4,49 @@ const express = require('express')
, router = express.Router()
, Boards = require(__dirname+'/../db/boards.js')
, Posts = require(__dirname+'/../db/posts.js')
, Captchas = require(__dirname+'/../db/captchas.js')
, Trips = require(__dirname+'/../db/trips.js')
, Bans = require(__dirname+'/../db/bans.js')
, Mongo = require(__dirname+'/../db/db.js')
, remove = require('fs-extra').remove
, deletePosts = require(__dirname+'/../models/forms/delete-post.js')
, spoilerPosts = require(__dirname+'/../models/forms/spoiler-post.js')
, dismissGlobalReports = require(__dirname+'/../models/forms/dismissglobalreport.js')
, banPoster = require(__dirname+'/../models/forms/ban-poster.js')
, upload = require('express-fileupload')
, path = require('path')
, postFiles = upload({
createParentPath: true,
safeFileNames: /[^\w-]+/g,
preserveExtension: 4,
limits: {
fileSize: 10 * 1024 * 1024,
files: 3
},
abortOnLimit: true,
useTempFiles: true,
tempFileDir: path.join(__dirname+'/../tmp/')
})
, bannerFiles = upload({
createParentPath: true,
safeFileNames: /[^\w-]+/g,
preserveExtension: 4,
limits: {
fileSize: 10 * 1024 * 1024,
files: 10
},
abortOnLimit: true,
useTempFiles: true,
tempFileDir: path.join(__dirname+'/../tmp/')
})
, removeBans = require(__dirname+'/../models/forms/removebans.js')
, makePost = require(__dirname+'/../models/forms/make-post.js')
, makePost = require(__dirname+'/../models/forms/makepost.js')
, deleteTempFiles = require(__dirname+'/../helpers/files/deletetempfiles.js')
, uploadBanners = require(__dirname+'/../models/forms/uploadbanners.js')
, deleteBanners = require(__dirname+'/../models/forms/deletebanners.js')
, loginAccount = require(__dirname+'/../models/forms/login.js')
, changePassword = require(__dirname+'/../models/forms/changepassword.js')
, registerAccount = require(__dirname+'/../models/forms/register.js')
, checkPermsMiddleware = require(__dirname+'/../helpers/haspermsmiddleware.js')
, checkPerms = require(__dirname+'/../helpers/hasperms.js')
, paramConverter = require(__dirname+'/../helpers/paramconverter.js')
, banCheck = require(__dirname+'/../helpers/bancheck.js')
, deletePostFiles = require(__dirname+'/../helpers/files/deletepostfiles.js')
, verifyCaptcha = require(__dirname+'/../helpers/captchaverify.js')
, actionHandler = require(__dirname+'/../models/forms/actionhandler.js')
, csrf = require(__dirname+'/../helpers/csrfmiddleware.js')
, deleteFailedFiles = require(__dirname+'/../helpers/files/deletefailed.js')
, actionChecker = require(__dirname+'/../helpers/actionchecker.js');
@ -159,7 +178,7 @@ router.post('/register', verifyCaptcha, (req, res, next) => {
});
// make new post
router.post('/board/:board/post', Boards.exists, banCheck, paramConverter, verifyCaptcha, async (req, res, next) => {
router.post('/board/:board/post', Boards.exists, banCheck, postFiles, paramConverter, verifyCaptcha, async (req, res, next) => {
let numFiles = 0;
if (req.files && req.files.file) {
@ -207,25 +226,18 @@ router.post('/board/:board/post', Boards.exists, banCheck, paramConverter, verif
}
if (errors.length > 0) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}${req.body.thread ? '/thread/' + req.body.thread + '.html' : ''}`
})
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}${req.body.thread ? '/thread/' + req.body.thread + '.html' : ''}`
});
}
try {
await makePost(req, res, next, numFiles);
} catch (err) {
//handler errors here better
if (numFiles > 0) {
const fileNames = []
for (let i = 0; i < req.files.file.length; i++) {
remove(req.files.file[i].tempFilePath).catch(e => console.error);
fileNames.push(req.files.file[i].filename);
}
deletePostFiles(fileNames).catch(err => console.error);
}
await deleteTempFiles(req).catch(e => console.error);
return next(err);
}
@ -236,8 +248,8 @@ router.post('/board/:board/settings', csrf, Boards.exists, checkPermsMiddleware,
const errors = [];
if (req.body.default_name && req.body.default_name.length > 20) {
errors.push('Must provide a message or file');
if (req.body.default_name && req.body.default_name.length < 1 || req.body.default_name.length > 50) {
errors.push('Anon name must be 1-50 characters');
}
if (typeof req.body.reply_limit === 'number' && (req.body.reply_limit < 1 || req.body.reply_limit > 1000)) {
errors.push('Reply Limit must be from 1-1000');
@ -257,15 +269,19 @@ router.post('/board/:board/settings', csrf, Boards.exists, checkPermsMiddleware,
})
}
return res.status(501).render('message', {
'title': 'Not implemented',
'redirect': `/${req.params.board}/manage.html`
})
try {
return res.status(501).render('message', {
'title': 'Not implemented',
'redirect': `/${req.params.board}/manage.html`
})
} catch (err) {
return next(err);
}
});
//upload banners
router.post('/board/:board/addbanners', csrf, Boards.exists, checkPermsMiddleware, paramConverter, async (req, res, next) => {
router.post('/board/:board/addbanners', bannerFiles, csrf, Boards.exists, checkPermsMiddleware, paramConverter, async (req, res, next) => {
let numFiles = 0;
if (req.files && req.files.file) {
@ -282,11 +298,12 @@ router.post('/board/:board/addbanners', csrf, Boards.exists, checkPermsMiddlewar
if (numFiles === 0) {
errors.push('Must provide a file');
}
if (res.locals.board.banners.length > 100) {
errors.push('Limit of 100 banners reached');
if (res.locals.board.banners.length+numFiles > 100) {
errors.push('Number of uploads would exceed 100 banner limit');
}
if (errors.length > 0) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'errors': errors,
@ -297,14 +314,7 @@ router.post('/board/:board/addbanners', csrf, Boards.exists, checkPermsMiddlewar
try {
await uploadBanners(req, res, next, numFiles);
} catch (err) {
const fileNames = [];
if (numFiles > 0) {
for (let i = 0; i < req.files.file.length; i++) {
remove(req.files.file[i].tempFilePath).catch(e => console.error);
fileNames.push(req.files.file[i].filename);
}
}
deleteFailedFiles(fileNames, 'banner').catch(e => console.error);
await deleteTempFiles(req).catch(e => console.error);
return next(err);
}
@ -346,44 +356,73 @@ router.post('/board/:board/deletebanners', csrf, Boards.exists, checkPermsMiddle
});
//report/delete/spoiler/ban
router.post('/board/:board/actions', Boards.exists, banCheck, paramConverter, verifyCaptcha, actionHandler); //Captcha on regular actions
router.post('/board/:board/modactions', csrf, Boards.exists, checkPermsMiddleware, paramConverter, actionHandler); //CSRF for mod actions
//unban
router.post('/board/:board/unban', csrf, Boards.exists, checkPermsMiddleware, paramConverter, async (req, res, next) => {
//actions for a specific board
router.post('/board/:board/actions', Boards.exists, banCheck, paramConverter, verifyCaptcha, boardActionController); //Captcha on regular actions
router.post('/board/:board/modactions', csrf, Boards.exists, checkPermsMiddleware, paramConverter, boardActionController); //CSRF for mod actions
async function boardActionController(req, res, next) {
//keep this for later in case i add other options to unbans
const errors = [];
if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) {
errors.push('Must select 1-10 bans')
//make sure they checked 1-10 posts
if (!req.body.checkedposts || req.body.checkedposts.length === 0 || req.body.checkedposts.length > 10) {
errors.push('Must select 1-10 posts');
}
res.locals.actions = actionChecker(req);
//make sure they selected at least 1 action
if (!res.locals.actions.anyValid) {
errors.push('No actions selected');
}
//check if they have permission to perform the actions
res.locals.hasPerms = checkPerms(req, res);
if(!res.locals.hasPerms && res.locals.actions.anyAuthed) {
errors.push('No permission');
}
//check that actions are valid
if (req.body.password && req.body.password.length > 50) {
errors.push('Password must be 50 characters or less');
}
if (req.body.report_reason && req.body.report_reason.length > 50) {
errors.push('Report must be 50 characters or less');
}
if (req.body.ban_reason && req.body.ban_reason.length > 50) {
errors.push('Ban reason must be 50 characters or less');
}
if ((req.body.report || req.body.global_report) && (!req.body.report_reason || req.body.report_reason.length === 0)) {
errors.push('Reports must have a reason');
}
if (errors.length > 0) {
return res.status(400).render('message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}/manage.html`
});
'redirect': `/${req.params.board}/`
})
}
res.locals.posts = await Posts.getPosts(req.params.board, req.body.checkedposts, true);
if (!res.locals.posts || res.locals.posts.length === 0) {
return res.status(404).render('message', {
'title': 'Not found',
'error': 'Selected posts not found',
'redirect': `/${req.params.board}/`
})
}
const messages = [];
try {
messages.push((await removeBans(req, res, next)));
await actionHandler(req, res, next);
} catch (err) {
console.error(err);
return next(err);
}
return res.render('message', {
'title': 'Success',
'messages': messages,
'redirect': `/${req.params.board}/manage.html`
});
});
}
router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async(req, res, next) => {
//global actions (global manage page)
router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, globalActionController);
async function globalActionController(req, res, next) {
const errors = [];
@ -392,10 +431,10 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async
errors.push('Must select 1-10 posts')
}
const { anyGlobal } = actionChecker(req);
res.locals.actions = actionChecker(req);
//make sure they selected at least 1 global action
if (!anyGlobal) {
//make sure they have any global actions, and that they only selected global actions
if (!res.locals.actions.anyGlobal || res.locals.actions.anyValid > res.locals.actions.anyGlobal) {
errors.push('Invalid actions selected');
}
@ -403,15 +442,9 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async
if (req.body.password && req.body.password.length > 50) {
errors.push('Password must be 50 characters or less');
}
if (req.body.report_reason && req.body.report_reason.length > 50) {
errors.push('Report must be 50 characters or less');
}
if (req.body.ban_reason && req.body.ban_reason.length > 50) {
errors.push('Ban reason must be 50 characters or less');
}
if (req.body.report && (!req.body.report_reason || req.body.report_reason.length === 0)) {
errors.push('Reports must have a reason')
}
//return the errors
if (errors.length > 0) {
@ -423,8 +456,8 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async
}
//get posts with global ids only
const posts = await Posts.globalGetPosts(req.body.globalcheckedposts, true);
if (!posts || posts.length === 0) {
res.locals.posts = await Posts.globalGetPosts(req.body.globalcheckedposts, true);
if (!res.locals.posts || res.locals.posts.length === 0) {
return res.status(404).render('message', {
'title': 'Not found',
'errors': 'Selected posts not found',
@ -432,80 +465,36 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async
})
}
//get the ids
const postMongoIds = posts.map(post => Mongo.ObjectId(post._id));
try {
await actionHandler(req, res, next);
} catch (err) {
console.error(err);
return next(err);
}
}
//unban
router.post('/board/:board/unban', csrf, Boards.exists, checkPermsMiddleware, paramConverter, async (req, res, next) => {
//keep this for later in case i add other options to unbans
const errors = [];
if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) {
errors.push('Must select 1-10 bans')
}
if (errors.length > 0) {
return res.status(400).render('message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}/manage.html`
});
}
const messages = [];
const combinedQuery = {};
let aggregateNeeded = false;
try {
if (req.body.global_ban) {
const { message, action, query } = await banPoster(req, res, next, null, posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
}
if (req.body.delete_ip_global) {
const deletePostIps = posts.map(x => x.ip);
const deleteIpPosts = await Posts.db.find({
'ip': {
'$in': deletePostIps
}
}).toArray();
if (deleteIpPosts && deleteIpPosts.length > 0) {
const { message } = await deletePosts(req, res, next, deleteIpPosts, null);
messages.push(message);
aggregateNeeded = true;
}
} else if (req.body.delete) {
const { message } = await deletePosts(req, res, next, posts);
messages.push(message);
aggregateNeeded = true;
} else {
// if it was getting deleted, we cant do any of these
if (req.body.delete_file) {
const { message, action, query } = await deletePostsFiles(posts);
if (action) {
aggregateNeeded = true;
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
} else if (req.body.spoiler) {
const { message, action, query } = spoilerPosts(posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
}
if (req.body.global_dismiss) {
const { message, action, query } = dismissGlobalReports(posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
}
}
if (Object.keys(combinedQuery).length > 0) {
await Posts.db.updateMany({
'_id': {
'$in': postMongoIds
}
}, combinedQuery);
}
if (aggregateNeeded) {
const threadsToUpdate = [...new Set(posts.filter(post => post.thread !== null))];
//recalculate and set correct aggregation numbers again
await Promise.all(threadsToUpdate.map(async (post) => {
const replyCounts = await Posts.getReplyCounts(post.board, post.thread);
let replyposts = 0;
let replyfiles = 0;
if (replyCounts[0]) {
replyposts = replyCounts[0].replyposts;
replyfiles = replyCounts[0].replyfiles;
}
Posts.setReplyCounts(post.board, post.thread, replyposts, replyfiles);
}));
}
messages.push((await removeBans(req, res, next)));
} catch (err) {
return next(err);
}
@ -513,7 +502,7 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async
return res.render('message', {
'title': 'Success',
'messages': messages,
'redirect': '/globalmanage.html'
'redirect': `/${req.params.board}/manage.html`
});
});

@ -5,7 +5,7 @@ const Mongo = require(__dirname+'/db.js')
module.exports = {
db,
db: db.collection('boards'),
findOne: (name) => {
return db.collection('boards').findOne({ '_id': name });

@ -2,7 +2,6 @@
const Mongo = require(__dirname+'/db.js')
, Boards = require(__dirname+'/boards.js')
, deletePostFiles = require(__dirname+'/../helpers/files/deletepostfiles.js')
, db = Mongo.client.db('jschan').collection('posts');
module.exports = {
@ -272,7 +271,7 @@ module.exports = {
},
insertOne: async (board, data, thread) => {
if (data.thread !== null && data.email !== 'sage' && !thread.saged) {
if (data.thread !== null) {
const filter = {
'postId': data.thread,
'board': board
@ -355,14 +354,6 @@ module.exports = {
const threadPosts = await module.exports.getMultipleThreadPosts(board, threadIds);
//combine them
const postsAndThreads = threads.concat(threadPosts);
//get the filenames and delete all the files
let fileNames = [];
postsAndThreads.forEach(post => {
fileNames = fileNames.concat(post.files.map(x => x.filename))
});
if (fileNames.length > 0) {
await deletePostFiles(fileNames);
}
//get the mongoIds and delete them all
const postMongoIds = postsAndThreads.map(post => Mongo.ObjectId(post._id));
await module.exports.deleteMany(postMongoIds);

@ -19,6 +19,17 @@ body {
flex-direction: column;
}
pre {
font-family: inherit;
margin: 1em 2em;
white-space: pre-wrap;
}
.replies {
font-size: smaller;
clear: both;
}
.code {
text-align: left;
border-left: 10px solid #B7C5D9;
@ -27,7 +38,6 @@ body {
font-family: monospace;
margin: 0.5em 0;
display: flex;
clear: both;
overflow-x: auto;
white-space: pre;
}
@ -61,6 +71,7 @@ body {
box-sizing: border-box;
padding: 10px;
width: max-content;
width: -moz-max-content;
}
a, a:visited {
@ -87,22 +98,8 @@ object {
background: #d6daf0;
}
.catalog-tile-button {
width: 100%;
line-height: 30px;
float: left;
background: #B7C5D9;
text-decoration: none;
color: black;
margin-bottom: 5px;
overflow: hidden;
}
.catalog-tile-content {
padding: 5px;
}
.catalog-tile {
padding: 5px;
margin: 2px;
text-align: center;
max-height: 300px;
@ -120,15 +117,15 @@ object {
.catalog-thumb {
box-shadow: 0 0 3px black;
min-width: 64px;
min-height: 64px;
width: 64px;
height: 64px;
object-fit: cover;
}
.catalog {
display:flex;
align-items:flex-start;
justify-content: space-evenly;
justify-content: center;
flex-flow: row wrap;
}
@ -158,6 +155,13 @@ object {
font-weight: bold;
}
.close {
text-decoration: none;
width: 1.75em;
justify-content: center;
font-weight: bolder;
}
.reports {
background: #fca!important;
border-color: #c97!important;
@ -178,14 +182,10 @@ object {
color: green;
}
blockquote a, a:hover {
pre a, a:hover {
color: #d00!important;
}
blockquote {
white-space: pre-wrap;
}
.thread, .action-wrapper, .form-wrapper, .table-container {
display: flex;
flex-direction: column;
@ -219,7 +219,7 @@ td, th {
align-items: center;
}
.post-container, .pages, .toggle-summary {
.post-container, .pages, .toggle-summary, .catalog-tile {
background: #D6DAF0;
border: 1px solid #B7C5D9;
}
@ -260,6 +260,7 @@ td, th {
display: flex;
flex-flow: column wrap;
width: max-content;
width: -moz-max-content;
}
.toggle {
@ -326,7 +327,8 @@ td, th {
}
.post-file-src {
margin: 0 auto;
justify-content: center;
display: flex;
}
.file-thumb {
@ -390,8 +392,8 @@ input textarea {
}
.post-container:target {
background-color: #d6bad0;
border-color: #ba9dbf;
background-color: #d6bad0 !important;
border: 1px solid #ba9dbf !important;
}
.post-container.op {
@ -470,7 +472,7 @@ input textarea {
input[type="text"], input[type="submit"], input[type="password"], input[type="file"], textarea {
border: 1px solid #a9a9a9;
font-size: inherit;
font-family: arial,helvetica,sans-serif;
font-family: arial, helvetica, sans-serif;
margin: 0;
flex-grow: 1;
border-radius: 0px;
@ -484,6 +486,24 @@ input[type="file"] {
background: white;
}
#postform {
display: none;
width: 400px;
max-width: calc(100% - 10px);
position: fixed;
top: 45px;
right: 5px;
background-color: #D6DAF0;
border: 1px solid #b7c5d9;
padding: 5px;
z-index: 1;
box-sizing: border-box;
}
#postform:target {
display: flex;
}
.postform-row, .postform-col {
display: flex;
}
@ -539,14 +559,14 @@ hr {
}
.form-post {
width: 100%;
width: calc(100% - 10px)!important;
}
.form-login {
width: 100%;
}
blockquote {
pre {
margin: 1em;
}
@ -573,8 +593,8 @@ hr {
width: 100%;
}
.post-info {
background-color: #B7C5D9;
#postform {
top: 5px!important;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

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

@ -8,7 +8,9 @@ const Captchas = require(__dirname+'/../db/captchas.js')
module.exports = async (req, res, next) => {
//skip captcha if disabled on board for posts only
if (res.locals.board && req.path === `/board/${res.locals.board._id}/post` && !res.locals.board.settings.captcha) {
if (res.locals.board
&& req.path === `/board/${res.locals.board._id}/post`
&& !res.locals.board.settings.captcha) {
return next();
}

@ -0,0 +1,23 @@
'use strict';
const remove = require('fs-extra').remove;
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);
}));
}
}

@ -2,10 +2,10 @@ const gm = require('@tohru/gm')
, configs = require(__dirname+'/../../configs/main.json')
, uploadDirectory = require(__dirname+'/../uploadDirectory.js');
module.exports = (filename, folder) => {
module.exports = (filename, folder, temp) => {
return new Promise((resolve, reject) => {
gm(`${uploadDirectory}${folder}/${filename}`)
gm(temp === true ? filename : `${uploadDirectory}${folder}/${filename}`)
.identify(function (err, data) {
if (err) {
return reject(err);

@ -13,6 +13,7 @@ const animatedImageMimeTypes = new Set([
]);
const videoMimeTypes = new Set([
'video/quicktime',
'video/mp4',
'video/webm',
]);

@ -2,10 +2,10 @@ const ffmpeg = require('fluent-ffmpeg')
, configs = require(__dirname+'/../../configs/main.json')
, uploadDirectory = require(__dirname+'/../uploadDirectory.js');
module.exports = (filename) => {
module.exports = (filename, folder, temp) => {
return new Promise((resolve, reject) => {
ffmpeg.ffprobe(`${uploadDirectory}img/${filename}`, (err, metadata) => {
ffmpeg.ffprobe(temp === true ? filename : `${uploadDirectory}${folder}/${filename}`, (err, metadata) => {
if (err) {
return reject(err)
}

@ -2,7 +2,7 @@ const ffmpeg = require('fluent-ffmpeg')
, configs = require(__dirname+'/../../configs/main.json')
, uploadDirectory = require(__dirname+'/../uploadDirectory.js');
module.exports = (filename) => {
module.exports = (filename, geometry) => {
return new Promise((resolve, reject) => {
ffmpeg(`${uploadDirectory}img/${filename}`)
@ -14,7 +14,8 @@ module.exports = (filename) => {
count: 1,
filename: `thumb-${filename.split('.')[0]}.jpg`,
folder: `${uploadDirectory}img/`,
size: '128x?'
size: geometry.width > geometry.height ? '128x?' : '?x128'
//keep aspect ratio, but also making sure taller/wider thumbs dont exceed 128 in either dimension
});
});

@ -4,7 +4,8 @@ const hasPerms = require(__dirname+'/hasperms.js');
module.exports = async (req, res, next) => {
if (!hasPerms(req, res)) {
res.locals.hasPerms = hasPerms(req, res);
if (!res.locals.hasPerms) {
return res.status(403).render('message', {
'title': 'Forbidden',
'message': 'You do not have permission to access this page',

@ -1,8 +0,0 @@
'use strict';
module.exports = (hex) => {
const r = parseInt(hex.substr(0,2), 16);
const g = parseInt(hex.substr(2,2), 16);
const b = parseInt(hex.substr(4,2), 16)
return 0.375 * r + 0.5 * g + 0.125 * b;
}

@ -2,6 +2,7 @@
const Mongo = require(__dirname+'/../db/db.js')
, allowedArrays = new Set(['checkedposts', 'globalcheckedposts', 'checkedbans', 'checkedbanners'])
, numberFields = ['reply_limit', 'max_files', 'thread_limit', 'thread', 'min_message_length'];
module.exports = (req, res, next) => {
@ -25,48 +26,19 @@ module.exports = (req, res, next) => {
if (req.body.globalcheckedposts) {
req.body.globalcheckedposts = req.body.globalcheckedposts.map(Mongo.ObjectId)
}
//thread in post form
if (req.params.id) {
req.params.id = +req.params.id;
}
if (req.body.thread) {
req.body.thread = +req.body.thread;
}
//page number
if (req.query.p) {
const num = parseInt(req.query.p);
if (Number.isSafeInteger(num)) {
req.query.p = num;
} else {
req.query.p = null;
}
}
//board settings
if (req.body.reply_limit != null) {
const num = parseInt(req.body.reply_limit);
if (Number.isSafeInteger(num)) {
req.body.reply_limit = num;
} else {
req.body.reply_limit = null;
}
}
if (req.body.max_files != null) {
const num = parseInt(req.body.max_files);
if (Number.isSafeInteger(num)) {
req.body.max_files = num;
} else {
req.body.max_files = null;
}
}
if (req.body.thread_limit != null) {
const num = +parseInt(req.body.thread_limit);
if (Number.isSafeInteger(num)) {
req.body.thread_limit = num;
} else {
req.body.thread_limit = null;
for (let i = 0; i < numberFields.length; i++) {
const field = numberFields[i];
if (req.body[field]) {
const num = parseInt(req.body[field]);
if (Number.isSafeInteger(num)) {
req.body[field] = num;
} else {
req.body[field] = null;
}
}
}

@ -11,14 +11,14 @@ module.exports = async (board, text) => {
const quotes = text.match(quoteRegex);
const crossQuotes = text.match(crossQuoteRegex);
if (!quotes && !crossQuotes) {
return text;
return { quotedMessage: text, threadQuotes: [] };
}
//make query for db including crossquotes
const queryOrs = []
const crossQuoteMap = {};
if (quotes) {
const quoteIds = quotes.map(q => +q.substring(2));
const quoteIds = [...new Set(quotes.map(q => +q.substring(2)))]; //only uniques
queryOrs.push({
'board': board,
'postId': {
@ -58,7 +58,7 @@ module.exports = async (board, text) => {
const posts = await Posts.getPostsForQuotes(queryOrs);
//if none of the quotes were real, dont do a replace
if (posts.length === 0) {
return text;
return { quotedMessage: text, threadQuotes: [] };
}
//turn the result into a map of postId => threadId/postId
for (let i = 0; i < posts.length; i++) {
@ -71,11 +71,13 @@ module.exports = async (board, text) => {
}
//then replace the quotes with only ones that exist
const threadQuotes = [];
if (quotes && Object.keys(postThreadIdMap).length > 0) {
text = text.replace(quoteRegex, (match) => {
const quotenum = +match.substring(2);
if (postThreadIdMap[board] && postThreadIdMap[board][quotenum]) {
return `<a class='quote' href='/${board}/thread/${postThreadIdMap[board][quotenum]}.html#${quotenum}'>&gt;&gt;${quotenum}</a>`;
threadQuotes.push(quotenum)
return `<a class='quote' href='/${board}/thread/${postThreadIdMap[board][quotenum]}.html#${quotenum}'>&gt;&gt;${quotenum}</a>${postThreadIdMap[board][quotenum] === quotenum ? ' <small>(OP)</small> ' : ''}`;
}
return match;
});
@ -94,6 +96,6 @@ module.exports = async (board, text) => {
});
}
return text;
return { quotedMessage: text, threadQuotes: [...new Set(threadQuotes)] };
}

@ -1,85 +1,30 @@
'use strict';
const Posts = require(__dirname+'/../../db/posts.js')
, Boards = require(__dirname+'/../../db/boards.js')
, Mongo = require(__dirname+'/../../db/db.js')
, banPoster = require(__dirname+'/ban-poster.js')
, deletePosts = require(__dirname+'/delete-post.js')
, spoilerPosts = require(__dirname+'/spoiler-post.js')
, banPoster = require(__dirname+'/banposter.js')
, deletePosts = require(__dirname+'/deletepost.js')
, spoilerPosts = require(__dirname+'/spoilerpost.js')
, stickyPosts = require(__dirname+'/stickyposts.js')
, sagePosts = require(__dirname+'/sageposts.js')
, lockPosts = require(__dirname+'/lockposts.js')
, deletePostsFiles = require(__dirname+'/deletepostsfiles.js')
, reportPosts = require(__dirname+'/report-post.js')
, reportPosts = require(__dirname+'/reportpost.js')
, globalReportPosts = require(__dirname+'/globalreportpost.js')
, dismissReports = require(__dirname+'/dismiss-report.js')
, dismissReports = require(__dirname+'/dismissreport.js')
, dismissGlobalReports = require(__dirname+'/dismissglobalreport.js')
, actionChecker = require(__dirname+'/../../helpers/actionchecker.js')
, checkPerms = require(__dirname+'/../../helpers/hasperms.js')
, remove = require('fs-extra').remove
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, { buildCatalog, buildThread, buildBoardMultiple } = require(__dirname+'/../../build.js');
module.exports = async (req, res, next) => {
const errors = [];
//make sure they checked 1-10 posts
if (!req.body.checkedposts || req.body.checkedposts.length === 0 || req.body.checkedposts.length > 10) {
errors.push('Must select 1-10 posts');
}
//get what type of actions
const { anyPasswords, anyAuthed, anyValid } = actionChecker(req);
//make sure they selected at least 1 action
if (!anyValid) {
errors.push('No actions selected');
}
//check if they have permission to perform the actions
const hasPerms = checkPerms(req, res);
if(!hasPerms && anyAuthed) {
errors.push('No permission');
}
//check that actions are valid
if (req.body.password && req.body.password.length > 50) {
errors.push('Password must be 50 characters or less');
}
if (req.body.report_reason && req.body.report_reason.length > 50) {
errors.push('Report must be 50 characters or less');
}
if (req.body.ban_reason && req.body.ban_reason.length > 50) {
errors.push('Ban reason must be 50 characters or less');
}
if ((req.body.report || req.body.global_report) && (!req.body.report_reason || req.body.report_reason.length === 0)) {
errors.push('Reports must have a reason');
}
if (errors.length > 0) {
return res.status(400).render('message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}/`
})
}
let posts = await Posts.getPosts(req.params.board, req.body.checkedposts, true);
if (!posts || posts.length === 0) {
return res.status(404).render('message', {
'title': 'Not found',
'error': 'Selected posts not found',
'redirect': `/${req.params.board}/`
})
}
//get the ids
const postMongoIds = posts.map(post => Mongo.ObjectId(post._id));
const postMongoIds = res.locals.posts.map(post => Mongo.ObjectId(post._id));
let passwordPostMongoIds = [];
let passwordPosts = [];
if (!hasPerms && anyPasswords) {
if (!res.locals.hasPerms && res.locals.actions.anyPasswords) {
//just to avoid multiple filters and mapping, do it all here
passwordPosts = posts.filter(post => {
passwordPosts = res.locals.posts.filter(post => {
if (post.password != null
&& post.password.length > 0
&& post.password == req.body.password) {
@ -91,11 +36,11 @@ module.exports = async (req, res, next) => {
return res.status(403).render('message', {
'title': 'Forbidden',
'error': 'Password did not match any selected posts',
'redirect': `/${req.params.board}/`
'redirect': `/${req.params.board ? req.params.board+'/' : 'globalmanage.html'}`
});
}
} else {
passwordPosts = posts;
passwordPosts = res.locals.posts;
passwordPostMongoIds = postMongoIds;
}
@ -104,24 +49,22 @@ module.exports = async (req, res, next) => {
const passwordCombinedQuery = {};
let aggregateNeeded = false;
try {
if (hasPerms) {
// if getting global banned, board ban doesnt matter
if (req.body.global_ban) {
const { message, action, query } = await banPoster(req, res, next, null, posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
} else if (req.body.ban) {
const { message, action, query } = await banPoster(req, res, next, req.params.board, posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
// if getting global banned, board ban doesnt matter
if (req.body.global_ban) {
const { message, action, query } = await banPoster(req, res, next, null, res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
} else if (req.body.ban) {
const { message, action, query } = await banPoster(req, res, next, req.params.board, res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
}
if (hasPerms && (req.body.delete_ip_board || req.body.delete_ip_global)) {
const deletePostIps = posts.map(x => x.ip);
if (req.body.delete_ip_board || req.body.delete_ip_global) {
const deletePostIps = res.locals.posts.map(x => x.ip);
let query = {
'ip': {
'$in': deletePostIps
@ -131,7 +74,7 @@ module.exports = async (req, res, next) => {
query['board'] = req.params.board;
}
const deleteIpPosts = await Posts.db.find(query).toArray();
posts = posts.concat(deleteIpPosts);
res.locals.posts = res.locals.posts.concat(deleteIpPosts);
if (deleteIpPosts && deleteIpPosts.length > 0) {
const { message } = await deletePosts(req, res, next, deleteIpPosts, req.params.board);
messages.push(message);
@ -143,8 +86,8 @@ module.exports = async (req, res, next) => {
aggregateNeeded = true;
} else {
// if it was getting deleted, we cant do any of these
if (req.body.delete_file) {
const { message, action, query } = await deletePostsFiles(passwordPosts);
if (req.body.delete_file || req.body.unlink_file) {
const { message, action, query } = await deletePostsFiles(passwordPosts, req.body.unlink_file);
if (action) {
aggregateNeeded = true;
passwordCombinedQuery[action] = { ...passwordCombinedQuery[action], ...query}
@ -157,39 +100,37 @@ module.exports = async (req, res, next) => {
}
messages.push(message);
}
if (hasPerms) {
//lock, sticky, sage
if (req.body.sage) {
const { message, action, query } = sagePosts(posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
//lock, sticky, sage
if (req.body.sage) {
const { message, action, query } = sagePosts(res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
if (req.body.lock) {
const { message, action, query } = lockPosts(posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
messages.push(message);
}
if (req.body.lock) {
const { message, action, query } = lockPosts(res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
if (req.body.sticky) {
const { message, action, query } = stickyPosts(posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
messages.push(message);
}
if (req.body.sticky) {
const { message, action, query } = stickyPosts(res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
}
// cannot report and dismiss at same time
if (req.body.report) {
const { message, action, query } = reportPosts(req, posts);
const { message, action, query } = reportPosts(req, res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
} else if (hasPerms && req.body.dismiss) {
const { message, action, query } = dismissReports(posts);
} else if (req.body.dismiss) {
const { message, action, query } = dismissReports(res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
@ -197,13 +138,13 @@ module.exports = async (req, res, next) => {
}
// cannot report and dismiss at same time
if (req.body.global_report) {
const { message, action, query } = globalReportPosts(req, posts);
const { message, action, query } = globalReportPosts(req, res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
messages.push(message);
} else if (hasPerms && req.body.global_dismiss) {
const { message, action, query } = dismissGlobalReports(posts);
} else if (req.body.global_dismiss) {
const { message, action, query } = dismissGlobalReports(res.locals.posts);
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query}
}
@ -239,8 +180,8 @@ module.exports = async (req, res, next) => {
//get a map of boards to threads affected
const boardThreadMap = {};
const queryOrs = [];
for (let i = 0; i < posts.length; i++) {
const post = posts[i];
for (let i = 0; i < res.locals.posts.length; i++) {
const post = res.locals.posts[i];
if (!boardThreadMap[post.board]) {
boardThreadMap[post.board] = [];
}
@ -263,7 +204,7 @@ module.exports = async (req, res, next) => {
}
//get only posts (so we can use them for thread ids
const postThreadsToUpdate = posts.filter(post => post.thread !== null);
const postThreadsToUpdate = res.locals.posts.filter(post => post.thread !== null);
if (aggregateNeeded) {
//recalculate replies and image counts
await Promise.all(postThreadsToUpdate.map(async (post) => {
@ -296,7 +237,7 @@ module.exports = async (req, res, next) => {
'$or': queryOrs
}).toArray();
//combine it with what we already had
threadsEachBoard = threadsEachBoard.concat(posts.filter(post => post.thread === null))
threadsEachBoard = threadsEachBoard.concat(res.locals.posts.filter(post => post.thread === null))
//get the oldest and newest thread for each board to determine how to delete
const threadBounds = threadsEachBoard.reduce((acc, curr) => {
@ -315,36 +256,45 @@ module.exports = async (req, res, next) => {
//now we need to delete outdated html
//TODO: not do this for reports, handle global actions & move to separate handler + optimize and test
const parallelPromises = []
const boardsWithChanges = Object.keys(threadBounds);
for (let i = 0; i < boardsWithChanges.length; i++) {
const changeBoard = boardsWithChanges[i];
const bounds = threadBounds[changeBoard];
const boardNames = Object.keys(threadBounds);
const buildBoards = {};
const multiBoards = await Boards.db.find({
'_id': {
'$in': boardNames
}
}).toArray();
multiBoards.forEach(board => {
buildBoards[board._id] = board;
})
for (let i = 0; i < boardNames.length; i++) {
const boardName = boardNames[i];
const bounds = threadBounds[boardName];
//always need to refresh catalog
parallelPromises.push(buildCatalog(res.locals.board));
parallelPromises.push(buildCatalog(buildBoards[boardName]));
//rebuild impacted threads
for (let j = 0; j < boardThreadMap[changeBoard].length; j++) {
parallelPromises.push(buildThread(boardThreadMap[changeBoard][j], changeBoard));
for (let j = 0; j < boardThreadMap[boardName].length; j++) {
parallelPromises.push(buildThread(boardThreadMap[boardName][j], buildBoards[boardName]));
}
//refersh any pages affected
const afterPages = Math.ceil((await Posts.getPages(changeBoard)) / 10);
if (beforePages[changeBoard] && beforePages[changeBoard] !== afterPages) {
const afterPages = Math.ceil((await Posts.getPages(boardName)) / 10);
if (beforePages[boardName] && beforePages[boardName] !== afterPages) {
//amount of pages changed, rebuild all pages
parallelPromises.push(buildBoardMultiple(res.locals.board, 1, afterPages));
parallelPromises.push(buildBoardMultiple(buildBoards[boardName], 1, afterPages));
} else {
const threadPageOldest = await Posts.getThreadPage(req.params.board, bounds.oldest);
const threadPageNewest = await Posts.getThreadPage(req.params.board, bounds.newest);
const threadPageOldest = await Posts.getThreadPage(boardName, bounds.oldest);
const threadPageNewest = await Posts.getThreadPage(boardName, bounds.newest);
if (req.body.delete || req.body.delete_ip_board || req.body.delete_ip_global) {
//rebuild current and older pages for deletes
parallelPromises.push(buildBoardMultiple(res.locals.board, threadPageNewest, afterPages));
parallelPromises.push(buildBoardMultiple(buildBoards[boardName], threadPageNewest, afterPages));
} else if (req.body.sticky) { //else if -- if deleting, other actions are not executed/irrelevant
//rebuild current and newer pages for stickies
parallelPromises.push(buildBoardMultiple(res.locals.board, 1, threadPageOldest));
} else if ((hasPerms && (req.body.lock || req.body.sage)) || req.body.spoiler) {
//rebuild current and newer pages
parallelPromises.push(buildBoardMultiple(buildBoards[boardName], 1, threadPageOldest));
} else if (req.body.lock || req.body.sage || req.body.spoiler || req.body.ban || req.body.global_ban || req.body.unlink_file) {
//rebuild inbewteen pages for things that dont cause page/thread movement
//should rebuild only affected pages, but finding the page of all affected
//threads could end up being slower/more resource intensive. this is simpler
//but still avoids rebuilding _some_ pages unnecessarily
parallelPromises.push(buildBoardMultiple(res.locals.board, threadPageNewest, threadPageOldest));
//threads could end up being slower/more resource intensive. this is simpler.
//it avoids rebuilding _some_ but not all pages unnecessarily
parallelPromises.push(buildBoardMultiple(buildBoards[boardName], threadPageNewest, threadPageOldest));
}
}
}
@ -357,7 +307,7 @@ module.exports = async (req, res, next) => {
return res.render('message', {
'title': 'Success',
'messages': messages,
'redirect': `/${req.params.board}/`
'redirect': `/${req.params.board ? req.params.board+'/' : 'globalmanage.html'}`
});
}

@ -9,11 +9,9 @@ module.exports = async (req, res, next) => {
const redirect = `/${req.params.board}/manage.html`
await Promise.all(req.body.checkedbanners.map(async filename => {
remove(`${uploadDirectory}banner/${filename}`);
remove(`${uploadDirectory}banner/${req.params.board}/${filename}`);
}));
// i dont think there is a way to get the number of array items removed with $pullAll
// so i cant return how many banners were deleted
await Boards.removeBanners(req.params.board, req.body.checkedbanners);
return res.render('message', {

@ -1,7 +1,6 @@
'use strict';
const uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, deletePostFiles = require(__dirname+'/../../helpers/files/deletepostfiles.js')
, remove = require('fs-extra').remove
, Mongo = require(__dirname+'/../../db/db.js')
, Posts = require(__dirname+'/../../db/posts.js');
@ -39,7 +38,7 @@ module.exports = async (req, res, next, posts, board) => {
//combine them all into one array, there may be duplicates but it shouldnt matter
const allPosts = posts.concat(threadPosts)
//delete posts from DB
//get all mongoids and delete posts from
const postMongoIds = allPosts.map(post => Mongo.ObjectId(post._id))
const deletedPosts = await Posts.deleteMany(postMongoIds).then(result => result.deletedCount);
@ -49,11 +48,6 @@ module.exports = async (req, res, next, posts, board) => {
fileNames = fileNames.concat(post.files.map(x => x.filename))
})
//delete post files
if (fileNames.length > 0) {
await deletePostFiles(fileNames);
}
//hooray!
return { message:`Deleted ${threads.length} threads and ${deletedPosts-threads.length} posts` };

@ -3,7 +3,7 @@
const remove = require('fs-extra').remove
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
module.exports = async (posts) => {
module.exports = async (posts, unlinkOnly) => {
//get filenames from all the posts
let fileNames = [];
@ -13,25 +13,31 @@ module.exports = async (posts) => {
if (fileNames.length === 0) {
return {
message: 'No files to delete'
message: 'No files found'
}
}
//delete all the files using the filenames
await Promise.all(fileNames.map(async filename => {
//dont question it.
return Promise.all([
remove(`${uploadDirectory}img/${filename}`),
remove(`${uploadDirectory}img/thumb-${filename.split('.')[0]}.png`)
])
}));
if (unlinkOnly) {
return {
message:`Unlinked ${fileNames.length} file(s) across ${posts.length} post(s)`,
action:'$set',
query: {
'files': []
}
};
} else {
//delete all the files using the filenames
await Promise.all(fileNames.map(async filename => {
//dont question it.
return Promise.all([
remove(`${uploadDirectory}img/${filename}`),
remove(`${uploadDirectory}img/thumb-${filename.split('.')[0]}.jpg`)
])
}));
return {
message:`Deleted ${fileNames.length} file(s) from server`,
};
}
return {
message:`Deleted ${fileNames.length} file(s) across ${posts.length} post(s)`,
action:'$set',
query: {
'files': []
}
};
}

@ -1,11 +1,10 @@
'use strict';
const uuidv4 = require('uuid/v4')
, path = require('path')
const path = require('path')
, util = require('util')
, crypto = require('crypto')
, randomBytes = util.promisify(crypto.randomBytes)
, remove = require('fs-extra').remove
, { remove, pathExists } = require('fs-extra')
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, Posts = require(__dirname+'/../../db/posts.js')
, getTripCode = require(__dirname+'/../../helpers/tripcode.js')
@ -13,7 +12,7 @@ const uuidv4 = require('uuid/v4')
, simpleMarkdown = require(__dirname+'/../../helpers/markdown.js')
, sanitize = require('sanitize-html')
, sanitizeOptions = {
allowedTags: [ 'span', 'a', 'em', 'strong' ],
allowedTags: [ 'span', 'a', 'em', 'strong', 'small' ],
allowedAttributes: {
'a': [ 'href', 'class' ],
'span': [ 'class' ]
@ -23,12 +22,13 @@ const uuidv4 = require('uuid/v4')
, permsCheck = require(__dirname+'/../../helpers/hasperms.js')
, imageUpload = require(__dirname+'/../../helpers/files/imageupload.js')
, videoUpload = require(__dirname+'/../../helpers/files/videoupload.js')
, fileCheckMimeType = require(__dirname+'/../../helpers/files/file-check-mime-types.js')
, imageThumbnail = require(__dirname+'/../../helpers/files/image-thumbnail.js')
, imageIdentify = require(__dirname+'/../../helpers/files/image-identify.js')
, videoThumbnail = require(__dirname+'/../../helpers/files/video-thumbnail.js')
, videoIdentify = require(__dirname+'/../../helpers/files/video-identify.js')
, formatSize = require(__dirname+'/../../helpers/files/format-size.js')
, fileCheckMimeType = require(__dirname+'/../../helpers/files/mimetypes.js')
, imageThumbnail = require(__dirname+'/../../helpers/files/imagethumbnail.js')
, imageIdentify = require(__dirname+'/../../helpers/files/imageidentify.js')
, videoThumbnail = require(__dirname+'/../../helpers/files/videothumbnail.js')
, videoIdentify = require(__dirname+'/../../helpers/files/videoidentify.js')
, formatSize = require(__dirname+'/../../helpers/files/formatsize.js')
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
, { buildCatalog, buildThread, buildBoard, buildBoardMultiple } = require(__dirname+'/../../build.js');
module.exports = async (req, res, next, numFiles) => {
@ -42,6 +42,7 @@ module.exports = async (req, res, next, numFiles) => {
if (req.body.thread) {
thread = await Posts.getPost(req.params.board, req.body.thread, true);
if (!thread || thread.thread != null) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'message': 'Thread does not exist.',
@ -51,6 +52,7 @@ module.exports = async (req, res, next, numFiles) => {
salt = thread.salt;
redirect += `thread/${req.body.thread}.html`
if (thread.locked && !hasPerms) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'message': 'Thread Locked',
@ -58,6 +60,7 @@ module.exports = async (req, res, next, numFiles) => {
});
}
if (thread.replyposts >= res.locals.board.settings.replyLimit) { //reply limit
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'message': 'Thread reached reply limit',
@ -66,6 +69,7 @@ module.exports = async (req, res, next, numFiles) => {
}
}
if (numFiles > res.locals.board.settings.maxFiles) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'message': `Too many files. Max files per post is ${res.locals.board.settings.maxFiles}.`,
@ -78,6 +82,7 @@ module.exports = async (req, res, next, numFiles) => {
// check all mime types befoer we try saving anything
for (let i = 0; i < numFiles; i++) {
if (!fileCheckMimeType(req.files.file[i].mimetype, {animatedImage: true, image: true, video: true})) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'message': `Invalid file type for ${req.files.file[i].name}. Mimetype ${req.files.file[i].mimetype} not allowed.`,
@ -88,50 +93,59 @@ module.exports = async (req, res, next, numFiles) => {
// then upload, thumb, get metadata, etc.
for (let i = 0; i < numFiles; i++) {
const file = req.files.file[i];
const uuid = uuidv4();
const filename = uuid + path.extname(file.name);
const filename = file.sha256 + path.extname(file.name);
file.filename = filename; //for error to delete failed files
//get metadata
let processedFile = {
hash: file.sha256,
filename: filename,
originalFilename: file.name,
mimetype: file.mimetype,
size: file.size,
};
//check if already exists
const existsFull = await pathExists(`${uploadDirectory}img/${filename}`);
const existsThumb = await pathExists(`${uploadDirectory}img/thumb-${filename.split('.')[0]}.jpg`);
//handle video/image ffmpeg or graphicsmagick
const mainType = file.mimetype.split('/')[0];
switch (mainType) {
case 'image':
await imageUpload(file, filename, 'img');
const imageData = await imageIdentify(filename, 'img');
const imageData = await imageIdentify(req.files.file[i].tempFilePath, null, true);
processedFile.geometry = imageData.size // object with width and height pixels
processedFile.sizeString = formatSize(processedFile.size) // 123 Ki string
processedFile.geometryString = imageData.Geometry // 123 x 123 string
if (fileCheckMimeType(file.mimetype, {image: true}) //always thumbnail gif/webp
processedFile.hasThumb = !(fileCheckMimeType(file.mimetype, {image: true})
&& processedFile.geometry.height <= 128
&& processedFile.geometry.width <= 128) {
processedFile.hasThumb = false;
} else {
processedFile.hasThumb = true;
&& processedFile.geometry.width <= 128);
if (!existsFull) {
await imageUpload(file, filename, 'img');
}
if (!existsThumb && processedFile.hasThumb) {
await imageThumbnail(filename);
}
break;
case 'video':
//video metadata
await videoUpload(file, filename, 'img');
const videoData = await videoIdentify(filename);
const videoData = await videoIdentify(req.files.file[i].tempFilePath, null, true);
videoData.streams = videoData.streams.filter(stream => stream.width != null); //filter to only video streams or something with a resolution
processedFile.duration = videoData.format.duration;
processedFile.durationString = new Date(videoData.format.duration*1000).toLocaleString('en-US', {hour12:false}).split(' ')[1].replace(/^00:/, '');
processedFile.geometry = {width: videoData.streams[0].coded_width, height: videoData.streams[0].coded_height} // object with width and height pixels
processedFile.sizeString = formatSize(processedFile.size) // 123 Ki string
processedFile.geometryString = `${processedFile.geometry.width}x${processedFile.geometry.height}` // 123 x 123 string
processedFile.hasThumb = true;
await videoThumbnail(filename);
if (!existsFull) {
await videoUpload(file, filename, 'img');
}
if (!existsThumb) {
await videoThumbnail(filename, processedFile.geometry);
}
break;
default:
return next(err);
throw new Error(`invalid file mime type: ${mainType}`); //throw so goes to error handler before next'ing
}
//delete the temp file
@ -161,7 +175,7 @@ module.exports = async (req, res, next, numFiles) => {
salt = (await randomBytes(128)).toString('hex');
}
if (res.locals.board.settings.ids) {
const fullUserIdHash = crypto.createHash('sha256').update(salt + ip + req.params.board).digest('hex');
const fullUserIdHash = crypto.createHash('sha256').update(salt + ip).digest('hex');
userId = fullUserIdHash.substring(fullUserIdHash.length-6);
}
@ -180,7 +194,7 @@ module.exports = async (req, res, next, numFiles) => {
const groups = matches.groups;
//name
if (groups.name) {
name = groups.name
name = groups.name;
}
//tripcode
if (groups.tripcode) {
@ -189,16 +203,19 @@ module.exports = async (req, res, next, numFiles) => {
//capcode
if (groups.capcode && hasPerms) {
// TODO: add proper code for different capcodes
capcode = groups.capcode;
capcode = `## ${groups.capcode}`;
}
}
}
//simple markdown and sanitize
let message = req.body.message;
let quotes = [];
if (message && message.length > 0) {
message = simpleMarkdown(req.params.board, req.body.thread, message);
message = await linkQuotes(req.params.board, message);
const { quotedMessage, threadQuotes } = await linkQuotes(req.params.board, message);
message = quotedMessage;
quotes = threadQuotes;
message = sanitize(message, sanitizeOptions);
}
@ -222,6 +239,7 @@ module.exports = async (req, res, next, numFiles) => {
files,
'reports': [],
'globalreports': [],
quotes
}
if (!req.body.thread) {
@ -247,7 +265,7 @@ module.exports = async (req, res, next, numFiles) => {
if (data.thread) {
//refersh pages
const threadPage = await Posts.getThreadPage(req.params.board, thread);
if (data.email === 'sage') {
if (data.email === 'sage' || thread.sage) {
//refresh the page that the thread is on
parallelPromises.push(buildBoard(res.locals.board, threadPage));
} else {

@ -1,13 +1,12 @@
'use strict';
const uuidv4 = require('uuid/v4')
, path = require('path')
, remove = require('fs-extra').remove
const path = require('path')
, { remove, pathExists, ensureDir } = require('fs-extra')
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, imageUpload = require(__dirname+'/../../helpers/files/imageupload.js')
, fileCheckMimeType = require(__dirname+'/../../helpers/files/file-check-mime-types.js')
, deleteFailedFiles = require(__dirname+'/../../helpers/files/deletefailed.js')
, imageIdentify = require(__dirname+'/../../helpers/files/image-identify.js')
, fileCheckMimeType = require(__dirname+'/../../helpers/files/mimetypes.js')
, imageIdentify = require(__dirname+'/../../helpers/files/imageidentify.js')
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
, Boards = require(__dirname+'/../../db/boards.js')
module.exports = async (req, res, next, numFiles) => {
@ -17,6 +16,7 @@ module.exports = async (req, res, next, numFiles) => {
// check all mime types befoer we try saving anything
for (let i = 0; i < numFiles; i++) {
if (!fileCheckMimeType(req.files.file[i].mimetype, {image: true, animatedImage: true, video: false})) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'message': `Invalid file type for ${req.files.file[i].name}. Mimetype ${req.files.file[i].mimetype} not allowed.`,
@ -26,42 +26,57 @@ module.exports = async (req, res, next, numFiles) => {
}
const filenames = [];
// then upload
for (let i = 0; i < numFiles; i++) {
const file = req.files.file[i];
const uuid = uuidv4();
const filename = uuid + path.extname(file.name);
file.filename = filename; //for error to delete failed files
const filename = file.sha256 + path.extname(file.name);
file.filename = filename;
//check if already exists
const exists = await pathExists(`${uploadDirectory}banner/${req.params.board}/${filename}`);
if (exists) {
await remove(file.tempFilePath);
continue;
}
//add to list after checking it doesnt already exist
filenames.push(filename);
//upload it
await imageUpload(file, filename, 'banner');
const imageData = await imageIdentify(filename, 'banner');
const geometry = imageData.size;
await remove(file.tempFilePath);
//make directory if doesnt exist
await ensureDir(`${uploadDirectory}banner/${req.params.board}/`);
//get metadata from tempfile
const imageData = await imageIdentify(req.files.file[i].tempFilePath, null, true);
let geometry = imageData.size;
if (Array.isArray(geometry)) {
geometry = geometry[0];
}
//make sure its 300x100 banner
if (geometry.width !== 300 || geometry.height !== 100) {
const fileNames = [];
for (let i = 0; i < req.files.file.length; i++) {
remove(req.files.file[i].tempFilePath).catch(e => console.error);
fileNames.push(req.files.file[i].filename);
}
deleteFailedFiles(fileNames, 'banner').catch(e => console.error);
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
'message': `Invalid file ${file.name}. Banners must be 300x100.`,
'redirect': redirect
});
}
//then upload it
await imageUpload(file, filename, `banner/${req.params.board}`);
//and delete the temp file
await remove(file.tempFilePath);
}
await Boards.addBanners(req.params.board, filenames);
//TODO: banners pages
// await buildBanners(res.locals.board);
return res.render('message', {
'title': 'Success',
'message': `Uploaded ${filenames.length} banners.`,
'message': `Uploaded ${filenames.length} new banners.`,
'redirect': redirect
});

@ -8,21 +8,20 @@ module.exports = async (req, res, next) => {
return next();
}
// get all threads
let board;
try {
board = await Boards.findOne(req.query.board);
} catch (err) {
return next(err);
}
if (!board) {
return next();
}
if (board.banners.length > 0) {
const randomBanner = board.banners[Math.floor(Math.random()*board.banners.length)];
return res.redirect(`/banner/${randomBanner}`);
//agregate to get single random item from banners array
const board = await Boards.db.aggregate([
{
'$unwind': '$banners'
},
{
'$sample': {
'size' : 1
}
}
]).toArray().then(res => res[0]);
if (board && board.banners != null) {
return res.redirect(`/banner/${req.query.board}/${board.banners}`);
}
return res.redirect('/img/defaultbanner.png');

517
package-lock.json generated

@ -5,8 +5,8 @@
"requires": true,
"dependencies": {
"@tohru/gm": {
"version": "git+https://github.com/iCrawl/gm.git#70ade5ebee96db0e38d621eb8f9744e5eee159c7",
"from": "git+https://github.com/iCrawl/gm.git",
"version": "git+https://github.com/fatchan/gm.git#5f0ec1a0a262be8e4ac3916886ee55576db2c026",
"from": "git+https://github.com/fatchan/gm.git",
"requires": {
"array-parallel": "^0.1.3",
"array-series": "^0.1.5",
@ -43,18 +43,62 @@
"@types/babel-types": "*"
}
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
"@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
"integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/minimatch": "*",
"@types/node": "*"
}
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"@types/node": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.3.tgz",
"integrity": "sha512-zkOxCS/fA+3SsdA+9Yun0iANxzhQRiNwTvJSr6N95JhuJ/x27z9G2URx1Jpt3zYFfCGUXZGL5UDxt5eyLE7wgw==",
"dev": true
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
"integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.18",
"negotiator": "0.6.1"
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
},
"dependencies": {
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
}
}
},
"accord": {
@ -396,7 +440,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true
"dev": true,
"optional": true
},
"assign-symbols": {
"version": "1.0.0",
@ -582,6 +627,14 @@
}
}
},
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"bcrypt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.6.tgz",
@ -608,20 +661,35 @@
"dev": true
},
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.0.0",
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
"brace-expansion": {
@ -717,9 +785,9 @@
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"cache-base": {
"version": "1.0.1",
@ -783,9 +851,9 @@
}
},
"chokidar": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
"integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz",
"integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==",
"dev": true,
"requires": {
"anymatch": "^2.0.0",
@ -961,6 +1029,7 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"dev": true,
"optional": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@ -1093,9 +1162,12 @@
}
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-security-policy-builder": {
"version": "2.0.0",
@ -1338,11 +1410,12 @@
}
},
"del": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/del/-/del-4.1.0.tgz",
"integrity": "sha512-C4kvKNlYrwXhKxz97BuohF8YoGgQ23Xm9lvoHmgT7JaPGprSEjk3+XFled74Yt/x0ZABUHg2D67covzAPUKx5Q==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
"integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
"dev": true,
"requires": {
"@types/glob": "^7.1.1",
"globby": "^6.1.0",
"is-path-cwd": "^2.0.0",
"is-path-in-cwd": "^2.0.0",
@ -1355,7 +1428,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
"dev": true,
"optional": true
},
"delegates": {
"version": "1.0.0",
@ -1564,9 +1638,9 @@
}
},
"es5-ext": {
"version": "0.10.49",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.49.tgz",
"integrity": "sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg==",
"version": "0.10.50",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz",
"integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==",
"dev": true,
"requires": {
"es6-iterator": "~2.0.3",
@ -1677,51 +1751,56 @@
}
},
"expect-ct": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.1.1.tgz",
"integrity": "sha512-ngXzTfoRGG7fYens3/RMb6yYoVLvLMfmsSllP/mZPxNHgFq41TmPSLF/nLY7fwoclI2vElvAmILFWGUYqdjfCg=="
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz",
"integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g=="
},
"express": {
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
"integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.5",
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.18.3",
"content-disposition": "0.5.2",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.1.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.4",
"qs": "6.5.2",
"range-parser": "~1.2.0",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "~1.4.0",
"type-is": "~1.6.16",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
@ -1857,7 +1936,8 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
"dev": true,
"optional": true
},
"fancy-log": {
"version": "1.3.3",
@ -1886,9 +1966,9 @@
"optional": true
},
"feature-policy": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.2.0.tgz",
"integrity": "sha512-2hGrlv6efG4hscYVZeaYjpzpT6I2OZgYqE2yDUzeAcKj2D1SH0AsEzqJNXzdoglEddcIXQQYop3lD97XpG75Jw=="
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz",
"integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ=="
},
"fill-range": {
"version": "4.0.0",
@ -1914,24 +1994,17 @@
}
},
"finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"statuses": "~1.4.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"dependencies": {
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
}
}
},
"find-up": {
@ -2069,9 +2142,9 @@
}
},
"frameguard": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.0.0.tgz",
"integrity": "sha1-e8rUae57lukdEs6zlZx4I1qScuk="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.1.0.tgz",
"integrity": "sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g=="
},
"fresh": {
"version": "0.5.2",
@ -2117,9 +2190,9 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.8.tgz",
"integrity": "sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==",
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
"integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
"dev": true,
"optional": true,
"requires": {
@ -2136,7 +2209,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -2157,12 +2231,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -2177,17 +2253,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -2304,7 +2383,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -2316,6 +2396,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -2330,6 +2411,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -2337,12 +2419,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -2361,6 +2445,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -2441,7 +2526,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -2453,6 +2539,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -2538,7 +2625,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -2574,6 +2662,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -2593,6 +2682,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -2636,12 +2726,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@ -2839,9 +2931,9 @@
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
},
"gulp": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.1.tgz",
"integrity": "sha512-yDVtVunxrAdsk7rIV/b7lVSBifPN1Eqe6wTjsESGrFcL+MEVzaaeNTkpUuGTUptloSOU+8oJm/lBJbgPV+tMAw==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
"integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==",
"dev": true,
"requires": {
"glob-watcher": "^5.0.3",
@ -3114,24 +3206,24 @@
}
},
"helmet": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-3.16.0.tgz",
"integrity": "sha512-rsTKRogc5OYGlvSHuq5QsmOsOzF6uDoMqpfh+Np8r23+QxDq+SUx90Rf8HyIKQVl7H6NswZEwfcykinbAeZ6UQ==",
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-3.18.0.tgz",
"integrity": "sha512-TsKlGE5UVkV0NiQ4PllV9EVfZklPjyzcMEMjWlyI/8S6epqgRT+4s4GHVgc25x0TixsKvp3L7c91HQQt5l0+QA==",
"requires": {
"depd": "2.0.0",
"dns-prefetch-control": "0.1.0",
"dont-sniff-mimetype": "1.0.0",
"expect-ct": "0.1.1",
"feature-policy": "0.2.0",
"frameguard": "3.0.0",
"expect-ct": "0.2.0",
"feature-policy": "0.3.0",
"frameguard": "3.1.0",
"helmet-crossdomain": "0.3.0",
"helmet-csp": "2.7.1",
"hide-powered-by": "1.0.0",
"hpkp": "2.0.0",
"hsts": "2.2.0",
"ienoopen": "1.1.0",
"nocache": "2.0.0",
"referrer-policy": "1.1.0",
"nocache": "2.1.0",
"referrer-policy": "1.2.0",
"x-xss-protection": "1.1.0"
},
"dependencies": {
@ -3212,14 +3304,15 @@
}
},
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"http-signature": {
@ -3432,27 +3525,27 @@
}
},
"is-path-cwd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.0.0.tgz",
"integrity": "sha512-m5dHHzpOXEiv18JEORttBO64UgTEypx99vCxQLjbBvGhOJxnTNglYoFXxwo6AbsQb79sqqycQEHv2hWkHZAijA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.1.0.tgz",
"integrity": "sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw==",
"dev": true
},
"is-path-in-cwd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.0.0.tgz",
"integrity": "sha512-6Vz5Gc9s/sDA3JBVu0FzWufm8xaBsqy1zn8Q6gmvGP6nSDMw78aS4poBNeatWjaRpTpxxLn1WOndAiOlk+qY8A==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
"integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
"dev": true,
"requires": {
"is-path-inside": "^1.0.0"
"is-path-inside": "^2.1.0"
}
},
"is-path-inside": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
"integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
"integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
"dev": true,
"requires": {
"path-is-inside": "^1.0.1"
"path-is-inside": "^1.0.2"
}
},
"is-plain-object": {
@ -3552,7 +3645,8 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true
"dev": true,
"optional": true
},
"json-schema": {
"version": "0.2.3",
@ -3953,17 +4047,23 @@
"mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
"dev": true,
"optional": true
},
"mime-db": {
"version": "1.35.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz",
"integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg=="
"integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==",
"dev": true,
"optional": true
},
"mime-types": {
"version": "2.1.19",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz",
"integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==",
"dev": true,
"optional": true,
"requires": {
"mime-db": "~1.35.0"
}
@ -4035,11 +4135,11 @@
}
},
"mongodb": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.2.3.tgz",
"integrity": "sha512-jw8UyPsq4QleZ9z+t/pIVy3L++51vKdaJ2Q/XXeYxk/3cnKioAH8H6f5tkkDivrQL4PUgUOHe9uZzkpRFH1XtQ==",
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.2.6.tgz",
"integrity": "sha512-qnHc4tjEkHKemuzBq9R7ycYnhFE0Dlpt6+n6suoZp2DcDdqviQ+teloJU24fsOw/PLmr75yGk4mRx/YabjDQEQ==",
"requires": {
"mongodb-core": "^3.2.3",
"mongodb-core": "3.2.6",
"safe-buffer": "^5.1.2"
},
"dependencies": {
@ -4049,9 +4149,9 @@
"integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg=="
},
"mongodb-core": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.2.3.tgz",
"integrity": "sha512-UyI0rmvPPkjOJV8XGWa9VCTq7R4hBVipimhnAXeSXnuAPjuTqbyfA5Ec9RcYJ1Hhu+ISnc8bJ1KfGZd4ZkYARQ==",
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.2.6.tgz",
"integrity": "sha512-i+XRVjur9D0ywGF7cFebOUnALnbvMHajdNhhl3TQuopW6QDE655G8CpPeERbqSqfa3rOKEUo08lENDIiBIuAvQ==",
"requires": {
"bson": "^1.1.1",
"require_optional": "^1.0.1",
@ -4070,6 +4170,18 @@
"require_optional": "~1.0.0"
}
},
"morgan": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
"integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==",
"requires": {
"basic-auth": "~2.0.0",
"debug": "2.6.9",
"depd": "~1.1.2",
"on-finished": "~2.3.0",
"on-headers": "~1.0.1"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -4139,9 +4251,9 @@
}
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"next-tick": {
"version": "1.0.0",
@ -4150,9 +4262,9 @@
"dev": true
},
"nocache": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz",
"integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz",
"integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q=="
},
"node-pre-gyp": {
"version": "0.12.0",
@ -4193,9 +4305,9 @@
},
"dependencies": {
"resolve": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
"integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",
"integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
@ -4623,9 +4735,9 @@
"dev": true
},
"postcss": {
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz",
"integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==",
"version": "7.0.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.16.tgz",
"integrity": "sha512-MOo8zNSlIqh22Uaa3drkdIAgUGEL+AD1ESiSdmElLUmE2uVDo1QloiT/IfW9qRw8Gw+Y/w69UVMGwbufMSftxA==",
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
@ -4839,7 +4951,9 @@
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true,
"optional": true
},
"random-bytes": {
"version": "1.0.0",
@ -4847,19 +4961,29 @@
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
},
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
"rc": {
@ -4958,9 +5082,9 @@
}
},
"referrer-policy": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz",
"integrity": "sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz",
"integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA=="
},
"regenerator-runtime": {
"version": "0.11.1",
@ -5167,9 +5291,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sanitize-html": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.20.0.tgz",
"integrity": "sha512-BpxXkBoAG+uKCHjoXFmox6kCSYpnulABoGcZ/R3QyY9ndXbIM5S94eOr1IqnzTG8TnbmXaxWoDDzKC5eJv7fEQ==",
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.20.1.tgz",
"integrity": "sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==",
"requires": {
"chalk": "^2.4.1",
"htmlparser2": "^3.10.0",
@ -5184,9 +5308,9 @@
}
},
"saslprep": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz",
"integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"optional": true,
"requires": {
"sparse-bitfield": "^3.0.3"
@ -5212,9 +5336,9 @@
}
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@ -5223,30 +5347,35 @@
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"set-blocking": {
@ -5278,9 +5407,9 @@
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"signal-exit": {
"version": "3.0.2",
@ -5815,15 +5944,31 @@
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
"dev": true,
"optional": true
},
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
"integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.18"
"mime-types": "~2.1.24"
},
"dependencies": {
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
}
}
},
"typedarray": {
@ -6039,9 +6184,9 @@
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"v8flags": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.2.tgz",
"integrity": "sha512-MtivA7GF24yMPte9Rp/BWGCYQNaUj86zeYxV/x2RRJMKagImbbv3u8iJC57lNhWLPcGLJmHcHmFWkNsplbbLWw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz",
"integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==",
"dev": true,
"requires": {
"homedir-polyfill": "^1.0.1"

@ -4,28 +4,29 @@
"description": "",
"main": "server.js",
"dependencies": {
"@tohru/gm": "git+https://github.com/iCrawl/gm.git",
"@tohru/gm": "git+https://github.com/fatchan/gm.git",
"bcrypt": "^3.0.6",
"body-parser": "^1.18.3",
"body-parser": "^1.19.0",
"connect-mongo": "^2.0.3",
"cookie-parser": "^1.4.4",
"csurf": "^1.10.0",
"express": "^4.16.4",
"express": "^4.17.1",
"express-fileupload": "^1.1.4",
"express-session": "^1.16.1",
"fluent-ffmpeg": "^2.1.2",
"fs": "0.0.1-security",
"fs-extra": "^7.0.1",
"helmet": "^3.16.0",
"mongodb": "^3.2.3",
"helmet": "^3.18.0",
"mongodb": "^3.2.6",
"morgan": "^1.9.1",
"path": "^0.12.7",
"pug": "^2.0.3",
"sanitize-html": "^1.20.0",
"sanitize-html": "^1.20.1",
"uuid": "^3.3.2"
},
"devDependencies": {
"del": "^4.1.0",
"gulp": "^4.0.1",
"del": "^4.1.1",
"gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0",
"gulp-concat": "^2.6.1",
"gulp-less": "^4.0.1",

@ -3,41 +3,29 @@
process.on('uncaughtException', console.error);
process.on('unhandledRejection', console.error);
const express = require('express')
, session = require('express-session')
const express = require('express')
, session = require('express-session')
, MongoStore = require('connect-mongo')(session)
, path = require('path')
, app = express()
, helmet = require('helmet')
, path = require('path')
, app = express()
, bodyParser = require('body-parser')
, cookieParser = require('cookie-parser')
, configs = require(__dirname+'/configs/main.json')
, Mongo = require(__dirname+'/db/db.js')
, upload = require('express-fileupload');
, Mongo = require(__dirname+'/db/db.js');
(async () => {
// let db connect
await Mongo.connect();
// parse forms and allow file uploads
// parse forms
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
app.use(upload({
createParentPath: true,
safeFileNames: true,
preserveExtension: 4,
limits: {
fileSize: 10 * 1024 * 1024,
files: 3
},
abortOnLimit: true,
useTempFiles: true,
tempFileDir: path.join(__dirname+'/tmp/')
}));
//parse cookies
app.use(cookieParser());
// session store
app.set('trust proxy', 1);
app.use(session({
secret: configs.sessionSecret,
store: new MongoStore({ db: Mongo.client.db('sessions') }),
@ -49,10 +37,9 @@ const express = require('express')
sameSite: 'lax',
}
}));
app.use(cookieParser());
// csurf and helmet
app.use(helmet());
//trust proxy for nginx
app.set('trust proxy', 1);
//referer header check
app.use((req, res, next) => {
@ -97,11 +84,11 @@ const express = require('express')
// listen
const server = app.listen(configs.port, '127.0.0.1', () => {
console.log(`Listening on port ${configs.port}`);
console.log(`listening on port ${configs.port}`);
//let PM2 know that this is ready (for graceful reloads)
if (typeof process.send === 'function') { //make sure we are a child process
console.info('Sending ready signal to PM2')
console.info('sending ready signal to PM2')
process.send('ready');
}
@ -109,18 +96,20 @@ const express = require('express')
process.on('SIGINT', () => {
console.info('SIGINT signal received.')
console.info('SIGINT signal received')
// Stops the server from accepting new connections and finishes existing connections.
server.close((err) => {
// if error, log and exit with error (1 code)
console.info('closing http server')
if (err) {
console.error(err);
process.exit(1);
}
// close database connection
console.info('closing db connection')
Mongo.client.close();
// now close without error

@ -6,11 +6,11 @@ details.toggle-label
input.post-check(type='checkbox', name='delete' value=1)
| Delete
label
input.post-check(type='checkbox', name='delete_file' value=1)
| Delete File Only
input.post-check(type='checkbox', name='unlink_file' value=1)
| Unlink Files
label
input.post-check(type='checkbox', name='spoiler' value=1)
| Spoiler Images
| Spoiler Files
label
input#password(type='text', name='password', placeholder='post password' autocomplete='off')
label
@ -29,6 +29,9 @@ details.toggle-label
label
input.post-check(type='checkbox', name='delete_ip_global' value=1)
| Delete from IP globally
label
input.post-check(type='checkbox', name='delete_file' value=1)
| Delete Files
label
input.post-check(type='checkbox', name='sticky' value=1)
| Sticky

@ -7,10 +7,10 @@ details.toggle-label
| Delete
label
input.post-check(type='checkbox', name='delete_file' value=1)
| Delete File Only
| Delete Files
label
input.post-check(type='checkbox', name='spoiler' value=1)
| Spoiler Images
| Spoiler Files
label
input.post-check(type='checkbox', name='delete_ip_global' value=1)
| Delete from IP globally

@ -7,10 +7,10 @@ details.toggle-label
| Delete
label
input.post-check(type='checkbox', name='delete_file' value=1)
| Delete File Only
| Delete Files
label
input.post-check(type='checkbox', name='spoiler' value=1)
| Spoiler Images
| Spoiler Files
label
input.post-check(type='checkbox', name='global_report' value=1)
| Global Report

@ -1,2 +1,2 @@
.footer
a(href='https://github.com/fatchan/jschan/') not lynxchan™
a(href='https://github.com/fatchan/jschan/') open source

@ -1,47 +1,46 @@
section.form-wrapper.flex-center
details.toggle-label
summary.toggle-summary Show Post Form
form.form-post(action=`/forms/board/${board._id}/post`, enctype='multipart/form-data', method='POST')
//input(type='hidden' name='_csrf' value=csrf)
input(type='hidden' name='thread' value=thread != null ? thread.postId : null)
unless board.settings.forceAnon
section.postform-row
.postform-label Name
input#name(type='text', name='name', placeholder=board.defaultName autocomplete='off' maxlength='50')
a.toggle-summary(href='#postform') Show Post Form
form.form-post#postform(action=`/forms/board/${board._id}/post`, enctype='multipart/form-data', method='POST')
input(type='hidden' name='thread' value=thread != null ? thread.postId : null)
unless board.settings.forceAnon
section.postform-row
.postform-label Name
input#name(type='text', name='name', placeholder=board.defaultName autocomplete='off' maxlength='50')
a.close.postform-style.ml-1(href='#!') X
section.postform-row
.postform-label Subject
input#title(type='text', name='subject', autocomplete='off' maxlength='50')
section.postform-row
.postform-label Email
input#name(type='text', name='email', autocomplete='off' maxlength='50')
else
section.postform-row
.postform-label Sage
label.postform-style.ph-5
input#spoiler(type='checkbox', name='email', value='sage')
| Sage
a.close.postform-style.ml-1(href='#!') X
if !thread
section.postform-row
.postform-label Subject
input#title(type='text', name='subject', autocomplete='off' maxlength='50')
section.postform-row
.postform-label Email
input#name(type='text', name='email', autocomplete='off' maxlength='50')
else
section.postform-row
.postform-label Sage
label.postform-style.ph-5
input#spoiler(type='checkbox', name='email', value='sage')
| Sage
if !thread
section.postform-row
.postform-label Subject
input#title(type='text', name='subject', autocomplete='off' maxlength='50')
section.postform-row
.postform-label Message
textarea#message(name='message', rows='5', autocomplete='off' maxlength='2000')
if board.settings.maxFiles !== 0
section.postform-row
.postform-label Message
textarea#message(name='message', rows='5', autocomplete='off' maxlength='2000')
if board.settings.maxFiles !== 0
section.postform-row
.postform-label Files
input#file(type='file', name='file' multiple='multiple')
label.postform-style.ph-5.ml-1
input#spoiler(type='checkbox', name='spoiler', value='true')
| Spoiler
.postform-label Files
input#file(type='file', name='file' multiple)
label.postform-style.ph-5.ml-1
input#spoiler(type='checkbox', name='spoiler', value='true')
| Spoiler
section.postform-row
.postform-label Password
input#password(type='password', name='password', autocomplete='off' placeholder='password for deleting post later' maxlength='50')
if board.settings.captcha
section.postform-row
.postform-label Password
input#password(type='password', name='password', autocomplete='off' placeholder='password for deleting post later' maxlength='50')
if board.settings.captcha
section.postform-row
.postform-label Captcha
.postform-col
img.captcha(src='/captcha' width=200 height=80)
input#captcha(type='text', name='captcha', autocomplete='off' placeholder='captcha text' maxlength='6')
input(type='submit', value=`New ${threads ? 'Thread' : 'Reply'}`)
//.mode Posting mode: #{threads ? 'Thread' : 'Reply'}
.postform-label Captcha
.postform-col
img.captcha(src='/captcha' width=200 height=80)
input#captcha(type='text', name='captcha', autocomplete='off' placeholder='captcha text' maxlength='6')
input(type='submit', value=`New ${threads ? 'Thread' : 'Reply'}`)

@ -1,30 +1,28 @@
mixin catalogtile(board, post, truncate)
article(class='catalog-tile')
- const postURL = `/${board._id}/thread/${post.postId}.html#${post.postId}`
a.catalog-tile-button(href=postURL) Open Thread
if post.subject
span: a.no-decoration.post-subject(href=postURL) #{post.subject}
.catalog-tile-content
if post.files.length > 0
.post-file-src
a(href=postURL)
if post.spoiler
object(data='/img/spoiler.png' width='64' height='64')
else
object.catalog-thumb(data=`/img/thumb-${post.files[0].filename.split('.')[0]}.jpg` width='64' height='64')
header.post-info
if post.sticky
img(src='/img/sticky.svg' height='12')
if post.saged
img(src='/img/saged.svg' height='12')
if post.locked
img(src='/img/locked.svg' height='12')
if post.files.length > 0
.post-file-src
a(href=postURL)
if post.spoiler
object(data='/img/spoiler.png' width='64' height='64')
else
object.catalog-thumb(data=`/img/thumb-${post.files[0].filename.split('.')[0]}.jpg` width='64' height='64')
header.post-info
if post.sticky
img(src='/img/sticky.svg' height='12')
|
span: a(href=postURL) No.#{post.postId}
if post.saged
img(src='/img/saged.svg' height='12')
|
span Replies: #{post.replyposts}
if post.locked
img(src='/img/locked.svg' height='12')
|
span Images: #{post.replyfiles}
if post.message
br
blockquote.no-m-p.post-message !{post.message}
span: a.no-decoration.post-subject(href=postURL) #{post.subject || 'No Subject'}
br
span Replies: #{post.replyposts}
|
span Files: #{post.replyfiles}
if post.message
br
pre.no-m-p.post-message !{post.message}

@ -10,11 +10,13 @@ mixin post(post, truncate, manage=false, globalmanage=false)
if !post.thread
if post.sticky
img(src='/img/sticky.svg' height='12')
|
if post.saged
img(src='/img/saged.svg' height='12')
|
if post.locked
img(src='/img/locked.svg' height='12')
|
|
if post.subject
span.post-subject #{post.subject}
|
@ -36,13 +38,16 @@ mixin post(post, truncate, manage=false, globalmanage=false)
span.user-id(style=`background: #${post.userId}`) #{post.userId}
|
span: a(href=postURL) No.#{post.postId}
if !post.thread
|
span: a(href=`/${post.board}/thread/${post.thread || post.postId}.html#postform`) [Reply]
.post-data
if post.files.length > 0
.post-files
each file in post.files
.post-file
span.post-file-info
span: a(href='/img/'+file.filename title=file.originalFilename download=file.originalFilename) #{file.originalFilename}
span: a(href='/img/'+file.filename title=file.originalFilename download=file.originalFilename) #{post.spoiler ? 'Spoiler File' : file.originalFilename}
br
span
| (#{file.sizeString}, #{file.geometryString}
@ -55,8 +60,10 @@ mixin post(post, truncate, manage=false, globalmanage=false)
object.file-thumb(data='/img/spoiler.png' width='128' height='128')
else if file.hasThumb
object.file-thumb(data=`/img/thumb-${file.filename.split('.')[0]}.jpg`)
img(src='/img/deleted.png')
else
object.file-thumb(data=`/img/${file.filename}`)
img(src='/img/deleted.png')
if post.message
if truncate
-
@ -64,20 +71,27 @@ mixin post(post, truncate, manage=false, globalmanage=false)
const messageLines = splitPost.length;
let truncatedMessage = post.message;
let truncated = false;
if (messageLines > 10 || post.message.length > 1000) {
if (messageLines > 10) {
truncatedMessage = splitPost.slice(0, 10).join('\n');
truncated = true;
} else if (post.message.length > 1000) {
truncatedMessage = `${post.message.substring(0,1000)}...`;
truncated = true;
}
blockquote.post-message !{truncatedMessage}
pre.post-message !{truncatedMessage}
if truncated
blockquote.left.clear-both Message too long. #[a(href=postURL) View the full text]
blockquote Message too long. #[a(href=postURL) View the full text]
else
blockquote.post-message !{post.message}
pre.post-message !{post.message}
if post.banmessage
blockquote.left.clear-both.banmessage USER WAS BANNED FOR THIS POST (#{post.banmessage})
blockquote.banmessage USER WAS BANNED FOR THIS POST (#{post.banmessage})
if post.omittedposts || post.omittedimages
blockquote.left.clear-both #{post.omittedposts} post(s)#{post.omittedimages > 0 ? ' and '+post.omittedimages+' image(s)' : ''} omitted. #[a(href=postURL) View the full thread]
blockquote #{post.omittedposts} post(s)#{post.omittedimages > 0 ? ' and '+post.omittedimages+' image(s)' : ''} omitted. #[a(href=postURL) View the full thread]
if post.backlinks && post.backlinks.length > 0
.replies Replies:
each backlink in post.backlinks
a.quote(href=`/${post.board}/thread/${post.thread || post.postId}.html#${backlink}`) &gt;&gt;#{backlink}
|
if manage === true
each report in post.reports
.reports.post-container

@ -7,6 +7,8 @@ block head
block content
include ../includes/boardheader.pug
br
include ../includes/postform.pug
br
nav.pages#top
a(href='#bottom') [Bottom]
|

@ -72,7 +72,7 @@ block content
each banner in board.banners
label.banner-check
input(type='checkbox' name='checkedbanners[]' value=banner)
object.board-banner(data=`/banner/${banner}` width='300' height='100')
object.board-banner(data=`/banner/${board._id}/${banner}` width='300' height='100')
input(type='submit', value='delete')
hr(size=1)
h4.no-m-p Reports:

@ -22,7 +22,6 @@ block content
a(href=`/${board._id}/catalog.html`) [Catalog]
hr(size=1)
form(action=`/forms/board/${board._id}/actions` method='POST' enctype='application/x-www-form-urlencoded')
input(type='hidden' name='_csrf' value=csrf)
section.thread
+post(thread)
for post in thread.replies

@ -42,7 +42,7 @@ const Mongo = require(__dirname+'/db/db.js')
moderators: [],
banners: [],
settings: {
captcha: false,
captcha: true,
forceAnon: true,
ids: true,
threadLimit: 100,
@ -63,7 +63,7 @@ const Mongo = require(__dirname+'/db/db.js')
moderators: [],
banners: [],
settings: {
captcha: true,
captcha: false,
forceAnon: false,
ids: false,
threadLimit: 100,

Loading…
Cancel
Save