diff --git a/build.js b/build.js new file mode 100644 index 00000000..303dc3c5 --- /dev/null +++ b/build.js @@ -0,0 +1,97 @@ +'use strict'; + +const Posts = require(__dirname+'/db/posts.js') + , Boards = require(__dirname+'/db/boards.js') + , uploadDirectory = require(__dirname+'/helpers/uploadDirectory.js') + , render = require(__dirname+'/helpers/render.js'); + +module.exports = { + + buildCatalog: async (board) => { + const threads = await Posts.getCatalog(board._id); + return render(`${board._id}/catalog.html`, 'catalog.pug', { + board, + threads + }); + }, + + buildThread: async (threadId, board) => { +//console.log('building thread', `${board._id || board}/thread/${threadId}.html`); + if (!board._id) { + board = await Boards.findOne(board); + } + 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 render(`${board._id}/thread/${threadId}.html`, 'thread.pug', { + board, + thread + }); + }, + + buildBoard: async (board, page, maxPage=null) => { +//console.log('building board page', `${board._id}/${page === 1 ? 'index' : page}.html`); + const threads = await Posts.getRecent(board._id, page); + if (!maxPage) { + maxPage = Math.ceil((await Posts.getPages(board._id)) / 10); + } + return render(`${board._id}/${page === 1 ? 'index' : page}.html`, 'board.pug', { + board, + threads, + maxPage, + page + }); + }, + + //building multiple pages (for rebuilds) + buildBoardMultiple: async (board, startpage=1, endpage=10) => { + const maxPage = Math.ceil((await Posts.getPages(board._id)) / 10); + if (endpage === 0) { + //deleted only/all posts, so only 1 page will remain + endpage = 1; + } else if (maxPage < endpage) { + //else just build up to the max page if it is greater than input page number + endpage = maxPage + } + const difference = endpage-startpage + 1; //+1 because for single pagemust be > 0 + const threads = await Posts.getRecent(board._id, startpage, difference*10); + const buildArray = []; + for (let i = startpage; i <= endpage; i++) { +//console.log('multi building board page', `${board._id}/${i === 1 ? 'index' : i}.html`); + let spliceStart = (i-1)*10; + if (spliceStart > 0) { + spliceStart = spliceStart - 1; + } + buildArray.push( + render(`${board._id}/${i === 1 ? 'index' : i}.html`, 'board.pug', { + board, + threads: threads.splice(0,10), + maxPage, + page: i, + }) + ); + } + return Promise.all(buildArray); + }, + + buildHomepage: async () => { + const boards = await Boards.find(); + return render('index.html', 'home.pug', { + boards + }); + }, + + buildChangePassword: () => { + return render('changepassword.html', 'changepassword.pug'); + }, + + buildLogin: () => { + return render('login.html', 'login.pug'); + }, + + buildRegister: () => { + return render('register.html', 'register.pug'); + }, + +} diff --git a/controllers/forms.js b/controllers/forms.js index d95039ad..cf31baa2 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -8,6 +8,10 @@ const express = require('express') , 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') , removeBans = require(__dirname+'/../models/forms/removebans.js') , makePost = require(__dirname+'/../models/forms/make-post.js') @@ -17,16 +21,18 @@ const express = require('express') , 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'); + , csrf = require(__dirname+'/../helpers/csrfmiddleware.js') + , deleteFailedFiles = require(__dirname+'/../helpers/files/deletefailed.js') + , actionChecker = require(__dirname+'/../helpers/actionchecker.js'); + // login to account -router.post('/login', csrf, (req, res, next) => { +router.post('/login', (req, res, next) => { const errors = []; @@ -50,7 +56,7 @@ router.post('/login', csrf, (req, res, next) => { return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': '/login' + 'redirect': '/login.html' }) } @@ -98,7 +104,7 @@ router.post('/changepassword', verifyCaptcha, async (req, res, next) => { return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': '/changepassword' + 'redirect': '/changepassword.html' }) } @@ -144,7 +150,7 @@ router.post('/register', verifyCaptcha, (req, res, next) => { return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': '/register' + 'redirect': '/register.html' }) } @@ -153,7 +159,7 @@ router.post('/register', verifyCaptcha, (req, res, next) => { }); // make new post -router.post('/board/:board/post', Boards.exists, banCheck, paramConverter, async (req, res, next) => { +router.post('/board/:board/post', Boards.exists, banCheck, paramConverter, verifyCaptcha, async (req, res, next) => { let numFiles = 0; if (req.files && req.files.file) { @@ -171,15 +177,25 @@ router.post('/board/:board/post', Boards.exists, banCheck, paramConverter, async if (!req.body.message && numFiles === 0) { errors.push('Must provide a message or file'); } - if (req.body.message && req.body.message.length > 2000) { - errors.push('Message must be 2000 characters or less'); + if (!req.body.thread && (res.locals.board.settings.forceOPFile && res.locals.board.settings.maxFiles === 0)) { + errors.push('Threads must include a file'); } - if (!req.body.thread && (!req.body.message || req.body.message.length === 0)) { + if (!req.body.thread && res.locals.board.settings.forceOPMessage && (!req.body.message || req.body.message.length === 0)) { errors.push('Threads must include a message'); } + if (req.body.message) { + if (req.body.message.length > 2000) { + errors.push('Message must be 2000 characters or less'); + } else if (req.body.message.length < res.locals.board.settings.minMessageLength) { + errors.push(`Message must be at least ${res.locals.board.settings.minMessageLength} characters long`); + } + } if (req.body.name && req.body.name.length > 50) { errors.push('Name must be 50 characters or less'); } + if (res.locals.board.settings.forceOPSubject && (!req.body.subject || req.body.subject.length === 0)) { + errors.push('Threads must include a subject'); + } if (req.body.subject && req.body.subject.length > 50) { errors.push('Subject must be 50 characters or less'); } @@ -194,16 +210,21 @@ router.post('/board/:board/post', Boards.exists, banCheck, paramConverter, async return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': `/${req.params.board}${req.body.thread ? '/thread/' + req.body.thread : ''}` + '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 = req.files.file.map(file => file.filename); - await deletePostFiles(fileNames).catch(err => console.error); + 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); } return next(err); } @@ -232,13 +253,13 @@ router.post('/board/:board/settings', csrf, Boards.exists, checkPermsMiddleware, return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': `/${req.params.board}/manage` + 'redirect': `/${req.params.board}/manage.html` }) } return res.status(501).render('message', { 'title': 'Not implemented', - 'redirect': `/${req.params.board}/manage` + 'redirect': `/${req.params.board}/manage.html` }) }); @@ -261,19 +282,29 @@ 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 (errors.length > 0) { return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': `/${req.params.board}/manage` + 'redirect': `/${req.params.board}/manage.html` }) } try { await uploadBanners(req, res, next, numFiles); } catch (err) { - console.error(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); return next(err); } @@ -292,7 +323,7 @@ router.post('/board/:board/deletebanners', csrf, Boards.exists, checkPermsMiddle return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': `/${req.params.board}/manage` + 'redirect': `/${req.params.board}/manage.html` }) } @@ -301,7 +332,7 @@ router.post('/board/:board/deletebanners', csrf, Boards.exists, checkPermsMiddle return res.status(400).render('message', { 'title': 'Bad request', 'message': 'Invalid banners selected', - 'redirect': `/${req.params.board}/manage` + 'redirect': `/${req.params.board}/manage.html` }) } } @@ -333,7 +364,7 @@ router.post('/board/:board/unban', csrf, Boards.exists, checkPermsMiddleware, pa return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': `/${req.params.board}/manage` + 'redirect': `/${req.params.board}/manage.html` }); } @@ -347,7 +378,7 @@ router.post('/board/:board/unban', csrf, Boards.exists, checkPermsMiddleware, pa return res.render('message', { 'title': 'Success', 'messages': messages, - 'redirect': `/${req.params.board}/manage` + 'redirect': `/${req.params.board}/manage.html` }); }); @@ -387,7 +418,7 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': '/globalmanage' + 'redirect': '/globalmanage.html' }) } @@ -397,7 +428,7 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async return res.status(404).render('message', { 'title': 'Not found', 'errors': 'Selected posts not found', - 'redirect': '/globalmanage' + 'redirect': '/globalmanage.html' }) } @@ -414,7 +445,7 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async } messages.push(message); } - if (hasPerms && req.body.delete_ip_global) { + if (req.body.delete_ip_global) { const deletePostIps = posts.map(x => x.ip); const deleteIpPosts = await Posts.db.find({ 'ip': { @@ -439,7 +470,13 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async 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) { @@ -476,7 +513,7 @@ router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, async return res.render('message', { 'title': 'Success', 'messages': messages, - 'redirect': '/globalmanage' + 'redirect': '/globalmanage.html' }); }); @@ -493,7 +530,7 @@ router.post('/global/unban', csrf, checkPermsMiddleware, paramConverter, async(r return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': `/globalmanage` + 'redirect': `/globalmanage.html` }); } @@ -507,7 +544,7 @@ router.post('/global/unban', csrf, checkPermsMiddleware, paramConverter, async(r return res.render('message', { 'title': 'Success', 'messages': messages, - 'redirect': `/globalmanage` + 'redirect': `/globalmanage.html` }); }); diff --git a/controllers/pages.js b/controllers/pages.js index d3d021de..4b3e0c66 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -3,6 +3,7 @@ const express = require('express') , router = express.Router() , Boards = require(__dirname+'/../db/boards.js') + , Posts = require(__dirname+'/../db/posts.js') , hasPerms = require(__dirname+'/../helpers/haspermsmiddleware.js') , isLoggedIn = require(__dirname+'/../helpers/isloggedin.js') , paramConverter = require(__dirname+'/../helpers/paramconverter.js') @@ -21,19 +22,19 @@ const express = require('express') , thread = require(__dirname+'/../models/pages/thread.js'); //homepage with board list -router.get('/index', home); +router.get('/index.html', home); //login page -router.get('/login', csrf, login); +router.get('/login.html', login); //registration page -router.get('/register', register); +router.get('/register.html', register); //change password page -router.get('/changepassword', changePassword); +router.get('/changepassword.html', changePassword); //logout -router.get('/logout', csrf, isLoggedIn, (req, res, next) => { +router.get('/logout', isLoggedIn, (req, res, next) => { //remove session req.session.destroy(); @@ -48,19 +49,19 @@ router.get('/captcha', captcha); router.get('/banners', banners); //board manage page -router.get('/:board/manage', Boards.exists, isLoggedIn, hasPerms, csrf, manage); +router.get('/:board/manage.html', Boards.exists, isLoggedIn, hasPerms, csrf, manage); //board manage page -router.get('/globalmanage', isLoggedIn, hasPerms, csrf, globalManage); +router.get('/globalmanage.html', isLoggedIn, hasPerms, csrf, globalManage); // board page/recents -router.get('/:board/(:page([2-9]*|index))?', Boards.exists, paramConverter, board); +router.get('/:board/:page(1[0-9]*|[2-9]*|index).html', Boards.exists, paramConverter, board); // thread view page -router.get('/:board/thread/:id(\\d+)', Boards.exists, paramConverter, thread); +router.get('/:board/thread/:id(\\d+).html', Boards.exists, paramConverter, Posts.exists, thread); // board catalog page -router.get('/:board/catalog', Boards.exists, catalog); +router.get('/:board/catalog.html', Boards.exists, catalog); module.exports = router; diff --git a/db/boards.js b/db/boards.js index 4157a550..0e225f46 100644 --- a/db/boards.js +++ b/db/boards.js @@ -78,7 +78,7 @@ module.exports = { return res.status(403).render('message', { 'title': 'Forbidden', 'message': 'You do not have permission to manage this board', - 'redirect': '/login' + 'redirect': '/login.html' }); }, diff --git a/db/posts.js b/db/posts.js index c12f2493..dbd727ed 100644 --- a/db/posts.js +++ b/db/posts.js @@ -9,7 +9,18 @@ module.exports = { db, - getRecent: async (board, page) => { + getThreadPage: async (board, thread) => { + const threadsBefore = await db.countDocuments({ + 'board': board, + 'thread': null, + 'bumped': { + '$gte': thread.bumped + } + }); + return Math.ceil(threadsBefore/10) || 1; //1 because 0 threads before is page 1 + }, + + getRecent: async (board, page, limit=10) => { // get all thread posts (posts with null thread id) const threads = await db.find({ 'thread': null, @@ -25,7 +36,7 @@ module.exports = { }).sort({ 'sticky': -1, 'bumped': -1, - }).skip(10*(page-1)).limit(10).toArray(); + }).skip(10*(page-1)).limit(limit).toArray(); // add last 5 posts in reverse order to preview await Promise.all(threads.map(async thread => { @@ -47,8 +58,8 @@ module.exports = { //reverse order for board page thread.replies = replies.reverse(); - //temporary mitigation for deletion issue - if (replies.length > 5) { + //if enough replies, show omitted count + if (thread.replyposts > 5) { //cout omitted image and posts const numPreviewImages = replies.reduce((acc, post) => { return acc + post.files.length }, 0); thread.omittedimages = thread.replyfiles - numPreviewImages; @@ -125,7 +136,6 @@ module.exports = { thread.replies = data[1]; } return thread; - }, getThreadPosts: (board, id) => { @@ -178,6 +188,9 @@ module.exports = { 'reports': 0, 'globalreports': 0, } + }).sort({ + 'sticky': -1, + 'bumped': -1, }).toArray(); }, @@ -331,27 +344,29 @@ module.exports = { }).sort({ 'sticky': -1, 'bumped': -1 - }).skip(threadLimit).toArray(); //100 therads in board limit for now + }).skip(threadLimit).toArray(); //if there are any - if (threads.length > 0) { - //get the postIds - const threadIds = threads.map(thread => thread.postId); - //get all the posts from those threads - 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); + if (threads.length === 0) { + return []; } + //get the postIds + const threadIds = threads.map(thread => thread.postId); + //get all the posts from those threads + 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); + return threadIds; }, deleteMany: (ids) => { @@ -368,4 +383,13 @@ module.exports = { }); }, + exists: async (req, res, next) => { + const thread = await module.exports.getThread(req.params.board, req.params.id); + if (!thread) { + return res.status(404).render('404'); + } + res.locals.thread = thread; // can acces this in views or next route handlers + next(); + } + } diff --git a/ecosystem.config.js b/ecosystem.config.js index 06764043..9b6f62b3 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -8,6 +8,9 @@ module.exports = { watch: false, max_memory_restart: '1G', log_date_format: 'YYYY-MM-DD HH:mm:ss', + wait_ready: true, + listen_timeout: 5000, + kill_timeout: 5000, env: { NODE_ENV: 'development' }, diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index 35cc1445..7bfa3365 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -48,9 +48,11 @@ body { } .pages { - margin: 10px 0; + box-sizing: border-box; padding: 10px; + width: max-content; } + a, a:visited { text-decoration: underline; color: #34345C; @@ -72,6 +74,7 @@ object { .navbar { border-bottom: 1px solid #a9a9a9; + background: #d6daf0; } .catalog-tile-button { @@ -133,6 +136,7 @@ object { } .mode { + margin-top: 1px; background-color: red; color: white; font-weight: bold; @@ -156,8 +160,8 @@ object { font-weight: bold; } -.redtext { - color: maroon; +.pinktext { + color: #E0727F; } .greentext { @@ -205,13 +209,14 @@ td, th { align-items: center; } -.post-container, .pages, .toggle-label { +.post-container, .pages, summary { background: #D6DAF0; border: 1px solid #B7C5D9; } .actions { - max-width: 100%; + text-align: left; + max-width: 200px; display: flex; flex-direction: column; margin: 2px 0; @@ -232,8 +237,13 @@ td, th { box-shadow: inset 0 0 100px 100px rgba(255,255,255,.25); } -.toggle-label { +summary { + margin-bottom: 1px; padding: 10px; + cursor: pointer; +} + +.toggle-label { text-align: center; max-width: 100%; box-sizing: border-box; @@ -254,7 +264,7 @@ td, th { display: flex; flex-direction: column; max-width: 100%; - /*margin-top: 10px;*/ + width: 400px; } .togglable { @@ -298,17 +308,22 @@ td, th { text-align: center; margin: 2px; margin-top: 0px; - max-width: 160px; + max-width: 128px; overflow: hidden; - max-width: 160px; text-overflow: ellipsis; word-break: keep-all; + font-size: x-small; } .post-file-src { margin: 0 auto; } +.file-thumb { + max-width: 128px; + max-height: 128px; +} + figure { } @@ -410,7 +425,7 @@ input textarea { } .nav-item { - line-height: 30px; + line-height: 38px; text-decoration: none; float: left; padding-left: 10px; @@ -439,6 +454,7 @@ input textarea { margin-top: auto; line-height: 30px; border-top: 1px solid #a9a9a9; + background: #d6daf0; } input[type="text"], input[type="submit"], input[type="password"], input[type="file"], textarea { @@ -530,12 +546,17 @@ hr { height: 8px; } + .pages { + width:100%; + } + .post-container { width: 100%; } .catalog-tile { overflow-y: hidden; + width: 48%; } .table-body { diff --git a/helpers/captchaverify.js b/helpers/captchaverify.js index c681667b..21ee4de9 100644 --- a/helpers/captchaverify.js +++ b/helpers/captchaverify.js @@ -2,13 +2,16 @@ const Captchas = require(__dirname+'/../db/captchas.js') , Mongo = require(__dirname+'/../db/db.js') - , util = require('util') - , fs = require('fs') - , unlink = util.promisify(fs.unlink) + , remove = require('fs-extra').remove , uploadDirectory = require(__dirname+'/../helpers/uploadDirectory.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) { + return next(); + } + //check if captcha field in form is valid const input = req.body.captcha; if (!input || input.length !== 6) { @@ -46,7 +49,7 @@ module.exports = async (req, res, next) => { //it was correct, so delete the file, the cookie and continue res.clearCookie('captchaid'); - await unlink(`${uploadDirectory}captcha/${captchaId}.jpg`) + await remove(`${uploadDirectory}captcha/${captchaId}.jpg`) return next(); diff --git a/helpers/files/deletefailed.js b/helpers/files/deletefailed.js index 969e5142..d05c5ed6 100644 --- a/helpers/files/deletefailed.js +++ b/helpers/files/deletefailed.js @@ -1,15 +1,12 @@ 'use strict'; -const path = require('path') - , util = require('util') - , fs = require('fs') - , unlink = util.promisify(fs.unlink) +const remove = require('fs-extra').remove , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js'); module.exports = async (filenames, folder) => { await Promise.all(filenames.map(async filename => { - unlink(`${uploadDirectory}${folder}/${filename}`) + remove(`${uploadDirectory}${folder}/${filename}`) })); } diff --git a/helpers/files/deletepostfiles.js b/helpers/files/deletepostfiles.js index c1a29f69..b339e6b2 100644 --- a/helpers/files/deletepostfiles.js +++ b/helpers/files/deletepostfiles.js @@ -1,8 +1,6 @@ 'use strict'; -const util = require('util') - , fs = require('fs') - , unlink = util.promisify(fs.unlink) +const remove = require('fs-extra').remove , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') module.exports = (fileNames) => { @@ -11,8 +9,8 @@ module.exports = (fileNames) => { return Promise.all(fileNames.map(async filename => { //dont question it. return Promise.all([ - unlink(`${uploadDirectory}img/${filename}`), - unlink(`${uploadDirectory}img/thumb-${filename.split('.')[0]}.jpg`) + remove(`${uploadDirectory}img/${filename}`), + remove(`${uploadDirectory}img/thumb-${filename.split('.')[0]}.jpg`) ]).catch(e => console.error) //ignore for now })); diff --git a/helpers/files/file-check-mime-types.js b/helpers/files/file-check-mime-types.js index 846b02b5..2f1eedad 100644 --- a/helpers/files/file-check-mime-types.js +++ b/helpers/files/file-check-mime-types.js @@ -4,18 +4,21 @@ const imageMimeTypes = new Set([ 'image/jpeg', 'image/pjpeg', 'image/png', + 'image/bmp', +]); + +const animatedImageMimeTypes = new Set([ 'image/gif', + 'image/webp', ]); const videoMimeTypes = new Set([ - 'image/webp', - 'image/bmp', 'video/mp4', 'video/webm', ]); module.exports = (mimetype, options) => { - return (options.video && videoMimeTypes.has(mimetype)) || (options.image && imageMimeTypes.has(mimetype)); + return (options.video && videoMimeTypes.has(mimetype)) || (options.image && imageMimeTypes.has(mimetype) || options.animatedImage && animatedImageMimeTypes.has(mimetype)); }; diff --git a/helpers/files/imageupload.js b/helpers/files/imageupload.js new file mode 100644 index 00000000..310143f4 --- /dev/null +++ b/helpers/files/imageupload.js @@ -0,0 +1,19 @@ +'use strict'; + +const uploadDirectory = require(__dirname+'/../uploadDirectory.js') + , gm = require('@tohru/gm'); + +module.exports = (file, filename, folder) => { + + return new Promise((resolve, reject) => { + gm(file.tempFilePath) + .noProfile() + .write(`${uploadDirectory}${folder}/${filename}`, function (err) { + if (err) { + return reject(err); + } + return resolve(); + }); + }); + +}; diff --git a/helpers/files/file-upload.js b/helpers/files/videoupload.js similarity index 52% rename from helpers/files/file-upload.js rename to helpers/files/videoupload.js index 1bf7953b..a709d272 100644 --- a/helpers/files/file-upload.js +++ b/helpers/files/videoupload.js @@ -1,9 +1,8 @@ 'use strict'; -const configs = require(__dirname+'/../../configs/main.json') - , uploadDirectory = require(__dirname+'/../uploadDirectory.js'); +const uploadDirectory = require(__dirname+'/../uploadDirectory.js'); -module.exports = (req, res, file, filename, folder) => { +module.exports = (file, filename, folder) => { return new Promise((resolve, reject) => { file.mv(`${uploadDirectory}${folder}/${filename}`, function (err) { diff --git a/helpers/isloggedin.js b/helpers/isloggedin.js index 69d540c1..458c9ff5 100644 --- a/helpers/isloggedin.js +++ b/helpers/isloggedin.js @@ -4,5 +4,5 @@ module.exports = (req, res, next) => { if (req.session.authenticated === true) { return next(); } - res.redirect('/login'); + res.redirect('/login.html'); } diff --git a/helpers/markdown.js b/helpers/markdown.js index 9164a733..a882cf6c 100644 --- a/helpers/markdown.js +++ b/helpers/markdown.js @@ -2,7 +2,7 @@ const Posts = require(__dirname+'/../db/posts.js') , greentextRegex = /^>([^>].+)/gm - , redtextRegex = /^<([^<].+)/gm + , pinktextRegex = /^<([^<].+)/gm , boldRegex = /""(.+)""/gm , titleRegex = /==(.+)==/gm , italicRegex = /__(.+)__/gm @@ -13,9 +13,9 @@ const Posts = require(__dirname+'/../db/posts.js') module.exports = (board, thread, text) => { - //redtext - text = text.replace(redtextRegex, (match, redtext) => { - return `<${redtext}`; + //pinktext + text = text.replace(pinktextRegex, (match, pinktext) => { + return `<${pinktext}`; }); //greentext diff --git a/helpers/paramconverter.js b/helpers/paramconverter.js index c2315623..6ff9035b 100644 --- a/helpers/paramconverter.js +++ b/helpers/paramconverter.js @@ -1,7 +1,7 @@ 'use strict'; const Mongo = require(__dirname+'/../db/db.js') - , allowedArrays = new Set(['checkedposts', 'globalcheckedposts', 'checkedbans']) + , allowedArrays = new Set(['checkedposts', 'globalcheckedposts', 'checkedbans', 'checkedbanners']) module.exports = (req, res, next) => { diff --git a/helpers/quotes.js b/helpers/quotes.js index 4068a8f9..46fe8d46 100644 --- a/helpers/quotes.js +++ b/helpers/quotes.js @@ -75,7 +75,7 @@ module.exports = async (board, text) => { text = text.replace(quoteRegex, (match) => { const quotenum = +match.substring(2); if (postThreadIdMap[board] && postThreadIdMap[board][quotenum]) { - return `>>${quotenum}`; + return `>>${quotenum}`; } return match; }); @@ -86,9 +86,9 @@ module.exports = async (board, text) => { const quoteboard = quote[1]; const quotenum = +quote[2]; if (postThreadIdMap[quoteboard] && postThreadIdMap[quoteboard][quotenum]) { - return `>>>/${quoteboard}/${quotenum}`; - } else if (postThreadIdMap[quoteboard] && quotenum === 0) { - return `>>>/${quoteboard}/`; + return `>>>/${quoteboard}/${quotenum}`; + } else if (!quote[2]) { + return `>>>/${quoteboard}/`; } return match; }); diff --git a/helpers/render.js b/helpers/render.js new file mode 100644 index 00000000..1c3ec885 --- /dev/null +++ b/helpers/render.js @@ -0,0 +1,12 @@ +'use strict'; + +const outputFile = require('fs-extra').outputFile + , pug = require('pug') + , path = require('path') + , uploadDirectory = require(__dirname+'/uploadDirectory.js') + , templateDirectory = path.join(__dirname+'/../views/pages/'); + +module.exports = async (htmlName, templateName, options) => { + const html = pug.renderFile(`${templateDirectory}${templateName}`, { ...options, cache: true }); + return outputFile(`${uploadDirectory}html/${htmlName}`, html); +}; diff --git a/helpers/writepagehtml.js b/helpers/writepagehtml.js deleted file mode 100644 index 9fe0433e..00000000 --- a/helpers/writepagehtml.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -const util = require('util') - , fs = require('fs') - , pug = require('pug') - , path = require('path') - , writeFile = util.promisify(fs.writeFile) - , uploadDirectory = require(__dirname+'/uploadDirectory.js') - , pugDirectory = path.join(__dirname+'/../views/pages'); - -module.exports = async (htmlName, pugName, pugVars) => { - const html = pug.renderFile(`${pugDirectory}/${pugName}`, pugVars); - return writeFile(`${uploadDirectory}html/${htmlName}`, html); -}; diff --git a/models/forms/actionhandler.js b/models/forms/actionhandler.js index 95fc013a..d876ed0d 100644 --- a/models/forms/actionhandler.js +++ b/models/forms/actionhandler.js @@ -14,7 +14,10 @@ const Posts = require(__dirname+'/../../db/posts.js') , dismissReports = require(__dirname+'/dismiss-report.js') , dismissGlobalReports = require(__dirname+'/dismissglobalreport.js') , actionChecker = require(__dirname+'/../../helpers/actionchecker.js') - , checkPerms = require(__dirname+'/../../helpers/hasperms.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) => { @@ -57,16 +60,16 @@ module.exports = async (req, res, next) => { return res.status(400).render('message', { 'title': 'Bad request', 'errors': errors, - 'redirect': `/${req.params.board}` + 'redirect': `/${req.params.board}/` }) } - const posts = await Posts.getPosts(req.params.board, req.body.checkedposts, true); + 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}` + 'redirect': `/${req.params.board}/` }) } @@ -88,7 +91,7 @@ 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}/` }); } } else { @@ -128,6 +131,7 @@ module.exports = async (req, res, next) => { query['board'] = req.params.board; } const deleteIpPosts = await Posts.db.find(query).toArray(); + posts = posts.concat(deleteIpPosts); if (deleteIpPosts && deleteIpPosts.length > 0) { const { message } = await deletePosts(req, res, next, deleteIpPosts, req.params.board); messages.push(message); @@ -206,30 +210,63 @@ module.exports = async (req, res, next) => { messages.push(message); } } - const dbPromises = [] + const bulkWrites = [] if (Object.keys(combinedQuery).length > 0) { - dbPromises.push( - Posts.db.updateMany({ - '_id': { - '$in': postMongoIds - } - }, combinedQuery) - ) + bulkWrites.push({ + 'updateMany': { + 'filter': { + '_id': { + '$in': postMongoIds + } + }, + 'update': combinedQuery + } + }); } if (Object.keys(passwordCombinedQuery).length > 0) { - dbPromises.push( - Posts.db.updateMany({ - '_id': { - '$in': passwordPostMongoIds - } - }, passwordCombinedQuery) - ) + bulkWrites.push({ + 'updateMany': { + 'filter': { + '_id': { + '$in': passwordPostMongoIds + } + }, + 'update': passwordCombinedQuery + } + }); } - await Promise.all(dbPromises); + + //get a map of boards to threads affected + const boardThreadMap = {}; + const queryOrs = []; + for (let i = 0; i < posts.length; i++) { + const post = posts[i]; + if (!boardThreadMap[post.board]) { + boardThreadMap[post.board] = []; + } + boardThreadMap[post.board].push(post.thread || post.postId); + } + + const beforePages = {}; + const threadBoards = Object.keys(boardThreadMap); + //get how many pages each board is to know whether we should rebuild all pages (because of page nav changes) + //only if deletes actions selected because this could result in number of pages to change + if (req.body.delete || req.body.delete_ip_board || req.body.delete_ip_global) { + await Promise.all(threadBoards.map(async board => { + beforePages[board] = Math.ceil((await Posts.getPages(board)) / 10); + })); + } + + //execute actions now + if (bulkWrites.length > 0) { + await Posts.db.bulkWrite(bulkWrites); + } + + //get only posts (so we can use them for thread ids + const postThreadsToUpdate = posts.filter(post => post.thread !== null); 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) => { + //recalculate replies and image counts + await Promise.all(postThreadsToUpdate.map(async (post) => { const replyCounts = await Posts.getReplyCounts(post.board, post.thread); let replyposts = 0; let replyfiles = 0; @@ -240,6 +277,79 @@ module.exports = async (req, res, next) => { Posts.setReplyCounts(post.board, post.thread, replyposts, replyfiles); })); } + + //make it into an OR query for the db + for (let i = 0; i < threadBoards.length; i++) { + const threadBoard = threadBoards[i]; + boardThreadMap[threadBoard] = [...new Set(boardThreadMap[threadBoard])] + queryOrs.push({ + 'board': threadBoard, + 'postId': { + '$in': boardThreadMap[threadBoard] + } + }) + } + + //fetch threads per board that we only checked posts for + let threadsEachBoard = await Posts.db.find({ + 'thread': null, + '$or': queryOrs + }).toArray(); + //combine it with what we already had + threadsEachBoard = threadsEachBoard.concat(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) => { + if (!acc[curr.board] || curr.bumped < acc[curr.board].bumped) { + acc[curr.board] = { oldest: null, newest: null}; + } + if (!acc[curr.board].oldest || curr.bumped < acc[curr.board].oldest.bumped) { + acc[curr.board].oldest = curr; + } + if (!acc[curr.board].newest || curr.bumped > acc[curr.board].newest.bumped) { + acc[curr.board].newest = curr; + } + return acc; + }, {}); + + //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]; + //always need to refresh catalog + parallelPromises.push(buildCatalog(res.locals.board)); + //rebuild impacted threads + for (let j = 0; j < boardThreadMap[changeBoard].length; j++) { + parallelPromises.push(buildThread(boardThreadMap[changeBoard][j], changeBoard)); + } + //refersh any pages affected + const afterPages = Math.ceil((await Posts.getPages(changeBoard)) / 10); + if (beforePages[changeBoard] && beforePages[changeBoard] !== afterPages) { + //amount of pages changed, rebuild all pages + parallelPromises.push(buildBoardMultiple(res.locals.board, 1, afterPages)); + } else { + const threadPageOldest = await Posts.getThreadPage(req.params.board, bounds.oldest); + const threadPageNewest = await Posts.getThreadPage(req.params.board, 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)); + } 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 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)); + } + } + } + await Promise.all(parallelPromises); + } catch (err) { return next(err); } @@ -247,7 +357,7 @@ module.exports = async (req, res, next) => { return res.render('message', { 'title': 'Success', 'messages': messages, - 'redirect': `/${req.params.board}` + 'redirect': `/${req.params.board}/` }); } diff --git a/models/forms/changepassword.js b/models/forms/changepassword.js index 6fb034f0..c7dc6960 100644 --- a/models/forms/changepassword.js +++ b/models/forms/changepassword.js @@ -17,7 +17,7 @@ module.exports = async (req, res, next) => { return res.status(403).render('message', { 'title': 'Forbidden', 'message': 'Incorrect username or password', - 'redirect': redirect ? `/login?redirect=${redirect}` : '/changepassword' + 'redirect': '/changepassword.html' }); } @@ -25,16 +25,16 @@ module.exports = async (req, res, next) => { const passwordMatch = await bcrypt.compare(password, account.passwordHash); //if hashes matched - if (passwordMatch === true) { - //change the password - await Accounts.changePassword(username, newPassword); - return res.redirect('/login'); + if (passwordMatch === false) { + return res.status(403).render('message', { + 'title': 'Forbidden', + 'message': 'Incorrect username or password', + 'redirect': '/changepassword.html' + }); } - return res.status(403).render('message', { - 'title': 'Forbidden', - 'message': 'Incorrect username or password', - 'redirect': redirect ? `/login?redirect=${redirect}` : '/login' - }); + //change the password + await Accounts.changePassword(username, newPassword); + return res.redirect('/login.html'); } diff --git a/models/forms/delete-post.js b/models/forms/delete-post.js index 586d115c..eee05592 100644 --- a/models/forms/delete-post.js +++ b/models/forms/delete-post.js @@ -2,6 +2,7 @@ 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'); @@ -10,6 +11,13 @@ module.exports = async (req, res, next, posts, board) => { //filter to threads const threads = posts.filter(x => x.thread == null); + //delete the html for threads + const deleteHTML = [] + for (let i = 0; i < threads.length; i++) { + deleteHTML.push(remove(`${uploadDirectory}html/${threads[i].board}/thread/${threads[i].postId}.html`)); + } + await Promise.all(deleteHTML); + //get posts from all threads let threadPosts = [] if (threads.length > 0) { diff --git a/models/forms/deletebanners.js b/models/forms/deletebanners.js index c79f0581..f8015408 100644 --- a/models/forms/deletebanners.js +++ b/models/forms/deletebanners.js @@ -1,19 +1,15 @@ 'use strict'; -const uuidv4 = require('uuid/v4') - , path = require('path') - , util = require('util') - , fs = require('fs') - , unlink = util.promisify(fs.unlink) +const remove = require('fs-extra').remove , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') , Boards = require(__dirname+'/../../db/boards.js'); module.exports = async (req, res, next) => { - const redirect = `/${req.params.board}/manage` + const redirect = `/${req.params.board}/manage.html` await Promise.all(req.body.checkedbanners.map(async filename => { - unlink(`${uploadDirectory}banner/${filename}`); + remove(`${uploadDirectory}banner/${filename}`); })); // i dont think there is a way to get the number of array items removed with $pullAll diff --git a/models/forms/deletepostsfiles.js b/models/forms/deletepostsfiles.js index 03507893..b1d80988 100644 --- a/models/forms/deletepostsfiles.js +++ b/models/forms/deletepostsfiles.js @@ -1,9 +1,6 @@ 'use strict'; -const path = require('path') - , util = require('util') - , fs = require('fs') - , unlink = util.promisify(fs.unlink) +const remove = require('fs-extra').remove , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') module.exports = async (posts) => { @@ -24,8 +21,8 @@ module.exports = async (posts) => { await Promise.all(fileNames.map(async filename => { //dont question it. return Promise.all([ - unlink(`${uploadDirectory}img/${filename}`), - unlink(`${uploadDirectory}img/thumb-${filename.split('.')[0]}.png`) + remove(`${uploadDirectory}img/${filename}`), + remove(`${uploadDirectory}img/thumb-${filename.split('.')[0]}.png`) ]) })); diff --git a/models/forms/login.js b/models/forms/login.js index fa8e8b1a..532a6acf 100644 --- a/models/forms/login.js +++ b/models/forms/login.js @@ -7,7 +7,6 @@ module.exports = async (req, res, next) => { const username = req.body.username.toLowerCase(); const password = req.body.password; - const redirect = req.body.redirect; //fetch an account let account; @@ -22,7 +21,7 @@ module.exports = async (req, res, next) => { return res.status(403).render('message', { 'title': 'Forbidden', 'message': 'Incorrect username or password', - 'redirect': redirect ? `/login?redirect=${redirect}` : '/login' + 'redirect': '/login.html' }); } @@ -45,14 +44,14 @@ module.exports = async (req, res, next) => { req.session.authenticated = true; //successful login - return res.redirect(redirect || '/'); + return res.redirect('/'); } return res.status(403).render('message', { 'title': 'Forbidden', 'message': 'Incorrect username or password', - 'redirect': redirect ? `/login?redirect=${redirect}` : '/login' + 'redirect': '/login.html' }); } diff --git a/models/forms/make-post.js b/models/forms/make-post.js index 99463748..ca8e9d03 100644 --- a/models/forms/make-post.js +++ b/models/forms/make-post.js @@ -5,6 +5,7 @@ const uuidv4 = require('uuid/v4') , util = require('util') , crypto = require('crypto') , randomBytes = util.promisify(crypto.randomBytes) + , remove = require('fs-extra').remove , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') , Posts = require(__dirname+'/../../db/posts.js') , getTripCode = require(__dirname+'/../../helpers/tripcode.js') @@ -20,18 +21,20 @@ const uuidv4 = require('uuid/v4') } , nameRegex = /^(?[^\s#]+)?(?:##(?[^ ]{1}[^\s#]+))?(?:## (?[^\s#]+))?$/ , permsCheck = require(__dirname+'/../../helpers/hasperms.js') - , fileUpload = require(__dirname+'/../../helpers/files/file-upload.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'); + , formatSize = require(__dirname+'/../../helpers/files/format-size.js') + , { buildCatalog, buildThread, buildBoard, buildBoardMultiple } = require(__dirname+'/../../build.js'); module.exports = async (req, res, next, numFiles) => { // check if this is responding to an existing thread - let redirect = `/${req.params.board}` + let redirect = `/${req.params.board}/` let salt = null; let thread = null; const hasPerms = permsCheck(req, res); @@ -46,7 +49,7 @@ module.exports = async (req, res, next, numFiles) => { }); } salt = thread.salt; - redirect += `/thread/${req.body.thread}` + redirect += `thread/${req.body.thread}.html` if (thread.locked && !hasPerms) { return res.status(400).render('message', { 'title': 'Bad request', @@ -62,12 +65,19 @@ module.exports = async (req, res, next, numFiles) => { }); } } + if (numFiles > res.locals.board.settings.maxFiles) { + return res.status(400).render('message', { + 'title': 'Bad request', + 'message': `Too many files. Max files per post is ${res.locals.board.settings.maxFiles}.`, + 'redirect': redirect + }); + } let files = []; // if we got a file if (numFiles > 0) { // 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, video: true})) { + if (!fileCheckMimeType(req.files.file[i].mimetype, {animatedImage: true, image: true, video: true})) { 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.`, @@ -82,9 +92,6 @@ module.exports = async (req, res, next, numFiles) => { const filename = uuid + path.extname(file.name); file.filename = filename; //for error to delete failed files - //upload file - await fileUpload(req, res, file, filename, 'img'); - //get metadata let processedFile = { filename: filename, @@ -97,26 +104,39 @@ module.exports = async (req, res, next, numFiles) => { const mainType = file.mimetype.split('/')[0]; switch (mainType) { case 'image': + await imageUpload(file, filename, 'img'); const imageData = await imageIdentify(filename, 'img'); 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 - await imageThumbnail(filename); + if (fileCheckMimeType(file.mimetype, {image: true}) //always thumbnail gif/webp + && processedFile.geometry.height <= 128 + && processedFile.geometry.width <= 128) { + processedFile.hasThumb = false; + } else { + processedFile.hasThumb = true; + await imageThumbnail(filename); + } break; case 'video': //video metadata + await videoUpload(file, filename, 'img'); const videoData = await videoIdentify(filename); 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); break; default: return next(err); } + //delete the temp file + await remove(file.tempFilePath); + //handle gifs with multiple geometry and size if (Array.isArray(processedFile.geometry)) { processedFile.geometry = processedFile.geometry[0]; @@ -128,6 +148,7 @@ module.exports = async (req, res, next, numFiles) => { processedFile.geometryString = processedFile.geometryString[0]; } files.push(processedFile); + } } @@ -145,9 +166,10 @@ module.exports = async (req, res, next, numFiles) => { } //forceanon hide reply subjects so cant be used as name for replies - let subject = hasPerms || !forceAnon || !req.body.thread ? req.body.subject : null; //forceanon only allow sage email - let email = hasPerms || !forceAnon || req.body.email === 'sage' ? req.body.email : null; + let subject = (hasPerms || !forceAnon || !req.body.thread) ? req.body.subject : null; + let email = (hasPerms || !forceAnon || req.body.email === 'sage') ? req.body.email : null; + let name = res.locals.board.settings.defaultName; let tripcode = null; let capcode = null; @@ -214,11 +236,37 @@ module.exports = async (req, res, next, numFiles) => { } const postId = await Posts.insertOne(req.params.board, data, thread); - if (!data.thread) { //if we just added a new thread, prune any old ones - await Posts.pruneOldThreads(req.params.board, res.locals.board.settings.threadLimit); + const successRedirect = `/${req.params.board}/thread/${req.body.thread || postId}.html#${postId}`; + + //build just the thread they need to see first and send them immediately + await buildThread(data.thread || postId, res.locals.board); + res.redirect(successRedirect); + + //now rebuild other pages + const parallelPromises = [] + if (data.thread) { + //refersh pages + const threadPage = await Posts.getThreadPage(req.params.board, thread); + if (data.email === 'sage') { + //refresh the page that the thread is on + parallelPromises.push(buildBoard(res.locals.board, threadPage)); + } else { + //if not saged, it will bump so we should refresh any pages above it as well + parallelPromises.push(buildBoardMultiple(res.locals.board, 1, threadPage)); + } + } else { + //new thread, rebuild all pages and prunes old threads + const prunedThreads = await Posts.pruneOldThreads(req.params.board, res.locals.board.settings.threadLimit); + for (let i = 0; i < prunedThreads.length; i++) { + parallelPromises.push(remove(`${uploadDirectory}html/${req.params.board}/thread/${prunedThreads[i]}.html`)); + } + parallelPromises.push(buildBoardMultiple(res.locals.board, 1, 10)); } - const successRedirect = `/${req.params.board}/thread/${req.body.thread || postId}#${postId}`; + //always rebuild catalog for post counts and ordering + parallelPromises.push(buildCatalog(res.locals.board)); + + //finish building other pages + await Promise.all(parallelPromises); - return res.redirect(successRedirect); } diff --git a/models/forms/register.js b/models/forms/register.js index 51f96bf5..f732155b 100644 --- a/models/forms/register.js +++ b/models/forms/register.js @@ -20,7 +20,7 @@ module.exports = async (req, res, next) => { return res.status(409).render('message', { 'title': 'Conflict', 'message': 'Account with this username already exists', - 'redirect': '/register' + 'redirect': '/register.html' }); } @@ -31,6 +31,6 @@ module.exports = async (req, res, next) => { return next(err); } - return res.redirect('/login') + return res.redirect('/login.html') } diff --git a/models/forms/report-post.js b/models/forms/report-post.js index c5a0f4de..32135fc4 100644 --- a/models/forms/report-post.js +++ b/models/forms/report-post.js @@ -15,7 +15,10 @@ module.exports = (req, posts) => { message: `Reported ${posts.length} post(s)`, action: '$push', query: { - 'reports': report + 'reports': { + '$each': [report], + '$slice': -5 //limit number of reports + } } }; diff --git a/models/forms/spoiler-post.js b/models/forms/spoiler-post.js index f974b492..121816a3 100644 --- a/models/forms/spoiler-post.js +++ b/models/forms/spoiler-post.js @@ -4,12 +4,12 @@ module.exports = (posts) => { // filter to ones not spoilered const filteredPosts = posts.filter(post => { - return !post.spoiler + return !post.spoiler && post.files.length > 0; }); if (filteredPosts.length === 0) { return { - message:'Post(s) already spoilered' + message:'No post(s) to spoiler' }; } diff --git a/models/forms/stickyposts.js b/models/forms/stickyposts.js index 2b50a80a..5b32f0ee 100644 --- a/models/forms/stickyposts.js +++ b/models/forms/stickyposts.js @@ -16,7 +16,8 @@ module.exports = (posts) => { message: `Stickied ${filteredposts.length} post(s)`, action: '$set', query: { - 'sticky': true + 'sticky': true, + 'bumped': 8640000000000000 } }; diff --git a/models/forms/uploadbanners.js b/models/forms/uploadbanners.js index 1d185596..980b31ba 100644 --- a/models/forms/uploadbanners.js +++ b/models/forms/uploadbanners.js @@ -2,8 +2,9 @@ const uuidv4 = require('uuid/v4') , path = require('path') + , remove = require('fs-extra').remove , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') - , fileUpload = require(__dirname+'/../../helpers/files/file-upload.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') @@ -11,11 +12,11 @@ const uuidv4 = require('uuid/v4') module.exports = async (req, res, next, numFiles) => { - const redirect = `/${req.params.board}/manage` + const redirect = `/${req.params.board}/manage.html` // 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, video: false})) { + if (!fileCheckMimeType(req.files.file[i].mimetype, {image: true, animatedImage: true, video: false})) { 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.`, @@ -30,31 +31,33 @@ module.exports = async (req, res, next, numFiles) => { const file = req.files.file[i]; const uuid = uuidv4(); const filename = uuid + path.extname(file.name); - //add filenames to array add processing to delete previous if one fails + file.filename = filename; //for error to delete failed files filenames.push(filename); - // try to save - try { - //upload it - await fileUpload(req, res, file, filename, 'banner'); - const imageData = await imageIdentify(filename, 'banner'); - const geometry = imageData.size; - //make sure its 300x100 banner - if (geometry.width !== 300 || geometry.height !== 100) { - await deleteFailedFiles(filenames, 'banner'); - return res.status(400).render('message', { - 'title': 'Bad request', - 'message': `Invalid file ${file.name}. Banners must be 300x100.`, - 'redirect': redirect - }); + + //upload it + await imageUpload(file, filename, 'banner'); + const imageData = await imageIdentify(filename, 'banner'); + const geometry = imageData.size; + await remove(file.tempFilePath); + + //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); } - } catch (err) { - //TODO: this better - await deleteFailedFiles(filenames, 'banner'); - return next(err); + deleteFailedFiles(fileNames, 'banner').catch(e => console.error); + return res.status(400).render('message', { + 'title': 'Bad request', + 'message': `Invalid file ${file.name}. Banners must be 300x100.`, + 'redirect': redirect + }); } } await Boards.addBanners(req.params.board, filenames); +// await buildBanners(res.locals.board); return res.render('message', { 'title': 'Success', diff --git a/models/pages/board.js b/models/pages/board.js index efdce077..e2fa88d9 100644 --- a/models/pages/board.js +++ b/models/pages/board.js @@ -1,26 +1,22 @@ 'use strict'; -const Posts = require(__dirname+'/../../db/posts.js')l +const Posts = require(__dirname+'/../../db/posts.js') + , { buildBoard } = require(__dirname+'/../../build.js') + , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js'); module.exports = async (req, res, next) => { - const page = req.params.page === 'index' ? 1 : (req.params.page || 1); - let threads; - let pages; + const page = req.params.page === 'index' ? 1 : req.params.page; try { - pages = Math.ceil((await Posts.getPages(req.params.board)) / 10) - if (page > pages && pages > 0) { + const maxPage = Math.ceil((await Posts.getPages(req.params.board)) / 10); + if (page > maxPage && maxPage > 0) { return next(); } - threads = await Posts.getRecent(req.params.board, page); + await buildBoard(res.locals.board, page, maxPage); } catch (err) { return next(err); } - return res.render('board', { - threads: threads || [], - pages, - page, - }); + return res.sendFile(`${uploadDirectory}html/${req.params.board}/${page === 1 ? 'index' : page}.html`); } diff --git a/models/pages/catalog.js b/models/pages/catalog.js index 2dd42f0a..d818bb3d 100644 --- a/models/pages/catalog.js +++ b/models/pages/catalog.js @@ -1,20 +1,16 @@ 'use strict'; -const Posts = require(__dirname+'/../../db/posts.js'); +const { buildCatalog } = require(__dirname+'/../../build.js') + , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js'); module.exports = async (req, res, next) => { - // get all threads - let threads; try { - threads = await Posts.getCatalog(req.params.board); + await buildCatalog(res.locals.board); } catch (err) { return next(err); } - //render the page - res.render('catalog', { - threads: threads || [], - }); + return res.sendFile(`${uploadDirectory}html/${req.params.board}/catalog.html`); } diff --git a/models/pages/changepassword.js b/models/pages/changepassword.js index 99f6542c..ac3be547 100644 --- a/models/pages/changepassword.js +++ b/models/pages/changepassword.js @@ -1,8 +1,16 @@ 'use strict'; -module.exports = (req, res, next) => { +const { buildChangePassword } = require(__dirname+'/../../build.js') + , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js'); - //render the page - res.render('changepassword'); +module.exports = async (req, res, next) => { + + try { + await buildChangePassword(); + } catch (err) { + return next(err); + } + + return res.sendFile(`${uploadDirectory}html/changepassword.html`); } diff --git a/models/pages/home.js b/models/pages/home.js index f6f54c66..044cbbb0 100644 --- a/models/pages/home.js +++ b/models/pages/home.js @@ -1,17 +1,16 @@ 'use strict'; -const Boards = require(__dirname+'/../../db/boards.js'); +const { buildHomepage } = require(__dirname+'/../../build.js') + , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js'); module.exports = async (req, res, next) => { - //get a list of boards - let boards; try { - boards = await Boards.find(); + await buildHomepage(); } catch (err) { return next(err); } - res.render('home', { boards }); + return res.sendFile(`${uploadDirectory}html/index.html`); } diff --git a/models/pages/login.js b/models/pages/login.js index 373b330d..5fe0c372 100644 --- a/models/pages/login.js +++ b/models/pages/login.js @@ -1,11 +1,16 @@ 'use strict'; -module.exports = (req, res, next) => { +const { buildLogin } = require(__dirname+'/../../build.js') + , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js'); - //render the page - res.render('login', { - csrf: req.csrfToken(), - redirect: req.query.redirect, - }); +module.exports = async (req, res, next) => { + + try { + await buildLogin(); + } catch (err) { + return next(err); + } + + return res.sendFile(`${uploadDirectory}html/login.html`); } diff --git a/models/pages/manage.js b/models/pages/manage.js index 962f5039..76225c3d 100644 --- a/models/pages/manage.js +++ b/models/pages/manage.js @@ -1,7 +1,7 @@ 'use strict'; const Posts = require(__dirname+'/../../db/posts.js') - , Bans = require(__dirname+'/../../db/bans.js'); + , Bans = require(__dirname+'/../../db/bans.js') module.exports = async (req, res, next) => { diff --git a/models/pages/register.js b/models/pages/register.js index f8ce78fb..caa5eece 100644 --- a/models/pages/register.js +++ b/models/pages/register.js @@ -1,10 +1,16 @@ 'use strict'; -module.exports = (req, res, next) => { +const { buildRegister } = require(__dirname+'/../../build.js') + , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js'); - //render the page - res.render('register', { - csrf: req.csrfToken() - }); +module.exports = async (req, res, next) => { + + try { + await buildRegister(); + } catch (err) { + return next(err); + } + + return res.sendFile(`${uploadDirectory}html/register.html`); } diff --git a/models/pages/thread.js b/models/pages/thread.js index 2fd61447..16c20c5f 100644 --- a/models/pages/thread.js +++ b/models/pages/thread.js @@ -1,23 +1,16 @@ 'use strict'; -const Posts = require(__dirname+'/../../db/posts.js'); +const { buildThread } = require(__dirname+'/../../build.js') + , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js'); module.exports = async (req, res, next) => { - //get the recently bumped thread & preview posts - let thread; try { - thread = await Posts.getThread(req.params.board, req.params.id); + await buildThread(res.locals.thread.postId, res.locals.board); } catch (err) { return next(err); - } + } - if (!thread) { - return res.status(404).render('404'); - } + return res.sendFile(`${uploadDirectory}html/${req.params.board}/thread/${req.params.id}.html`); - //render the page - res.render('thread', { - thread - }); } diff --git a/package-lock.json b/package-lock.json index 045341be..ab716328 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2083,6 +2083,16 @@ "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs-minipass": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", @@ -2826,8 +2836,7 @@ "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "gulp": { "version": "4.0.1", @@ -3572,6 +3581,14 @@ "dev": true, "optional": true }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -5920,6 +5937,11 @@ "through2-filter": "^3.0.0" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 3adc6f9f..67d04b20 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "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", "path": "^0.12.7", @@ -33,7 +34,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js", - "wipe": "node reset.js && gulp" + "wipe": "node wipe.js && gulp" }, "author": "", "license": "ISC" diff --git a/server.js b/server.js index 85860e4d..eb8f1412 100644 --- a/server.js +++ b/server.js @@ -37,11 +37,17 @@ const express = require('express') })); // session store + app.set('trust proxy', 1); app.use(session({ secret: configs.sessionSecret, store: new MongoStore({ db: Mongo.client.db('sessions') }), resave: false, - saveUninitialized: false + saveUninitialized: false, + cookie: { + httpOnly: true, + secure: true, + sameSite: 'lax', + } })); app.use(cookieParser()); @@ -53,7 +59,7 @@ const express = require('express') if (req.method !== 'POST') { return next(); } - if (!req.headers.referer || !req.headers.referer.startsWith('https://fatpeople.lol')) { + if (!req.headers.referer || !req.headers.referer.match(/^https:\/\/(www\.)?fatpeople\.lol/)) { return res.status(403).render('message', { 'title': 'Forbidden', 'message': 'Invalid or missing "Referer" header. Are you posting from the correct URL?' @@ -89,8 +95,38 @@ const express = require('express') }) // listen - app.listen(configs.port, () => { + const server = app.listen(configs.port, '127.0.0.1', () => { + 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') + process.send('ready'); + } + }); + process.on('SIGINT', () => { + + 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) + if (err) { + console.error(err); + process.exit(1); + } + + // close database connection + Mongo.client.close(); + + // now close without error + process.exit(0); + + }) + }) + })(); diff --git a/views/includes/actionfooter.pug b/views/includes/actionfooter.pug index 1a1bf0ea..f8255dd5 100644 --- a/views/includes/actionfooter.pug +++ b/views/includes/actionfooter.pug @@ -1,57 +1,56 @@ -label.toggle-label Toggle Post Actions - input.toggle(type='checkbox') - .action-wrapper.togglable - .actions - h4.no-m-p Actions: - 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 - label - input.post-check(type='checkbox', name='spoiler' value=1) - | Spoiler Images - label - input#password(type='text', name='password', placeholder='post password' autocomplete='off') - label - input.post-check(type='checkbox', name='report' value=1) - | Report - label - input.post-check(type='checkbox', name='global_report' value=1) - | Global Report - label - input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') - .actions - h4.no-m-p Mod Actions: - label - input.post-check(type='checkbox', name='delete_ip_board' value=1) - | Delete from IP on board - label - input.post-check(type='checkbox', name='delete_ip_global' value=1) - | Delete from IP globally - label - input.post-check(type='checkbox', name='sticky' value=1) - | Sticky - label - input.post-check(type='checkbox', name='lock' value=1) - | Lock - label - input.post-check(type='checkbox', name='sage' value=1) - | Sage - label - input.post-check(type='checkbox', name='ban' value=1) - | Ban Poster - label - input.post-check(type='checkbox', name='global_ban' value=1) - | Global Ban Poster - label - input.post-check(type='checkbox', name='preserve_post' value=1) - | Show Post In Ban - label - input#report(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') - .actions - h4.no-m-p Captcha: - 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='submit') +details.toggle-label + summary Show Post Actions + .actions + h4.no-m-p Actions: + 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 + label + input.post-check(type='checkbox', name='spoiler' value=1) + | Spoiler Images + label + input#password(type='text', name='password', placeholder='post password' autocomplete='off') + label + input.post-check(type='checkbox', name='report' value=1) + | Report + label + input.post-check(type='checkbox', name='global_report' value=1) + | Global Report + label + input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') + .actions + h4.no-m-p Mod Actions: + label + input.post-check(type='checkbox', name='delete_ip_board' value=1) + | Delete from IP on board + label + input.post-check(type='checkbox', name='delete_ip_global' value=1) + | Delete from IP globally + label + input.post-check(type='checkbox', name='sticky' value=1) + | Sticky + label + input.post-check(type='checkbox', name='lock' value=1) + | Lock + label + input.post-check(type='checkbox', name='sage' value=1) + | Sage + label + input.post-check(type='checkbox', name='ban' value=1) + | Ban Poster + label + input.post-check(type='checkbox', name='global_ban' value=1) + | Global Ban Poster + label + input.post-check(type='checkbox', name='preserve_post' value=1) + | Show Post In Ban + label + input#report(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') + .actions + h4.no-m-p Captcha: + 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='submit') diff --git a/views/includes/actionfooter_globalmanage.pug b/views/includes/actionfooter_globalmanage.pug index d91014e8..c289a50e 100644 --- a/views/includes/actionfooter_globalmanage.pug +++ b/views/includes/actionfooter_globalmanage.pug @@ -1,32 +1,28 @@ -label.toggle-label Toggle Post Actions - input.toggle(type='checkbox') - .action-wrapper.togglable - .actions - h4.no-m-p Actions: - 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 - label - input.post-check(type='checkbox', name='spoiler' value=1) - | Spoiler Images - label - input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') - label - input.post-check(type='checkbox', name='delete_ip_global' value=1) - | Delete from IP globally - label - input.post-check(type='checkbox', name='global_dismiss' value=1) - | Dismiss Global Reports - label - input.post-check(type='checkbox', name='global_ban' value=1) - | Global Ban Poster - label - input.post-check(type='checkbox', name='preserve_post' value=1) - | Show Post In Ban - label - input#ban_reason(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') - input(type='submit', value='submit') - +details.toggle-label + summary Show Post Actions + .actions + h4.no-m-p Actions: + 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 + label + input.post-check(type='checkbox', name='spoiler' value=1) + | Spoiler Images + label + input.post-check(type='checkbox', name='delete_ip_global' value=1) + | Delete from IP globally + label + input.post-check(type='checkbox', name='global_dismiss' value=1) + | Dismiss Global Reports + label + input.post-check(type='checkbox', name='global_ban' value=1) + | Global Ban Poster + label + input.post-check(type='checkbox', name='preserve_post' value=1) + | Show Post In Ban + label + input#ban_reason(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') + input(type='submit', value='submit') diff --git a/views/includes/actionfooter_manage.pug b/views/includes/actionfooter_manage.pug index aeb1aeb4..68163047 100644 --- a/views/includes/actionfooter_manage.pug +++ b/views/includes/actionfooter_manage.pug @@ -1,36 +1,39 @@ -label.toggle-label Toggle Post Actions - input.toggle(type='checkbox') - .action-wrapper.togglable - .actions - h4.no-m-p Actions: - 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 - label - input.post-check(type='checkbox', name='spoiler' value=1) - | Spoiler Images - label - input.post-check(type='checkbox', name='delete_ip_board' value=1) - | Delete from IP on board - label - input.post-check(type='checkbox', name='delete_ip_global' value=1) - | Delete from IP globally - label - input.post-check(type='checkbox', name='dismiss' value=1) - | Dismiss Reports - label - input.post-check(type='checkbox', name='ban' value=1) - | Ban Poster - label - input.post-check(type='checkbox', name='global_ban' value=1) - | Global Ban Poster - label - input.post-check(type='checkbox', name='preserve_post' value=1) - | Show Post In Ban - label - input#ban_reason(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') - input(type='submit', value='submit') - +details.toggle-label + summary Show Post Actions + .actions + h4.no-m-p Actions: + 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 + label + input.post-check(type='checkbox', name='spoiler' value=1) + | Spoiler Images + label + input.post-check(type='checkbox', name='global_report' value=1) + | Global Report + label + input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') + label + input.post-check(type='checkbox', name='delete_ip_board' value=1) + | Delete from IP on board + label + input.post-check(type='checkbox', name='delete_ip_global' value=1) + | Delete from IP globally + label + input.post-check(type='checkbox', name='dismiss' value=1) + | Dismiss Reports + label + input.post-check(type='checkbox', name='ban' value=1) + | Ban Poster + label + input.post-check(type='checkbox', name='global_ban' value=1) + | Global Ban Poster + label + input.post-check(type='checkbox', name='preserve_post' value=1) + | Show Post In Ban + label + input#ban_reason(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') + input(type='submit', value='submit') diff --git a/views/includes/boardheader.pug b/views/includes/boardheader.pug index b49231b9..5426d559 100644 --- a/views/includes/boardheader.pug +++ b/views/includes/boardheader.pug @@ -1,6 +1,6 @@ section.board-header - if board.banners.length > 0 - object.board-banner(data=`/banners?board=${board._id}` width='300' height='100') - a.no-decoration(href=`/${board._id}/index`) + object.board-banner(data=`/banners?board=${board._id}` width='300' height='100') + br + a.no-decoration(href=`/${board._id}/index.html`) h1.board-title /#{board._id}/ - #{board.name} h4.board-description #{board.description} diff --git a/views/includes/footer.pug b/views/includes/footer.pug index be072f5d..25ab0523 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/') not lynxchan™ diff --git a/views/includes/navbar.pug b/views/includes/navbar.pug index bd6bfef8..7e430185 100644 --- a/views/includes/navbar.pug +++ b/views/includes/navbar.pug @@ -1,8 +1,5 @@ nav.navbar a.nav-item(href='/') Home a.nav-item.right(href='/logout') Logout - if board - a.nav-item.right(href=`/login?redirect=/${board._id}/index`) Login - a.nav-item.right(href=`/${board._id}/manage`) Manage - else - a.nav-item.right(href='/login') Login + a.nav-item.right(href=`/${board ? board._id+'/' : 'global'}manage.html`) Manage + a.nav-item.right(href='/login.html') Login diff --git a/views/includes/pages.pug b/views/includes/pages.pug index d8382215..d9456ea4 100644 --- a/views/includes/pages.pug +++ b/views/includes/pages.pug @@ -1,15 +1,15 @@ | Page: -span - a(href=`/${board._id}/index`) [#{1}] +if page === 1 + a(href=`/${board._id}/index.html`) [#{1}] | -- for(let i = 2; i <= pages; i++) +else + a(href=`/${board._id}/index.html`) #{1} + | +- for(let i = 2; i <= maxPage; i++) if i === page - span - a(href=`/${board._id}/${i}`) [#{i}] - | - + a(href=`/${board._id}/${i}.html`) [#{i}] + | else - span - a(href=`/${board._id}/${i}`) #{i} - | + a(href=`/${board._id}/${i}.html`) #{i} + | | | diff --git a/views/includes/postform.pug b/views/includes/postform.pug index 658d2e37..0dac0410 100644 --- a/views/includes/postform.pug +++ b/views/includes/postform.pug @@ -1,45 +1,47 @@ -section.form-wrapper.flex-center.mv-10 - 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') - 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.form-wrapper.flex-center + details.toggle-label + 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') 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') - 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 - section.postform-row - .postform-label Password - input#password(type='password', name='password', autocomplete='off' placeholder='password for deleting post later' maxlength='50') - 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') - if !thread - input(type='submit', value='New Thread') - else - input(type='submit', value='Reply') + 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 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 + 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'} diff --git a/views/mixins/catalogtile.pug b/views/mixins/catalogtile.pug index fc0ca6b1..24155d6a 100644 --- a/views/mixins/catalogtile.pug +++ b/views/mixins/catalogtile.pug @@ -1,23 +1,30 @@ mixin catalogtile(board, post, truncate) article(class='catalog-tile') - - const postURL = `/${board._id}/thread/${post.postId}#${post.postId}` + - 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=`/${board._id}/thread/${post.postId}#${post.postId}`) + 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 - span: a(href=postURL) ##{post.postId} + 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') + | + span: a(href=postURL) No.#{post.postId} | span Replies: #{post.replyposts} | span Images: #{post.replyfiles} - br if post.message + br blockquote.no-m-p.post-message !{post.message} diff --git a/views/mixins/post.pug b/views/mixins/post.pug index 1116335d..a8ae0fd3 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -1,6 +1,6 @@ mixin post(post, truncate, manage=false, globalmanage=false) article(id=post.postId class='post-container '+(post.thread ? '' : 'op')) - - const postURL = `/${post.board}/thread/${post.thread || post.postId}#${post.postId}`; + - const postURL = `/${post.board}/thread/${post.thread || post.postId}.html#${post.postId}`; header.post-info if globalmanage input.post-check(type='checkbox', name='globalcheckedposts[]' value=post._id) @@ -32,7 +32,7 @@ mixin post(post, truncate, manage=false, globalmanage=false) | span #{post.date.toLocaleString()} | - if post.userId && board.settings.ids + if post.userId span.user-id(style=`background: #${post.userId}`) #{post.userId} | span: a(href=postURL) No.#{post.postId} @@ -41,7 +41,7 @@ mixin post(post, truncate, manage=false, globalmanage=false) .post-files each file in post.files .post-file - small.post-file-info + span.post-file-info span: a(href='/img/'+file.filename title=file.originalFilename download=file.originalFilename) #{file.originalFilename} br span @@ -50,11 +50,13 @@ mixin post(post, truncate, manage=false, globalmanage=false) | , #{file.durationString} | ) .post-file-src - a(target='_blank' href='/img/'+file.filename) + a(target='_blank' href=`/img/${file.filename}`) if post.spoiler - object(data='/img/spoiler.png' width='128' height='128') + 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`) else - object(data=`/img/thumb-${file.filename.split('.')[0]}.jpg`) + object.file-thumb(data=`/img/${file.filename}`) if post.message if truncate - diff --git a/views/pages/board.pug b/views/pages/board.pug index 7bf5447f..5615b93d 100644 --- a/views/pages/board.pug +++ b/views/pages/board.pug @@ -6,16 +6,17 @@ block head block content include ../includes/boardheader.pug + br include ../includes/postform.pug - .mode Posting mode: Thread + br nav.pages#top include ../includes/pages.pug a(href='#bottom') [Bottom] | - a(href=`/${board._id}/catalog`) [Catalog] + 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) if threads.length === 0 p No posts. hr(size=1) @@ -26,7 +27,9 @@ block content +post(post, true) hr(size=1) nav.pages#bottom + include ../includes/pages.pug a(href='#top') [Top] | - a(href=`/${board._id}/catalog`) [Catalog] + a(href=`/${board._id}/catalog.html`) [Catalog] + br include ../includes/actionfooter.pug diff --git a/views/pages/catalog.pug b/views/pages/catalog.pug index fbfa6a2b..945732bb 100644 --- a/views/pages/catalog.pug +++ b/views/pages/catalog.pug @@ -6,10 +6,11 @@ block head block content include ../includes/boardheader.pug + br nav.pages#top a(href='#bottom') [Bottom] | - a(href=`/${board._id}/index`) [Return] + a(href=`/${board._id}/index.html`) [Return] hr(size=1) if threads.length === 0 p No posts. @@ -20,4 +21,4 @@ block content nav.pages#bottom a(href='#top') [Top] | - a(href=`/${board._id}/index`) [Return] + a(href=`/${board._id}/index.html`) [Return] diff --git a/views/pages/login.pug b/views/pages/login.pug index 9ea16120..b9f19ca2 100644 --- a/views/pages/login.pug +++ b/views/pages/login.pug @@ -6,8 +6,7 @@ block head block content section.form-wrapper.flex-center.mv-10 form.form-post(action='/forms/login' method='POST') - input(type='hidden' name='_csrf' value=csrf) - input(type='hidden' name='redirect' value=redirect) + //input(type='hidden' name='_csrf' value=csrf) section.postform-row .postform-label Username input#username(type='text', name='username', maxlength='50') @@ -15,5 +14,5 @@ block content .postform-label Password input#password(type='password', name='password', maxlength='100') input(type='submit', value='submit') - p No account? #[a(href='/register') Register] + p No account? #[a(href='/register.html') Register] diff --git a/views/pages/manage.pug b/views/pages/manage.pug index 235aa902..43417353 100644 --- a/views/pages/manage.pug +++ b/views/pages/manage.pug @@ -7,6 +7,7 @@ block head block content include ../includes/boardheader.pug + br h4.no-m-p Settings: section.form-wrapper.flexleft.mv-10 form.form-post(action=`/forms/board/${board._id}/settings` method='POST' enctype='application/x-www-form-urlencoded') @@ -21,9 +22,28 @@ block content label.postform-style.ph-5 input(type='checkbox', name='force_anon', value='true' checked=board.settings.forceAnon) | Disable names and only allow sage email + section.postform-row + .postform-label Post Captcha + label.postform-style.ph-5 + input(type='checkbox', name='captcha', value='true' checked=board.settings.captcha) + section.postform-row + .postform-label Force OP Message + label.postform-style.ph-5 + input(type='checkbox', name='force_op_message', value='true' checked=board.settings.forceOPMessage) + section.postform-row + .postform-label Force OP Subject + label.postform-style.ph-5 + input(type='checkbox', name='force_op_subject', value='true' checked=board.settings.forceOPSubject) + section.postform-row + .postform-label Force OP File + label.postform-style.ph-5 + input(type='checkbox', name='force_op_file', value='true' checked=board.settings.forceOPFile) section.postform-row .postform-label Anon Name input(type='text' name='default_name' placeholder=board.settings.defaultName) + section.postform-row + .postform-label Min Message Length + input(type='text' name='min_message_length' placeholder=board.settings.minMessageLength) section.postform-row .postform-label Thread Limit input(type='text' name='thread_limit' placeholder=board.settings.threadLimit) diff --git a/views/pages/register.pug b/views/pages/register.pug index 0824a292..e57f0c99 100644 --- a/views/pages/register.pug +++ b/views/pages/register.pug @@ -22,4 +22,4 @@ block content 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='Register') - p Already have an account? #[a(href='/login') Login] + p Already have an account? #[a(href='/login.html') Login] diff --git a/views/pages/thread.pug b/views/pages/thread.pug index 71b8d363..035c26c3 100644 --- a/views/pages/thread.pug +++ b/views/pages/thread.pug @@ -11,14 +11,15 @@ block head block content include ../includes/boardheader.pug + br include ../includes/postform.pug - .mode Posting mode: Reply + br nav.pages#top a(href='#bottom') [Bottom] | - a(href=`/${board._id}/index`) [Return] + a(href=`/${board._id}/index.html`) [Return] | - a(href=`/${board._id}/catalog`) [Catalog] + 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) @@ -30,7 +31,8 @@ block content nav.pages#bottom a(href='#top') [Top] | - a(href=`/${board._id}/index`) [Return] + a(href=`/${board._id}/index.html`) [Return] | - a(href=`/${board._id}/catalog`) [Catalog] + a(href=`/${board._id}/catalog.html`) [Catalog] + br include ../includes/actionfooter.pug diff --git a/wipe.js b/wipe.js index 0446e250..e35fe808 100644 --- a/wipe.js +++ b/wipe.js @@ -24,14 +24,16 @@ const Mongo = require(__dirname+'/db/db.js') console.log('deleting posts') await Posts.deleteAll('pol'); await Posts.deleteAll('b'); + await Posts.deleteAll('t'); console.log('deleting boards') await Boards.deleteIncrement('pol'); - await Boards.deleteIncrement('b'); + await Boards.deleteIncrement('b'); + await Boards.deleteIncrement('t'); await Boards.deleteAll(); await Trips.deleteAll(); console.log('deleting bans'); await Bans.deleteAll(); - console.log('adding b and pol') + console.log('adding boards') await Boards.insertOne({ _id: 'pol', name: 'Politically Incorrect', @@ -40,11 +42,16 @@ const Mongo = require(__dirname+'/db/db.js') moderators: [], banners: [], settings: { + captcha: false, forceAnon: true, ids: true, threadLimit: 100, replyLimit: 300, maxFiles: 3, + forceOPSubject: false, + forceOPMessage: true, + forceOPFile: true, + minMessageLength: 0, defaultName: 'Anonymous', } }) @@ -56,11 +63,37 @@ const Mongo = require(__dirname+'/db/db.js') moderators: [], banners: [], settings: { + captcha: true, forceAnon: false, ids: false, threadLimit: 100, replyLimit: 300, maxFiles: 3, + forceOPSubject: false, + forceOPMessage: true, + forceOPFile: true, + minMessageLength: 0, + defaultName: 'Anonymous', + } + }) + await Boards.insertOne({ + _id: 't', + name: 'text', + description: 'text only board', + owner: '', + moderators: [], + banners: [], + settings: { + captcha: true, + forceAnon: true, + ids: false, + threadLimit: 100, + replyLimit: 300, + maxFiles: 0, + forceOPSubject: false, + forceOPMessage: true, + forceOPFile: true, + minMessageLength: 0, defaultName: 'Anonymous', } }) @@ -99,28 +132,9 @@ const Mongo = require(__dirname+'/db/db.js') } } }); - await readdir('static/img/').then(async files => { - await Promise.all(files.map(async file => { - unlink(path.join('static/img/', file)); - })) - }); - await readdir('static/captcha/').then(async files => { - await Promise.all(files.map(async file => { - unlink(path.join('static/captcha/', file)); - })) - }); - await readdir('static/banner/').then(async files => { - await Promise.all(files.map(async file => { - unlink(path.join('static/banner/', file)); - })) - }); - await readdir('static/html/').then(async files => { - await Promise.all(files.map(async file => { - unlink(path.join('static/html/', file)); - })) - }); console.log('creating admin account: admin:changeme'); await Accounts.insertOne('admin', 'changeme', 3); + Mongo.client.close() console.log('done'); process.exit(0); })();