diff --git a/controllers/forms.js b/controllers/forms.js index e1e5e312..7b4189bd 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -35,6 +35,8 @@ const express = require('express') , removeBansController = require(__dirname+'/forms/removebans.js') , globalActionController = require(__dirname+'/forms/globalactions.js') , actionController = require(__dirname+'/forms/actions.js') + , addNewsController = require(__dirname+'/forms/addnews.js') + , deleteNewsController = require(__dirname+'/forms/deletenews.js') , uploadBannersController = require(__dirname+'/forms/uploadbanners.js') , deleteBannersController = require(__dirname+'/forms/deletebanners.js') , boardSettingsController = require(__dirname+'/forms/boardsettings.js') @@ -79,6 +81,10 @@ router.post('/board/:board/deletebanners', csrf, Boards.exists, calcPerms, banCh router.post('/global/unban', csrf, calcPerms, isLoggedIn, hasPerms(1), paramConverter, removeBansController); router.post('/board/:board/unban', csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), paramConverter, removeBansController); +//news +router.post('/global/addnews', csrf, calcPerms, isLoggedIn, hasPerms(0), addNewsController); +router.post('/global/deletenews', csrf, calcPerms, isLoggedIn, hasPerms(0), paramConverter, deleteNewsController); + //delete board router.post('/board/:board/deleteboard', csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), deleteBoardController); router.post('/global/deleteboard', csrf, calcPerms, isLoggedIn, hasPerms(1), deleteBoardController); diff --git a/controllers/forms/addnews.js b/controllers/forms/addnews.js new file mode 100644 index 00000000..fdf8e5f8 --- /dev/null +++ b/controllers/forms/addnews.js @@ -0,0 +1,36 @@ +'use strict'; + +const addNews = require(__dirname+'/../../models/forms/addnews.js') + +module.exports = async (req, res, next) => { + + const errors = []; + + if (!req.body.message || req.body.message.length === 0) { + errors.push('Missing message'); + } + if (req.body.message.length > 10000) { + errors.push('Message must be 10000 characters or less'); + } + if (!req.body.title || req.body.title.length === 0) { + errors.push('Missing title'); + } + if (req.body.title.length > 50) { + errors.push('Title must be 50 characters or less'); + } + + if (errors.length > 0) { + return res.status(400).render('message', { + 'title': 'Bad request', + 'errors': errors, + 'redirect': '/globalmanage.html' + }); + } + + try { + await addNews(req, res, next); + } catch (err) { + return next(err); + } + +} diff --git a/controllers/forms/deletenews.js b/controllers/forms/deletenews.js new file mode 100644 index 00000000..5f515721 --- /dev/null +++ b/controllers/forms/deletenews.js @@ -0,0 +1,27 @@ +'use strict'; + +const deleteNews = require(__dirname+'/../../models/forms/deletenews.js'); + +module.exports = async (req, res, next) => { + + const errors = []; + + if (!req.body.checkednews || req.body.checkednews.length === 0 || req.body.checkednews.length > 10) { + errors.push('Must select 1-10 newsposts delete'); + } + + if (errors.length > 0) { + return res.status(400).render('message', { + 'title': 'Bad request', + 'errors': errors, + 'redirect': `/${req.params.board}/manage.html` + }) + } + + try { + await deleteNews(req, res, next); + } catch (err) { + return next(err); + } + +} diff --git a/controllers/pages.js b/controllers/pages.js index 26f7fd2e..db74319d 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -21,6 +21,7 @@ const express = require('express') , catalog = require(__dirname+'/../models/pages/catalog.js') , banners = require(__dirname+'/../models/pages/banners.js') , randombanner = require(__dirname+'/../models/pages/randombanner.js') + , news = require(__dirname+'/../models/pages/news.js') , captchaPage = require(__dirname+'/../models/pages/captchapage.js') , captcha = require(__dirname+'/../models/pages/captcha.js') , thread = require(__dirname+'/../models/pages/thread.js'); @@ -52,6 +53,9 @@ router.get('/create.html', isLoggedIn, csrf, create); //registration page router.get('/register.html', register); +//news page +router.get('/news.html', news); + //captcha page router.get('/captcha.html', captchaPage); diff --git a/db/news.js b/db/news.js new file mode 100644 index 00000000..285a0c3e --- /dev/null +++ b/db/news.js @@ -0,0 +1,31 @@ + +'use strict'; + +const Mongo = require(__dirname+'/db.js') + , db = Mongo.client.db('jschan').collection('news'); + +module.exports = { + + find: () => { + return db.find({}).sort({ + '_id': -1 + }).toArray(); + }, + + insertOne: (news) => { + return db.insertOne(news); + }, + + deleteMany: (ids) => { + return db.deleteMany({ + '_id': { + '$in': ids + } + }) + }, + + deleteAll: () => { + return db.deleteMany({}); + }, + +} diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index 09a5588c..422e6482 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -153,6 +153,9 @@ pre { .mv-10 { margin: 10px 0; } +.mv-5 { + margin: 5px 0; +} .mv-0 { margin: 0 auto; } @@ -326,9 +329,8 @@ table { width: 600px; } -th { +.table-head, th { background: var(--label-color); - /*text-align: left;*/ } td, th { diff --git a/helpers/build.js b/helpers/build.js index 1bc9eb6d..0370cfc2 100644 --- a/helpers/build.js +++ b/helpers/build.js @@ -5,6 +5,7 @@ const Mongo = require(__dirname+'/../db/db.js') , Posts = require(__dirname+'/../db/posts.js') , Files = require(__dirname+'/../db/files.js') , Boards = require(__dirname+'/../db/boards.js') + , News = require(__dirname+'/../db/news.js') , formatSize = require(__dirname+'/files/formatsize.js') , uploadDirectory = require(__dirname+'/files/uploadDirectory.js') , render = require(__dirname+'/render.js'); @@ -102,6 +103,16 @@ module.exports = { console.timeEnd(label); }, + buildNews: async () => { + const label = '/news.html'; + console.time(label); + const news = await News.find(); + await render('news.html', 'news.pug', { + news + }); + console.timeEnd(label); + }, + buildHomepage: async () => { const label = '/index.html'; console.time(label); diff --git a/helpers/paramconverter.js b/helpers/paramconverter.js index a07e0a7b..24b9de11 100644 --- a/helpers/paramconverter.js +++ b/helpers/paramconverter.js @@ -1,7 +1,7 @@ 'use strict'; const { ObjectId } = require(__dirname+'/../db/db.js') - , allowedArrays = new Set(['checkedposts', 'globalcheckedposts', 'checkedbans', 'checkedbanners']) //only these can be arrays, since express bodyparser will output arrays + , allowedArrays = new Set(['checkednews', 'checkedposts', 'globalcheckedposts', 'checkedbans', 'checkedbanners']) //only these can be arrays, since express bodyparser will output arrays , trimFields = ['uri', 'moderators', 'filters', 'announcement', 'description', 'message', 'name', 'subject', 'email', 'password', 'default_name', 'report_reason', 'ban_reason'] //trim if we dont want filed with whitespace , numberFields = ['filter_mode', 'captcha_mode', 'tph_trigger', 'tph_trigger_action', 'reply_limit', @@ -55,6 +55,9 @@ module.exports = (req, res, next) => { if (req.body.globalcheckedposts) { req.body.globalcheckedposts = req.body.globalcheckedposts.map(ObjectId) } + if (req.body.checkednews) { + req.body.checkednews = req.body.checkednews.map(ObjectId) + } //convert checked bans to mongoid if (req.body.checkedbans) { req.body.checkedbans = req.body.checkedbans.map(ObjectId) diff --git a/helpers/posting/quotes.js b/helpers/posting/quotes.js index 8552cce0..30dd7997 100644 --- a/helpers/posting/quotes.js +++ b/helpers/posting/quotes.js @@ -18,7 +18,7 @@ module.exports = async (board, text, thread) => { const postQueryOrs = [] const boardQueryIns = [] const crossQuoteMap = {}; - if (quotes) { + if (quotes && board) { const quoteIds = [...new Set(quotes.map(q => { return Number(q.substring(8)) }))]; postQueryOrs.push({ 'board': board, diff --git a/models/forms/addnews.js b/models/forms/addnews.js new file mode 100644 index 00000000..e117121b --- /dev/null +++ b/models/forms/addnews.js @@ -0,0 +1,38 @@ +'use strict'; + +const News = require(__dirname+'/../../db/news.js') + , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') + , { buildNews } = require(__dirname+'/../../helpers/build.js') + , linkQuotes = require(__dirname+'/../../helpers/posting/quotes.js') + , simpleMarkdown = require(__dirname+'/../../helpers/posting/markdown.js') + , escape = require(__dirname+'/../../helpers/posting/escape.js') + , sanitizeOptions = require(__dirname+'/../../helpers/posting/sanitizeoptions.js') + , sanitize = require('sanitize-html'); + +module.exports = async (req, res, next) => { + + const escaped = escape(req.body.message); + const styled = simpleMarkdown(escaped); + const quoted = (await linkQuotes(null, styled, null)).quotedMessage; + const sanitized = sanitize(quoted, sanitizeOptions.after); + + const post = { + 'title': req.body.title, + 'message': { + 'raw': req.body.message, + 'markdown': sanitized + }, + 'date': new Date(), + }; + + await News.insertOne(post); + + await buildNews(); + + return res.render('message', { + 'title': 'Success', + 'message': 'Added newspost', + 'redirect': '/globalmanage.html' + }); + +} diff --git a/models/forms/deletenews.js b/models/forms/deletenews.js new file mode 100644 index 00000000..429d241c --- /dev/null +++ b/models/forms/deletenews.js @@ -0,0 +1,18 @@ +'use strict'; + +const News = require(__dirname+'/../../db/news.js') + , { buildNews } = require(__dirname+'/../../helpers/build.js') + +module.exports = async (req, res, next) => { + + await News.deleteMany(req.body.checkednews); + + await buildNews(); + + return res.render('message', { + 'title': 'Success', + 'message': 'Deleted news', + 'redirect': '/globalmanage.html' + }); + +} diff --git a/models/pages/globalmanage.js b/models/pages/globalmanage.js index f0dfa034..38bb6d27 100644 --- a/models/pages/globalmanage.js +++ b/models/pages/globalmanage.js @@ -1,15 +1,18 @@ 'use strict'; const Posts = require(__dirname+'/../../db/posts.js') - , Bans = require(__dirname+'/../../db/bans.js'); + , Bans = require(__dirname+'/../../db/bans.js') + , News = require(__dirname+'/../../db/news.js'); module.exports = async (req, res, next) => { - let reports; - let bans; + let reports, bans, news; try { - reports = await Posts.getGlobalReports(); - bans = await Bans.getGlobalBans(); + [ reports, bans, news ] = await Promise.all([ + Posts.getGlobalReports(), + Bans.getGlobalBans(), + News.find() + ]); } catch (err) { return next(err) } @@ -19,6 +22,7 @@ module.exports = async (req, res, next) => { csrf: req.csrfToken(), reports, bans, + news, }); } diff --git a/models/pages/news.js b/models/pages/news.js new file mode 100644 index 00000000..dce614d1 --- /dev/null +++ b/models/pages/news.js @@ -0,0 +1,16 @@ +'use strict'; + +const { buildNews } = require(__dirname+'/../../helpers/build.js') + , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js'); + +module.exports = async (req, res, next) => { + + try { + await buildNews(); + } catch (err) { + return next(err); + } + + return res.sendFile(`${uploadDirectory}html/news.html`); + +} diff --git a/schedules.js b/schedules.js index 4e3b26e4..42636f75 100644 --- a/schedules.js +++ b/schedules.js @@ -37,7 +37,6 @@ async function deleteCaptchas() { console.log('Starting schedules'); -await buildHomepage(); setInterval(async () => { try { await buildHomepage(); diff --git a/views/includes/navbar.pug b/views/includes/navbar.pug index 05f44a8c..ed2e90e9 100644 --- a/views/includes/navbar.pug +++ b/views/includes/navbar.pug @@ -1,5 +1,6 @@ nav.navbar#top a.nav-item(href='/') Home + a.nav-item(href='/news.html') News a.nav-item(href=`/${board ? board._id+'/' : 'global'}manage.html`) Manage a.nav-item(href='/create.html') Create a.nav-item.right(href='/logout') Logout diff --git a/views/mixins/newspost.pug b/views/mixins/newspost.pug new file mode 100644 index 00000000..64c1e417 --- /dev/null +++ b/views/mixins/newspost.pug @@ -0,0 +1,16 @@ +mixin newspost(post, globalmanage=false) + .table-container.flex-center.mv-5 + .anchor(id=post._id) + table.table-body + tr.table-head + th + if globalmanage === true + input.left.post-check(type='checkbox', name='checkednews[]' value=post._id) + a.left(href=`#${post._id}`) #{post.title} + p.right.no-m-p #{post.date.toLocaleString()} + tr.table-row + td + if globalmanage === true + p.no-m-p #{`${post.message.raw.substring(0,50)}...`} + else + pre.post-message.no-m-p !{post.message.markdown} diff --git a/views/pages/globalmanage.pug b/views/pages/globalmanage.pug index a019ace8..5457afa6 100644 --- a/views/pages/globalmanage.pug +++ b/views/pages/globalmanage.pug @@ -1,6 +1,7 @@ extends ../layout.pug include ../mixins/post.pug include ../mixins/ban.pug +include ../mixins/newspost.pug block head title Manage @@ -8,6 +9,29 @@ block head block content h1.board-title Global Management hr(size=1) + h4.no-m-p Add News: + section.form-wrapper.flexleft.mv-10 + form.form-post(action=`/forms/global/addnews`, enctype='application/x-www-form-urlencoded', method='POST') + input(type='hidden' name='_csrf' value=csrf) + section.row + .label Title + input(type='text' name='title' required) + section.row + .label Message + textarea(name='message' placeholder='supports post styling' required) + input(type='submit', value='submit') + hr(size=1) + if news.length > 0 + h4.no-m-p Delete News: + section.form-wrapper.flexleft.mv-10 + form.form-post(action=`/forms/global/deletenews`, enctype='application/x-www-form-urlencoded', method='POST') + input(type='hidden' name='_csrf' value=csrf) + each post in news + +newspost(post, true) + if news.length === 1 + .anchor + input(type='submit', value='delete') + hr(size=1) h4.no-m-p Delete board: section.form-wrapper.flexleft.mv-10 form.form-post(action=`/forms/global/deleteboard`, enctype='application/x-www-form-urlencoded', method='POST') diff --git a/views/pages/news.pug b/views/pages/news.pug new file mode 100644 index 00000000..d487f083 --- /dev/null +++ b/views/pages/news.pug @@ -0,0 +1,12 @@ +extends ../layout.pug +include ../mixins/newspost.pug + +block head + title News + +block content + h1.board-title News + if news.length === 0 + p.text-center No news. + each post in news + +newspost(post)