diff --git a/build.js b/build.js index 303dc3c5..baef993b 100644 --- a/build.js +++ b/build.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`); diff --git a/controllers/forms.js b/controllers/forms.js index cf31baa2..4a663c01 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -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` }); }); diff --git a/db/boards.js b/db/boards.js index 0e225f46..e0ec5baf 100644 --- a/db/boards.js +++ b/db/boards.js @@ -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 }); diff --git a/db/posts.js b/db/posts.js index 15b9f787..83d06d44 100644 --- a/db/posts.js +++ b/db/posts.js @@ -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); diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index 891b216f..39d6258f 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -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; } } diff --git a/gulp/res/img/deleted.png b/gulp/res/img/deleted.png new file mode 100644 index 00000000..848c79c0 Binary files /dev/null and b/gulp/res/img/deleted.png differ diff --git a/helpers/actionchecker.js b/helpers/actionchecker.js index 10ab2486..ab43e884 100644 --- a/helpers/actionchecker.js +++ b/helpers/actionchecker.js @@ -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) { diff --git a/helpers/captchaverify.js b/helpers/captchaverify.js index 21ee4de9..ec939409 100644 --- a/helpers/captchaverify.js +++ b/helpers/captchaverify.js @@ -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(); } diff --git a/helpers/files/deletetempfiles.js b/helpers/files/deletetempfiles.js new file mode 100644 index 00000000..6a383e10 --- /dev/null +++ b/helpers/files/deletetempfiles.js @@ -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); + })); + } + +} diff --git a/helpers/files/format-size.js b/helpers/files/formatsize.js similarity index 100% rename from helpers/files/format-size.js rename to helpers/files/formatsize.js diff --git a/helpers/files/image-identify.js b/helpers/files/imageidentify.js similarity index 72% rename from helpers/files/image-identify.js rename to helpers/files/imageidentify.js index 709c118d..f42789b3 100644 --- a/helpers/files/image-identify.js +++ b/helpers/files/imageidentify.js @@ -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); diff --git a/helpers/files/image-thumbnail.js b/helpers/files/imagethumbnail.js similarity index 100% rename from helpers/files/image-thumbnail.js rename to helpers/files/imagethumbnail.js diff --git a/helpers/files/file-check-mime-types.js b/helpers/files/mimetypes.js similarity index 95% rename from helpers/files/file-check-mime-types.js rename to helpers/files/mimetypes.js index 2f1eedad..55aef447 100644 --- a/helpers/files/file-check-mime-types.js +++ b/helpers/files/mimetypes.js @@ -13,6 +13,7 @@ const animatedImageMimeTypes = new Set([ ]); const videoMimeTypes = new Set([ + 'video/quicktime', 'video/mp4', 'video/webm', ]); diff --git a/helpers/files/video-identify.js b/helpers/files/videoidentify.js similarity index 65% rename from helpers/files/video-identify.js rename to helpers/files/videoidentify.js index dd98a735..a4ebbe57 100644 --- a/helpers/files/video-identify.js +++ b/helpers/files/videoidentify.js @@ -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) } diff --git a/helpers/files/video-thumbnail.js b/helpers/files/videothumbnail.js similarity index 68% rename from helpers/files/video-thumbnail.js rename to helpers/files/videothumbnail.js index 28d12f84..c05e7fc9 100644 --- a/helpers/files/video-thumbnail.js +++ b/helpers/files/videothumbnail.js @@ -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 }); }); diff --git a/helpers/haspermsmiddleware.js b/helpers/haspermsmiddleware.js index e928c663..8f15cbff 100644 --- a/helpers/haspermsmiddleware.js +++ b/helpers/haspermsmiddleware.js @@ -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', diff --git a/helpers/id-contrast.js b/helpers/id-contrast.js deleted file mode 100644 index 50232bbb..00000000 --- a/helpers/id-contrast.js +++ /dev/null @@ -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; -} diff --git a/helpers/paramconverter.js b/helpers/paramconverter.js index 6ff9035b..3f31c91b 100644 --- a/helpers/paramconverter.js +++ b/helpers/paramconverter.js @@ -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; + } } } diff --git a/helpers/quotes.js b/helpers/quotes.js index 46fe8d46..fcd1fac9 100644 --- a/helpers/quotes.js +++ b/helpers/quotes.js @@ -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 `>>${quotenum}`; + threadQuotes.push(quotenum) + return `>>${quotenum}${postThreadIdMap[board][quotenum] === quotenum ? ' (OP) ' : ''}`; } return match; }); @@ -94,6 +96,6 @@ module.exports = async (board, text) => { }); } - return text; + return { quotedMessage: text, threadQuotes: [...new Set(threadQuotes)] }; } diff --git a/models/forms/actionhandler.js b/models/forms/actionhandler.js index d876ed0d..d8a7c41b 100644 --- a/models/forms/actionhandler.js +++ b/models/forms/actionhandler.js @@ -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'}` }); } diff --git a/models/forms/ban-poster.js b/models/forms/banposter.js similarity index 100% rename from models/forms/ban-poster.js rename to models/forms/banposter.js diff --git a/models/forms/deletebanners.js b/models/forms/deletebanners.js index f8015408..2d4ba234 100644 --- a/models/forms/deletebanners.js +++ b/models/forms/deletebanners.js @@ -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', { diff --git a/models/forms/delete-post.js b/models/forms/deletepost.js similarity index 91% rename from models/forms/delete-post.js rename to models/forms/deletepost.js index eee05592..04755730 100644 --- a/models/forms/delete-post.js +++ b/models/forms/deletepost.js @@ -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` }; diff --git a/models/forms/deletepostsfiles.js b/models/forms/deletepostsfiles.js index b1d80988..beeb509d 100644 --- a/models/forms/deletepostsfiles.js +++ b/models/forms/deletepostsfiles.js @@ -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': [] - } - }; } diff --git a/models/forms/dismiss-report.js b/models/forms/dismissreport.js similarity index 100% rename from models/forms/dismiss-report.js rename to models/forms/dismissreport.js diff --git a/models/forms/make-post.js b/models/forms/makepost.js similarity index 79% rename from models/forms/make-post.js rename to models/forms/makepost.js index ca8e9d03..3aa9fde3 100644 --- a/models/forms/make-post.js +++ b/models/forms/makepost.js @@ -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 { diff --git a/models/forms/report-post.js b/models/forms/reportpost.js similarity index 100% rename from models/forms/report-post.js rename to models/forms/reportpost.js diff --git a/models/forms/spoiler-post.js b/models/forms/spoilerpost.js similarity index 100% rename from models/forms/spoiler-post.js rename to models/forms/spoilerpost.js diff --git a/models/forms/uploadbanners.js b/models/forms/uploadbanners.js index 980b31ba..47a76186 100644 --- a/models/forms/uploadbanners.js +++ b/models/forms/uploadbanners.js @@ -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 }); diff --git a/models/pages/banners.js b/models/pages/banners.js index dbd6b309..33c36290 100644 --- a/models/pages/banners.js +++ b/models/pages/banners.js @@ -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'); diff --git a/package-lock.json b/package-lock.json index ab716328..7e7fcfe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 67d04b20..2ffd0f0d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server.js b/server.js index eb8f1412..f0e20568 100644 --- a/server.js +++ b/server.js @@ -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 diff --git a/views/includes/actionfooter.pug b/views/includes/actionfooter.pug index 5e938d6a..54c3997c 100644 --- a/views/includes/actionfooter.pug +++ b/views/includes/actionfooter.pug @@ -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 diff --git a/views/includes/actionfooter_globalmanage.pug b/views/includes/actionfooter_globalmanage.pug index 90d9b47a..3d70d36d 100644 --- a/views/includes/actionfooter_globalmanage.pug +++ b/views/includes/actionfooter_globalmanage.pug @@ -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 diff --git a/views/includes/actionfooter_manage.pug b/views/includes/actionfooter_manage.pug index 636e708c..d36f9770 100644 --- a/views/includes/actionfooter_manage.pug +++ b/views/includes/actionfooter_manage.pug @@ -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 diff --git a/views/includes/footer.pug b/views/includes/footer.pug index 25ab0523..2f1c38a3 100644 --- a/views/includes/footer.pug +++ b/views/includes/footer.pug @@ -1,2 +1,2 @@ .footer - a(href='https://github.com/fatchan/jschan/') not lynxchan™ + a(href='https://github.com/fatchan/jschan/') open source diff --git a/views/includes/postform.pug b/views/includes/postform.pug index 38107809..496f2f28 100644 --- a/views/includes/postform.pug +++ b/views/includes/postform.pug @@ -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'}`) diff --git a/views/mixins/catalogtile.pug b/views/mixins/catalogtile.pug index 24155d6a..0c59d1a8 100644 --- a/views/mixins/catalogtile.pug +++ b/views/mixins/catalogtile.pug @@ -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} diff --git a/views/mixins/post.pug b/views/mixins/post.pug index a8ae0fd3..f4ab7a13 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -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}`) >>#{backlink} + | if manage === true each report in post.reports .reports.post-container diff --git a/views/pages/catalog.pug b/views/pages/catalog.pug index 945732bb..be09ee56 100644 --- a/views/pages/catalog.pug +++ b/views/pages/catalog.pug @@ -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] | diff --git a/views/pages/manage.pug b/views/pages/manage.pug index 43417353..116a3b97 100644 --- a/views/pages/manage.pug +++ b/views/pages/manage.pug @@ -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: diff --git a/views/pages/thread.pug b/views/pages/thread.pug index 035c26c3..998ed669 100644 --- a/views/pages/thread.pug +++ b/views/pages/thread.pug @@ -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 diff --git a/wipe.js b/wipe.js index e35fe808..2037bdb2 100644 --- a/wipe.js +++ b/wipe.js @@ -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,