Merge pull request #27 from fatchan/html-generate

merge-requests/208/head
Tom 5 years ago committed by GitHub
commit 2ccafcdb06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 97
      build.js
  2. 93
      controllers/forms.js
  3. 21
      controllers/pages.js
  4. 2
      db/boards.js
  5. 72
      db/posts.js
  6. 3
      ecosystem.config.js
  7. 41
      gulp/res/css/style.css
  8. 11
      helpers/captchaverify.js
  9. 7
      helpers/files/deletefailed.js
  10. 8
      helpers/files/deletepostfiles.js
  11. 9
      helpers/files/file-check-mime-types.js
  12. 19
      helpers/files/imageupload.js
  13. 5
      helpers/files/videoupload.js
  14. 2
      helpers/isloggedin.js
  15. 8
      helpers/markdown.js
  16. 2
      helpers/paramconverter.js
  17. 8
      helpers/quotes.js
  18. 12
      helpers/render.js
  19. 14
      helpers/writepagehtml.js
  20. 160
      models/forms/actionhandler.js
  21. 20
      models/forms/changepassword.js
  22. 8
      models/forms/delete-post.js
  23. 10
      models/forms/deletebanners.js
  24. 9
      models/forms/deletepostsfiles.js
  25. 7
      models/forms/login.js
  26. 78
      models/forms/make-post.js
  27. 4
      models/forms/register.js
  28. 5
      models/forms/report-post.js
  29. 4
      models/forms/spoiler-post.js
  30. 3
      models/forms/stickyposts.js
  31. 47
      models/forms/uploadbanners.js
  32. 20
      models/pages/board.js
  33. 12
      models/pages/catalog.js
  34. 14
      models/pages/changepassword.js
  35. 9
      models/pages/home.js
  36. 17
      models/pages/login.js
  37. 2
      models/pages/manage.js
  38. 16
      models/pages/register.js
  39. 17
      models/pages/thread.js
  40. 26
      package-lock.json
  41. 3
      package.json
  42. 42
      server.js
  43. 113
      views/includes/actionfooter.pug
  44. 60
      views/includes/actionfooter_globalmanage.pug
  45. 75
      views/includes/actionfooter_manage.pug
  46. 6
      views/includes/boardheader.pug
  47. 2
      views/includes/footer.pug
  48. 7
      views/includes/navbar.pug
  49. 20
      views/includes/pages.pug
  50. 86
      views/includes/postform.pug
  51. 15
      views/mixins/catalogtile.pug
  52. 14
      views/mixins/post.pug
  53. 11
      views/pages/board.pug
  54. 5
      views/pages/catalog.pug
  55. 5
      views/pages/login.pug
  56. 20
      views/pages/manage.pug
  57. 2
      views/pages/register.pug
  58. 12
      views/pages/thread.pug
  59. 58
      wipe.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');
},
}

@ -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`
});
});

@ -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;

@ -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'
});
},

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

@ -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'
},

@ -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 {

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

@ -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}`)
}));
}

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

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

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

@ -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) {

@ -4,5 +4,5 @@ module.exports = (req, res, next) => {
if (req.session.authenticated === true) {
return next();
}
res.redirect('/login');
res.redirect('/login.html');
}

@ -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 `<span class='redtext'>&lt;${redtext}</span>`;
//pinktext
text = text.replace(pinktextRegex, (match, pinktext) => {
return `<span class='pinktext'>&lt;${pinktext}</span>`;
});
//greentext

@ -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) => {

@ -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 `<a class='quote' href='/${board}/thread/${postThreadIdMap[board][quotenum]}#${quotenum}'>&gt;&gt;${quotenum}</a>`;
return `<a class='quote' href='/${board}/thread/${postThreadIdMap[board][quotenum]}.html#${quotenum}'>&gt;&gt;${quotenum}</a>`;
}
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 `<a class='quote' href='/${quoteboard}/thread/${postThreadIdMap[quoteboard][quotenum]}#${quotenum}'>&gt;&gt;&gt;/${quoteboard}/${quotenum}</a>`;
} else if (postThreadIdMap[quoteboard] && quotenum === 0) {
return `<a class='quote' href='/${quoteboard}/'>&gt;&gt;&gt;/${quoteboard}/</a>`;
return `<a class='quote' href='/${quoteboard}/thread/${postThreadIdMap[quoteboard][quotenum]}.html#${quotenum}'>&gt;&gt;&gt;/${quoteboard}/${quotenum}</a>`;
} else if (!quote[2]) {
return `<a class='quote' href='/${quoteboard}/index.html'>&gt;&gt;&gt;/${quoteboard}/</a>`;
}
return match;
});

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

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

@ -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}/`
});
}

@ -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');
}

@ -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) {

@ -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

@ -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`)
])
}));

@ -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'
});
}

@ -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 = /^(?<name>[^\s#]+)?(?:##(?<tripcode>[^ ]{1}[^\s#]+))?(?:## (?<capcode>[^\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);
}

@ -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')
}

@ -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
}
}
};

@ -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'
};
}

@ -16,7 +16,8 @@ module.exports = (posts) => {
message: `Stickied ${filteredposts.length} post(s)`,
action: '$set',
query: {
'sticky': true
'sticky': true,
'bumped': 8640000000000000
}
};

@ -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',

@ -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`);
}

@ -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`);
}

@ -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`);
}

@ -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`);
}

@ -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`);
}

@ -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) => {

@ -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`);
}

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

26
package-lock.json generated

@ -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",

@ -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"

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

@ -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')

@ -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')

@ -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')

@ -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}

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

@ -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

@ -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}
|
| |

@ -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'}

@ -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}

@ -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
-

@ -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

@ -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]

@ -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]

@ -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)

@ -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]

@ -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

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

Loading…
Cancel
Save