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