diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b5099b..5611a1fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,3 +32,4 @@ - Try to fallback thumbnail generation for video with horribly broken encoding - Country blocklist now can actually fit all countries - Make "auth level" text box into "account type" dropdown in accounts page, easier to understand + - Board owners can now edit custom pages diff --git a/controllers/forms.js b/controllers/forms.js index 5bf043ba..8bb6ca44 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -28,7 +28,7 @@ const express = require('express') addFlagsController, deleteFlagsController, boardSettingsController, transferController, resignController, deleteAccountController, loginController, registerController, changePasswordController, editAccountsController, globalSettingsController, createBoardController, makePostController, - editPostController, newCaptcha, blockBypass, logout } = require(__dirname+'/forms/index.js'); + editCustomPageController, editPostController, newCaptcha, blockBypass, logout } = require(__dirname+'/forms/index.js'); //make new post @@ -57,6 +57,7 @@ router.post('/board/:board/addcustompages', useSession, sessionRefresh, csrf, Bo router.post('/board/:board/deletecustompages', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), deleteCustomPageController.paramConverter, deleteCustomPageController.controller); //delete banners router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(3), editBansController.paramConverter, editBansController.controller); //edit bans router.post('/board/:board/deleteboard', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(config.get.deleteBoardPermLevel), deleteBoardController.controller); //delete board +router.post('/board/:board/editcustompage', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), editCustomPageController.paramConverter, editCustomPageController.controller); //edit custom page //global management forms router.post('/global/editbans', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(1), editBansController.paramConverter, editBansController.controller); //remove bans diff --git a/controllers/forms/editcustompage.js b/controllers/forms/editcustompage.js new file mode 100644 index 00000000..d685f42c --- /dev/null +++ b/controllers/forms/editcustompage.js @@ -0,0 +1,62 @@ +'use strict'; + +const editCustomPage = require(__dirname+'/../../models/forms/editcustompage.js') + , { CustomPages } = require(__dirname+'/../../db/') + , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') + , paramConverter = require(__dirname+'/../../helpers/paramconverter.js') + , { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable, + inArrayBody, arrayInBody, existsBody } = require(__dirname+'/../../helpers/schema.js') + , config = require(__dirname+'/../../config.js'); + +module.exports = { + + paramConverter: paramConverter({ + trimFields: ['message', 'title', 'page'], + processMessageLength: true, + objectIdFields: ['page_id'], + }), + + controller: async (req, res, next) => { + + const { globalLimits } = config.get; + + const errors = await checkSchema([ + { result: existsBody(req.body.page_id), expected: true, error: 'Missing page id' }, + { result: existsBody(req.body.message), expected: true, error: 'Missing message' }, + { result: existsBody(req.body.title), expected: true, error: 'Missing title' }, + { result: existsBody(req.body.page), expected: true, error: 'Missing .html name' }, + { result: () => { + if (req.body.page) { + return /[a-z0-9_-]+/.test(req.body.page); + } + return false; + } , expected: true, error: '.html name must contain a-z 0-9 _ - only' }, + { result: numberBody(res.locals.messageLength, 0, globalLimits.customPages.maxLength), expected: true, error: `Message must be ${globalLimits.customPages.maxLength} characters or less` }, + { result: lengthBody(req.body.title, 0, 50), expected: false, error: 'Title must be 50 characters or less' }, + { result: lengthBody(req.body.page, 0, 50), expected: false, error: '.html name must be 50 characters or less' }, + { result: async () => { + const existingPage = await CustomPages.findOne(req.params.board, req.body.page); + if (existingPage && existingPage.page === req.body.page) { + return existingPage._id === req.body.page_id; + } + return true; + }, expected: true, error: '.html name must be unique'}, + ]); + + if (errors.length > 0) { + return dynamicResponse(req, res, 400, 'message', { + 'title': 'Bad request', + 'errors': errors, + 'redirect': req.headers.referer || '/${req.params.board}/manage/custompages.html' + }); + } + + try { + await editCustomPage(req, res, next); + } catch (err) { + return next(err); + } + + } + +} diff --git a/controllers/forms/index.js b/controllers/forms/index.js index cccef6be..31c9c80e 100644 --- a/controllers/forms/index.js +++ b/controllers/forms/index.js @@ -11,6 +11,7 @@ module.exports = { deleteCustomPageController: require(__dirname+'/deletecustompage.js'), addNewsController: require(__dirname+'/addnews.js'), editNewsController: require(__dirname+'/editnews.js'), + editCustomPageController: require(__dirname+'/editcustompage.js'), deleteNewsController: require(__dirname+'/deletenews.js'), uploadBannersController: require(__dirname+'/uploadbanners.js'), deleteBannersController: require(__dirname+'/deletebanners.js'), diff --git a/controllers/pages.js b/controllers/pages.js index b86f2dc1..d8f8991f 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -16,16 +16,17 @@ const express = require('express') , csrf = require(__dirname+'/../helpers/checks/csrfmiddleware.js') , setMinimal = require(__dirname+'/../helpers/setminimal.js') //page models - , { manageRecent, manageReports, manageAssets, manageSettings, manageBans, + , { manageRecent, manageReports, manageAssets, manageSettings, manageBans, editCustomPage, manageBoard, manageThread, manageLogs, manageCatalog, manageCustomPages } = require(__dirname+'/../models/pages/manage/') - , { globalManageSettings, globalManageReports, globalManageBans, globalManageBoards, + , { globalManageSettings, globalManageReports, globalManageBans, globalManageBoards, editNews, globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/') - , { changePassword, blockBypass, home, register, login, create, editNews, + , { changePassword, blockBypass, home, register, login, create, board, catalog, banners, randombanner, news, captchaPage, overboard, overboardCatalog, captcha, thread, modlog, modloglist, account, boardlist, customPage } = require(__dirname+'/../models/pages/') , threadParamConverter = paramConverter({ processThreadIdParam: true }) , logParamConverter = paramConverter({ processDateParam: true }) - , newsParamConverter = paramConverter({ objectIdParams: ['newsid'] }); + , newsParamConverter = paramConverter({ objectIdParams: ['newsid'] }) + , custompageParamConverter = paramConverter({ objectIdParams: ['custompageid'] }); //homepage router.get('/index.html', home); @@ -73,9 +74,9 @@ router.get('/globalmanage/accounts.html', useSession, sessionRefresh, isLoggedIn router.get('/globalmanage/settings.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageSettings); //edit pages -router.get('/editnews/:newsid([a-f0-9]{24}).html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, newsParamConverter, editNews); +router.get('/globalmanage/editnews/:newsid([a-f0-9]{24}).html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, newsParamConverter, editNews); +router.get('/:board/manage/editcustompage/:custompageid([a-f0-9]{24}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, custompageParamConverter, editCustomPage); //TODO: edit post get endpoint -//TODO: edit board custom page get endpoint //captcha router.get('/captcha', geoAndTor, processIp, captcha); //get captcha image and cookie diff --git a/db/custompages.js b/db/custompages.js index 01f738ea..4aae22e2 100644 --- a/db/custompages.js +++ b/db/custompages.js @@ -18,6 +18,7 @@ module.exports = { .toArray(); }, + //browsing board findOne: (board, page) => { return db.findOne({ 'board': board, @@ -25,6 +26,14 @@ module.exports = { }); }, + //editing + findOneId: (id, board) => { + return db.findOne({ + '_id': id, + 'board': board, + }); + }, + boardCount: (board) => { return db.countDocuments({ 'board': board, @@ -35,7 +44,22 @@ module.exports = { return db.insertOne(custompage); }, - updateOne: () => {}, + findOneAndUpdate: (id, board, page, title, raw, markdown, edited) => { + return db.findOneAndUpdate({ + '_id': id, + 'board': board, + }, { + '$set': { + 'page': page, + 'title': title, + 'message.raw': raw, + 'message.markdown': markdown, + 'edited': edited, + } + }, { + returnDocument: 'before', + }); + }, deleteMany: (pages, board) => { return db.deleteMany({ diff --git a/models/forms/addcustompage.js b/models/forms/addcustompage.js index b507528c..7d00127a 100644 --- a/models/forms/addcustompage.js +++ b/models/forms/addcustompage.js @@ -20,7 +20,7 @@ module.exports = async (req, res, next) => { 'markdown': markdownMessage }, 'date': new Date(), - 'edited': null, //unused currently + 'edited': null, }; const insertedCustomPage = await CustomPages.insertOne(post); diff --git a/models/forms/editcustompage.js b/models/forms/editcustompage.js new file mode 100644 index 00000000..0e34c549 --- /dev/null +++ b/models/forms/editcustompage.js @@ -0,0 +1,58 @@ +'use strict'; + +const { CustomPages } = require(__dirname+'/../../db/') + , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') + , { remove } = require('fs-extra') + , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') + , buildQueue = require(__dirname+'/../../queue.js') + , { prepareMarkdown } = require(__dirname+'/../../helpers/posting/markdown.js') + , messageHandler = require(__dirname+'/../../helpers/posting/message.js'); + +module.exports = async (req, res, next) => { + + const message = prepareMarkdown(req.body.message, false); + const { message: markdownPage } = await messageHandler(message, null, null, res.locals.permLevel); + const editedDate = new Date(); + + const oldPage = await CustomPages.findOneAndUpdate(req.body.page_id, req.params.board, + req.body.page, req.body.title, message, markdownPage, editedDate).then(res => res.value); + + if (oldPage === null) { + return dynamicResponse(req, res, 400, 'message', { + 'title': 'Bad request', + 'errors': 'Custom page does not exist', + 'redirect': req.headers.referer || '/${req.params.board}/manage/custompages.html' + }); + } + + await remove(`${uploadDirectory}/html/${req.params.board}/custompage/${oldPage.page}.html`); + + const newPage = { + '_id': oldPage._id, + 'board': req.params.board, + 'page': req.body.page, + 'title': req.body.title, + 'message': { + 'raw': message, + 'markdown': markdownPage + }, + 'date': oldPage.date, + 'edited': editedDate, + }; + + buildQueue.push({ + 'task': 'buildCustomPage', + 'options': { + 'board': res.locals.board, + 'page': newPage.page, + 'customPage': newPage, + } + }); + + return dynamicResponse(req, res, 200, 'message', { + 'title': 'Success', + 'message': 'Updated custom page', + 'redirect': `/${req.params.board}/manage/custompages.html`, + }); + +} diff --git a/models/pages/editnews.js b/models/pages/globalmanage/editnews.js similarity index 85% rename from models/pages/editnews.js rename to models/pages/globalmanage/editnews.js index 7b7c8530..6968cd09 100644 --- a/models/pages/editnews.js +++ b/models/pages/globalmanage/editnews.js @@ -1,6 +1,6 @@ 'use strict'; -const { News } = require(__dirname+'/../../db/'); +const { News } = require(__dirname+'/../../../db/'); module.exports = async (req, res, next) => { diff --git a/models/pages/globalmanage/index.js b/models/pages/globalmanage/index.js index 02cc8a84..3d536d7e 100644 --- a/models/pages/globalmanage/index.js +++ b/models/pages/globalmanage/index.js @@ -9,4 +9,5 @@ module.exports = { globalManageNews: require(__dirname+'/news.js'), globalManageAccounts: require(__dirname+'/accounts.js'), globalManageSettings: require(__dirname+'/settings.js'), + editNews: require(__dirname+'/editnews.js'), } diff --git a/models/pages/index.js b/models/pages/index.js index 564d292c..3db7b42b 100644 --- a/models/pages/index.js +++ b/models/pages/index.js @@ -22,5 +22,4 @@ module.exports = { boardlist: require(__dirname+'/boardlist.js'), overboard: require(__dirname+'/overboard.js'), overboardCatalog: require(__dirname+'/overboardcatalog.js'), - editNews: require(__dirname+'/editnews.js'), } diff --git a/models/pages/manage/editcustompage.js b/models/pages/manage/editcustompage.js new file mode 100644 index 00000000..47cefeff --- /dev/null +++ b/models/pages/manage/editcustompage.js @@ -0,0 +1,26 @@ +'use strict'; + +const { CustomPages } = require(__dirname+'/../../../db/'); + +module.exports = async (req, res, next) => { + + let customPage; + try { + customPage = await CustomPages.findOneId(req.params.custompageid, req.params.board); + } catch (err) { + return next(err) + } + + if (!customPage) { + return next(); + } + + res + .set('Cache-Control', 'private, max-age=5') + .render('editcustompage', { + csrf: req.csrfToken(), + page: customPage, + board: res.locals.board, + }); + +} diff --git a/models/pages/manage/index.js b/models/pages/manage/index.js index 1d321cba..739192d6 100644 --- a/models/pages/manage/index.js +++ b/models/pages/manage/index.js @@ -11,4 +11,5 @@ module.exports = { manageCatalog: require(__dirname+'/catalog.js'), manageThread: require(__dirname+'/thread.js'), manageCustomPages: require(__dirname+'/custompages.js'), + editCustomPage: require(__dirname+'/editcustompage.js'), } diff --git a/views/mixins/custompage.pug b/views/mixins/custompage.pug index 488f0474..eff0c381 100644 --- a/views/mixins/custompage.pug +++ b/views/mixins/custompage.pug @@ -6,6 +6,7 @@ mixin custompage(page, manage=false) if manage === true input.left.post-check(type='checkbox', name='checkedcustompages' value=page.page) a.left(href=`/${board._id}/custompage/${page.page}.html`) #{page.title} + a.right.ml-5(href=`/${board._id}/manage/editcustompage/${page._id}.html`) [Edit] - const pageDate = new Date(page.date); time.right.reltime(datetime=pageDate.toISOString()) #{pageDate.toLocaleString(undefined, {hourCycle:'h23'})} tr @@ -14,3 +15,8 @@ mixin custompage(page, manage=false) p.no-m-p #{`${page.message.raw.substring(0,50)}...`} else pre.post-message.no-m-p !{page.message.markdown} + if page.edited + small.right.cb.edited + | Last edited + - const pageEditDate = new Date(page.edited); + time.reltime(datetime=pageEditDate.toISOString()) #{pageEditDate.toLocaleString(undefined, {hourCycle:'h23'})} diff --git a/views/mixins/newspost.pug b/views/mixins/newspost.pug index 86e1da91..338d5cb9 100644 --- a/views/mixins/newspost.pug +++ b/views/mixins/newspost.pug @@ -8,7 +8,7 @@ mixin newspost(post, globalmanage=false) input.left.post-check(type='checkbox', name='checkednews' value=post._id) a.left(href=`#${post._id}`) #{post.title} if globalmanage === true - a.right.ml-5(href=`/editnews/${post._id}.html`) [Edit] + a.right.ml-5(href=`/globalmanage/editnews/${post._id}.html`) [Edit] - const newsDate = new Date(post.date); time.right.reltime(datetime=newsDate.toISOString()) #{newsDate.toLocaleString(undefined, {hourCycle:'h23'})} tr diff --git a/views/pages/editcustompage.pug b/views/pages/editcustompage.pug new file mode 100644 index 00000000..50be4831 --- /dev/null +++ b/views/pages/editcustompage.pug @@ -0,0 +1,27 @@ +extends ../layout.pug + +block head + title Edit Custom Page + +block content + h1.board-title Edit Custom Page + include ../includes/stickynav.pug + .form-wrapper.flex-center.mv-10 + form.form-post(action=`/forms/board/${board._id}/editcustompage` method='POST') + input(type='hidden' name='_csrf' value=csrf) + input(type='hidden' name='page_id' value=page._id) + .row + .label .html name + input(type='text' name='page' pattern='[a-z0-9-_]+' title='a-z0-9-_ only' value=page.page required) + .table-container.flex-center.mv-5 + table + tr + th + input.edit.left(type='text' name='title' value=page.title required) + - const pageDate = new Date(page.date); + time.right.reltime(datetime=pageDate.toISOString()) #{pageDate.toLocaleString(undefined, {hourCycle:'h23'})} + tr + td + + textarea.edit.fw(name='message' rows='10' placeholder='Supports post styling' required) #{page.message.raw} + input(type='submit', value='save') diff --git a/views/pages/managecustompages.pug b/views/pages/managecustompages.pug index d507c8d5..12096c9f 100644 --- a/views/pages/managecustompages.pug +++ b/views/pages/managecustompages.pug @@ -27,7 +27,7 @@ block content input(type='submit', value='submit') if customPages.length > 0 hr(size=1) - h4.no-m-p Delete Custom Pages: + h4.no-m-p Manage Custom Pages: .form-wrapper.flexleft form.form-post(action=`/forms/board/${board._id}/deletecustompages`, enctype='application/x-www-form-urlencoded', method='POST') input(type='hidden' name='_csrf' value=csrf)