diff --git a/controllers/forms.js b/controllers/forms.js index c16a994a..a72cba22 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -55,6 +55,7 @@ const express = require('express') , registerController = require(__dirname+'/forms/register.js') , changePasswordController = require(__dirname+'/forms/changepassword.js') , editAccountsController = require(__dirname+'/forms/editaccounts.js') + , globalSetttingsController = require(__dirname+'/forms/globalsettings.js') , createBoardController = require(__dirname+'/forms/create.js') , makePostController = require(__dirname+'/forms/makepost.js') , newcaptcha = require(__dirname+'/../models/forms/newcaptcha.js') @@ -80,7 +81,7 @@ router.post('/global/editbans', sessionRefresh, csrf, calcPerms, isLoggedIn, has router.post('/global/addnews', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), addNewsController); //add new newspost router.post('/global/deletenews', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), paramConverter, deleteNewsController); //delete news router.post('/global/editaccounts', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), paramConverter, editAccountsController); //account editing -//router.post('/global/deleteboard', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(1), deleteBoardController); //delete board. removed and going to change when i add a board page to globalmanage with unlisted boards and searching +router.post('/global/setings', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), paramConverter, globalSettingsController); //global settings //accounts router.post('/login', loginController); diff --git a/controllers/forms/globalsettings.js b/controllers/forms/globalsettings.js new file mode 100644 index 00000000..61f6fe83 --- /dev/null +++ b/controllers/forms/globalsettings.js @@ -0,0 +1,43 @@ +'use strict'; + +const changeGlobalSettings = require(__dirname+'/../../models/forms/changeglobalsettings.js') + +module.exports = async (req, res, next) => { + + const errors = []; + + if (req.body.filters && req.body.filters.length > 2000) { + errors.push('Filters length must be less than 2000 characters'); + } + if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) { + errors.push('Invalid captcha mode.'); + } + if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) { + errors.push('Invalid filter mode.'); + } + if (typeof req.body.ban_duration === 'number' && req.body.ban_duration <= 0) { + errors.push('Invalid filter auto ban duration.'); + } + + if (errors.length > 0) { + return res.status(400).render('message', { + 'title': 'Bad request', + 'errors': errors, + 'redirect': '/globalmanage/settings.html' + }); + } + +//todo: implement this + return res.status(400).render('message', { + 'title': 'Bad request', + 'error': 'Not implemented', + 'redirect': '/globalmanage/settings.html' + }); + + try { + await changeGlobalSettings(req, res, next); + } catch (err) { + return next(err); + } + +} diff --git a/controllers/pages.js b/controllers/pages.js index 2efb76ef..33b36904 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -13,7 +13,7 @@ const express = require('express') , csrf = require(__dirname+'/../helpers/checks/csrfmiddleware.js') //page models , { manageReports, manageBanners, manageSettings, manageBans } = require(__dirname+'/../models/pages/manage/') - , { globalManageReports, globalManageBans, globalManageRecent, globalManageAccounts, globalManageNews } = require(__dirname+'/../models/pages/globalmanage/') + , { globalManageSettings, globalManageReports, globalManageBans, globalManageRecent, globalManageAccounts, globalManageNews } = require(__dirname+'/../models/pages/globalmanage/') , { changePassword, home, register, login, logout, create, board, catalog, banners, randombanner, news, captchaPage, captcha, thread, modlog, modloglist, account, boardlist } = require(__dirname+'/../models/pages/'); @@ -51,9 +51,10 @@ router.get('/:board/manage/thread/:id(\\d+).html', sessionRefresh, isLoggedIn, B //global manage pages router.get('/globalmanage/reports.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageReports); router.get('/globalmanage/bans.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageBans); -router.get('/globalmanage/news.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageNews); -router.get('/globalmanage/accounts.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageAccounts); router.get('/globalmanage/recent.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageRecent); +router.get('/globalmanage/news.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageNews); +router.get('/globalmanage/accounts.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageAccounts); +router.get('/globalmanage/settings.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageSettings); //captcha router.get('/captcha', captcha); //get captcha image and cookie diff --git a/gulp/res/js/hover.js b/gulp/res/js/hover.js index df7d36a9..83462ece 100644 --- a/gulp/res/js/hover.js +++ b/gulp/res/js/hover.js @@ -107,7 +107,7 @@ window.addEventListener('DOMContentLoaded', (event) => { this.style.cursor = ''; } if (json) { - setLocalStorage(`hovercache-${jsonPath}`, JSON.stringify(jsonson)); + setLocalStorage(`hovercache-${jsonPath}`, JSON.stringify(json)); if (json.postId == hash) { postJson = json; } else { diff --git a/helpers/tasks.js b/helpers/tasks.js index 942fc2c5..92fc2e7b 100644 --- a/helpers/tasks.js +++ b/helpers/tasks.js @@ -91,7 +91,7 @@ module.exports = { if (options.endpage === 0) { //deleted only/all posts, so only 1 page will remain options.endpage = 1; - } else if (maxPage < options.endpage) { + } else if (maxPage < options.endpage || !options.endpage) { //else just build up to the max page if it is greater than input page number options.endpage = maxPage } diff --git a/models/forms/changeglobalsettings.js b/models/forms/changeglobalsettings.js new file mode 100644 index 00000000..e7a02695 --- /dev/null +++ b/models/forms/changeglobalsettings.js @@ -0,0 +1,74 @@ +'use strict'; + +const { Boards, Posts, Accounts } = require(__dirname+'/../../db/') + , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') + , buildQueue = require(__dirname+'/../../queue.js') + , cache = require(__dirname+'/../../redis.js') + , { remove } = require('fs-extra'); + +module.exports = async (req, res, next) => { + + const promises = []; + const oldSettings = await cache.get('globalsettings'); + const newSettings = { + 'captchaMode': typeof req.body.captcha_mode === 'number' && req.body.captcha_mode !== oldSettings.captchaMode ? req.body.captcha_mode : oldSettings.captchaMode, + 'filters': req.body.filters !== null ? req.body.filters.split('\r\n').filter(n => n).slice(0,50) : oldSettings.filters, + 'filterMode': typeof req.body.filter_mode === 'number' && req.body.filter_mode !== oldSettings.filterMode ? req.body.filter_mode : oldSettings.filterMode, + 'filterBanDuration': typeof req.body.ban_duration === 'number' && req.body.ban_duration !== oldSettings.filterBanDuration ? req.body.ban_duration : oldSettings.filterBanDuration, + }; + + cache.set('globalsettings', newSettings); + + let rebuildThreads = false + , rebuildBoard = false + , rebuildCatalog = false; + + if (newSettings.captchaMode > oldSettings.captchaMode) { + rebuildBoard = true; + rebuildCatalog = true; + if (newSettings.captchaMode == 2) { + rebuildThreads = true; //thread captcha enabled, removes threads + } +//todo: implement removing pages/rebuilding for all affected boards +//i.e. query for ones with settings.catchaMode < newSettings.captchaMode + /* + const affectedBoards = ////// + for (let i = 0; i < affectedBoards.length; i++) { + const board = affectedBoards[i]; + if (rebuildThreads) { + promises.push(remove(`${uploadDirectory}/html/${board._id}/thread/`)); + } + if (rebuildBoard) { + buildQueue.push({ + 'task': 'buildBoardMultiple', + 'options': { + board, + 'startpage': 1, + 'endpage': null //no endpage will use whatver maxpage of board is + } + }); + } + if (rebuildCatalog) { + buildQueue.push({ + 'task': 'buildCatalog', + 'options': { + board, + } + }); + } + } + */ + } + + //finish the promises in parallel e.g. removing files + if (promises.length > 0) { + await Promise.all(promises); + } + + return res.render('message', { + 'title': 'Success', + 'message': 'Updated settings.', + 'redirect': '/globalmanage/settings.html' + }); + +} diff --git a/models/forms/makepost.js b/models/forms/makepost.js index 85789651..04d43e68 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -7,6 +7,7 @@ const path = require('path') , Mongo = require(__dirname+'/../../db/db.js') , Socketio = require(__dirname+'/../../socketio.js') , { Stats, Posts, Boards, Files, Bans } = require(__dirname+'/../../db/') + , cache = require(__dirname+'/../../redis.js') , getTripCode = require(__dirname+'/../../helpers/posting/tripcode.js') , linkQuotes = require(__dirname+'/../../helpers/posting/quotes.js') , { markdown } = require(__dirname+'/../../helpers/posting/markdown.js') @@ -98,24 +99,38 @@ module.exports = async (req, res, next) => { }); } //filters - if (res.locals.permLevel >= 4 && filterMode > 0 && filters && filters.length > 0) { - const allContents = req.body.name+req.body.message+req.body.subject+req.body.email; - const containsFilter = filters.some(filter => { return allContents.includes(filter) }); - if (containsFilter === true) { + if (res.locals.permLevel > 1) { //global staff bypass filters + const allContents = req.body.name+req.body.message+req.body.subject+req.body.email + , globalSettings = await cache.get('globalsettings'); + let hitGlobalFilter = false + , hitLocalFilter = false + , ban; + //global filters + if (globalSettings && globalSettings.filters.length > 0 && globalSettings.filterMode > 0) { + hitGlobalfilter = globalSettings.filters.some(filter => { return allContents.includes(filter) }); + } + //board-specific filters + if (!hitGlobalFilter && res.locals.permLevel >= 4 && filterMode > 0 && filters && filters.length > 0) { + hitLocalFilter = filters.some(filter => { return allContents.includes(filter) }); + } + if (hitGlobalFilter || hitLocalFilter) { await deleteTempFiles(req).catch(e => console.error); - if (filterMode === 1) { + const useFilterMode = hitGlobalFilter ? globalSettings.filterMode : filterMode; //global override local filter + if (useFilterMode === 1) { return dynamicResponse(req, res, 400, 'message', { 'title': 'Bad request', 'message': 'Your post was blocked by a word filter', 'redirect': redirect }); - } else if (filterMode === 2) { + } else { //otherwise filter mode must be 2 + const useFilterBanDuration = hitGlobalFilter ? globalSettings.filterBanDuration : filterBanDuration; + const banBoard = hitGlobalFilter ? null : res.locals.board._id; const banDate = new Date(); - const banExpiry = new Date(filterBanDuration + banDate.getTime()); + const banExpiry = new Date(useFilterBanDuration + banDate.getTime()); const ban = { 'ip': res.locals.ip.hash, 'reason': 'post word filter auto ban', - 'board': res.locals.board._id, + 'board': banBoard, 'posts': null, 'issuer': 'system', //what should i call this 'date': banDate, @@ -124,12 +139,13 @@ module.exports = async (req, res, next) => { 'seen': false }; await Bans.insertOne(ban); - const bans = await Bans.find(res.locals.ip.hash, res.locals.board._id); //need to query db so it has _id field for unban checkmark + const bans = await Bans.find(res.locals.ip.hash, banBoard); //need to query db so it has _id field for appeal checkmark return res.status(403).render('ban', { bans: bans }); } } + } let files = []; // if we got a file @@ -140,7 +156,7 @@ module.exports = async (req, res, next) => { await deleteTempFiles(req).catch(e => console.error); return dynamicResponse(req, res, 400, 'message', { 'title': 'Bad request', - 'message': `Mime type ${req.files.file[i].mimetype} for "${req.files.file[i].name}" not allowed.`, + 'message': `Mime type "${req.files.file[i].mimetype}" for "${req.files.file[i].name}" not allowed.`, 'redirect': redirect }); } diff --git a/models/pages/globalmanage/index.js b/models/pages/globalmanage/index.js index 64a1e0b8..91b66fc0 100644 --- a/models/pages/globalmanage/index.js +++ b/models/pages/globalmanage/index.js @@ -6,4 +6,5 @@ module.exports = { globalManageRecent: require(__dirname+'/recent.js'), globalManageNews: require(__dirname+'/news.js'), globalManageAccounts: require(__dirname+'/accounts.js'), + globalManageSettings: require(__dirname+'/settings.js'), } diff --git a/models/pages/globalmanage/settings.js b/models/pages/globalmanage/settings.js new file mode 100644 index 00000000..f34fc4ba --- /dev/null +++ b/models/pages/globalmanage/settings.js @@ -0,0 +1,23 @@ +'use strict'; + +const cache = require(__dirname+'/../../../redis.js'); + +module.exports = async (req, res, next) => { + + let settings = await cache.get('globalsettings'); + if (!settings) { + settings = { + captchaMode: 0, + filters: [], + filterMode: 0, + filterBanDuration: 0, + } + cache.set('globalsettings', settings); + } + + res.render('globalmanagesettings', { + csrf: req.csrfToken(), + settings, + }); + +} diff --git a/schedules/index.js b/schedules/index.js index ae9227cb..aabd4c96 100644 --- a/schedules/index.js +++ b/schedules/index.js @@ -21,10 +21,10 @@ const timeUtils = require(__dirname+'/../helpers/timeutils.js') 'task': 'updateStats', 'options': {} }, { - 'repeat': { - 'cron': '0 * * * *' - } - }); + 'repeat': { + 'cron': '0 * * * *' + } + }); //delete files for expired captchas const deleteCaptchas = require(__dirname+'/deletecaptchas.js'); diff --git a/views/custompages/rules.pug b/views/custompages/rules.pug index 47261e9d..173baa87 100644 --- a/views/custompages/rules.pug +++ b/views/custompages/rules.pug @@ -1,3 +1,4 @@ + extends ../layout.pug block head @@ -10,6 +11,6 @@ block content table.table-body tr.table-row td - p Do not post, link or promote any content that violates laws of the United States of America. - p Do not spam, flood or perform any actions to an extent that negatively impacts the usual function of the website/server. - p SFW marked boards should have primarily SFW content and ensure that any NSFW content is spoilered. + p Do not post, link or promote any content illegal under United States law. + p Do not spam, flood, (D)DoS, etc. which negatively impacts site performance. + p SFW marked boards must be primarily SFW and spoiler all NSFW content. diff --git a/views/mixins/globalmanagenav.pug b/views/mixins/globalmanagenav.pug index c32cb03a..5eee6279 100644 --- a/views/mixins/globalmanagenav.pug +++ b/views/mixins/globalmanagenav.pug @@ -10,4 +10,6 @@ mixin globalmanagenav(selected) a(href='accounts.html' class=(selected === 'accounts' ? 'bold' : '')) [Accounts] | a(href='news.html' class=(selected === 'news' ? 'bold' : '')) [News] + | + a(href='settings.html' class=(selected === 'settings' ? 'bold' : '')) [Settings] diff --git a/views/pages/globalmanagesettings.pug b/views/pages/globalmanagesettings.pug new file mode 100644 index 00000000..81296845 --- /dev/null +++ b/views/pages/globalmanagesettings.pug @@ -0,0 +1,35 @@ +extends ../layout.pug +include ../mixins/globalmanagenav.pug + +block head + script(src='/js/all.js') + title Manage + +block content + h1.board-title Global Management + br + +globalmanagenav('settings') + hr(size=1) + h4.no-m-p Settings: + .form-wrapper.flexleft + form.form-post(action=`/forms/global/settings`, enctype='application/x-www-form-urlencoded', method='POST') + input(type='hidden' name='_csrf' value=csrf) + .row + .label Captcha Mode + select(name='captcha_mode') + option(value='0', selected=settings.captchaMode === 0) No Captcha + option(value='1', selected=settings.captchaMode === 1) Captcha for new thread + option(value='2', selected=settings.captchaMode === 2) Captcha for all posts + .row + .label Filters + textarea(name='filters' placeholder='newline separated, max 50') #{settings.filters.join('\n')} + .row + .label Filter Mode + select(name='filter_mode') + option(value='0', selected=settings.filterMode === 0) Do nothing + option(value='1', selected=settings.filterMode === 1) Block post + option(value='2', selected=settings.filterMode === 2) Ban + .row + .label Filter Auto Ban Duration + input(type='text' name='ban_duration' placeholder='e.g. 1w' value=settings.filterBanDuration) + input(type='submit', value='save settings')