diff --git a/configs/main.js.example b/configs/main.js.example index 9ad52c58..b3a7d5bf 100644 --- a/configs/main.js.example +++ b/configs/main.js.example @@ -146,13 +146,14 @@ module.exports = { //subset of languages to allow. languageSubset: [ 'javascript', - 'js', 'typescript', + 'perl', + 'js', + 'c++', + 'c', 'java', 'kotlin', 'php', - 'c++', - 'c', 'h', 'csharp', 'bash', @@ -233,9 +234,9 @@ module.exports = { theme: 'lain', codeTheme: 'ir-black', sfw: false, //safe for work board - locked: false, //board locked, only staff can post - unlisted: false, //board hidden from on-site board list and frontpage - webring: true, //board shown on webring + lockMode: 0, //board lock mode + unlistedLocal: false, //board hidden from on-site board list and frontpage + unlistedWebring: false, //board hidden from webring captchaMode: 0, //0=disabled, 1=for threads, 2=for all posts tphTrigger: 0, //numebr of threads in an hour before trigger action is activated pphTrigger: 0, //number of posts in an hour before ^ diff --git a/controllers/forms/actions.js b/controllers/forms/actions.js index c1888b46..38d8e8d7 100644 --- a/controllers/forms/actions.js +++ b/controllers/forms/actions.js @@ -27,6 +27,8 @@ module.exports = async (req, res, next) => { //50 because checked posts is max 10 and 5 reports max per post errors.push('Cannot check more than 50 reports'); } + } else if (!req.body.checkedreports && req.body.report_ban) { + errors.push('Must select posts+reports to report ban'); } res.locals.actions = actionChecker(req); diff --git a/controllers/forms/boardsettings.js b/controllers/forms/boardsettings.js index f896066e..fc6af109 100644 --- a/controllers/forms/boardsettings.js +++ b/controllers/forms/boardsettings.js @@ -96,13 +96,16 @@ module.exports = async (req, res, next) => { errors.push(`Max reply message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Reply Message Length" (currently ${res.locals.board.settings.minReplyMessageLength})`); } + if (typeof req.body.lock_mode === 'number' && (req.body.lock_mode < 0 || req.body.lock_mode > 2)) { + errors.push('Invalid lock mode'); + } 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.tph_trigger === 'number' && (req.body.tph_trigger < 0 || req.body.tph_trigger > 10000)) { errors.push('Invalid tph trigger threshold'); } - if (typeof req.body.tph_trigger_action === 'number' && (req.body.tph_trigger_action < 0 || req.body.tph_trigger_action > 3)) { + if (typeof req.body.tph_trigger_action === 'number' && (req.body.tph_trigger_action < 0 || req.body.tph_trigger_action > 4)) { errors.push('Invalid tph trigger action'); } if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) { @@ -128,7 +131,7 @@ module.exports = async (req, res, next) => { if (res.locals.permLevel > 1) { //if not global staff or above const ratelimitBoard = await Ratelimits.incrmentQuota(req.params.board, 'settings', rateLimitCost.boardSettings); //2 changes a minute - const ratelimitIp = await Ratelimits.incrmentQuota(res.locals.ip.hash, 'settings', rateLimitCost.boardSettings); + const ratelimitIp = await Ratelimits.incrmentQuota(res.locals.ip.single, 'settings', rateLimitCost.boardSettings); if (ratelimitBoard > 100 || ratelimitIp > 100) { return dynamicResponse(req, res, 429, 'message', { 'title': 'Ratelimited', diff --git a/controllers/forms/globalactions.js b/controllers/forms/globalactions.js index 8ed79692..56274ba0 100644 --- a/controllers/forms/globalactions.js +++ b/controllers/forms/globalactions.js @@ -24,7 +24,9 @@ module.exports = async (req, res, next) => { //50 because checked posts is max 10 and 5 reports max per post errors.push('Cannot check more than 50 reports'); } - } + } else if (!req.body.checkedreports && req.body.global_report_ban) { + errors.push('Must select posts+reports to report ban'); + } res.locals.actions = actionChecker(req); @@ -74,6 +76,7 @@ module.exports = async (req, res, next) => { //edit post, only allowing one return res.render('editpost', { 'post': res.locals.posts[0], + 'csrf': req.csrfToken(), }); } diff --git a/controllers/pages.js b/controllers/pages.js index d7f14d42..fbf545d1 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -14,7 +14,7 @@ const express = require('express') , setMinimal = require(__dirname+'/../helpers/setminimal.js') //page models , { manageRecent, manageReports, manageBanners, manageSettings, manageBans, - manageBoard, manageThread, manageLogs } = require(__dirname+'/../models/pages/manage/') + manageBoard, manageThread, manageLogs, manageCatalog } = require(__dirname+'/../models/pages/manage/') , { globalManageSettings, globalManageReports, globalManageBans, globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/') , { changePassword, blockBypass, home, register, login, logout, create, @@ -48,6 +48,7 @@ router.get('/:board/manage/logs.html', sessionRefresh, isLoggedIn, Boards.exists router.get('/:board/manage/settings.html', sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageSettings); router.get('/:board/manage/banners.html', sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageBanners); // if (mod view enabled) { +router.get('/:board/manage/catalog.html', sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageCatalog); router.get('/:board/manage/:page(1[0-9]{1,}|[2-9][0-9]{0,}|index).html', sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(3), csrf, manageBoard); router.get('/:board/manage/thread/:id([1-9][0-9]{0,}).html', sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(3), csrf, Posts.exists, manageThread); diff --git a/db/boards.js b/db/boards.js index 38cebc8d..053d1936 100644 --- a/db/boards.js +++ b/db/boards.js @@ -103,7 +103,7 @@ module.exports = { boardSort: (skip=0, limit=50, sort={ ips:-1, pph:-1, sequence_value:-1 }, filter={}, showUnlisted=false) => { const addedFilter = {}; if (!showUnlisted) { - addedFilter['settings.unlisted'] = false; + addedFilter['settings.unlistedLocal'] = false; } if (filter.search) { addedFilter['$or'] = [ @@ -122,7 +122,7 @@ module.exports = { 'settings.description': 1, 'settings.name': 1, 'settings.tags': 1, - 'settings.unlisted': 1, + 'settings.unlistedLocal': 1, } }) .sort(sort) @@ -133,7 +133,7 @@ module.exports = { webringBoards: () => { return db.find({ - 'settings.webring': true + 'settings.unlistedWebring': false }, { 'projection': { '_id': 1, @@ -152,7 +152,7 @@ module.exports = { count: (filter, showUnlisted=false) => { const addedFilter = {}; if (!showUnlisted) { - addedFilter['settings.unlisted'] = false; + addedFilter['settings.unlistedLocal'] = false; } if (filter.search) { addedFilter['$or'] = [ @@ -179,7 +179,7 @@ module.exports = { }, 'unlisted': { '$sum': { - '$cond': ['$settings.unlisted', 1, 0] + '$cond': ['$settings.unlistedLocal', 1, 0] } }, //removed ips because sum is inaccurate diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index c9b6ad38..a70a1038 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -62,6 +62,25 @@ main.minimal { text-decoration: line-through; } +@keyframes rainbow-anim { + 0% { + background-position: 0 0; + } + 100% { + background-position: 400% 0; + } +} + +.rainbow { + background: linear-gradient(to right, #6666ff, #0099ff , #00ff00, #ff3399, #6666ff); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: rainbow-anim 10s linear infinite; + background-size: 400% 100%; + text-shadow: #00000050 0px 0px 1px; +} + .underline { text-decoration: underline; } @@ -207,8 +226,8 @@ a, a:visited, a.post-name { } .invalid-quote { - cursor:pointer; - text-decoration: line-through; + cursor:pointer; + text-decoration: line-through; } .post-message a { @@ -277,7 +296,7 @@ p { } /* .upload-list::-webkit-scrollbar { - display: none; + display: none; } */ .upload-item { @@ -384,7 +403,7 @@ p { } .edited { - font-style: italic; + font-style: italic; } .close { @@ -629,7 +648,7 @@ details.actions div { .post-check { position: relative; top: 2px; - margin: -3px 1px !important; + margin: -3px 1px; } .post-files { @@ -773,7 +792,7 @@ input:invalid, textarea:invalid { box-sizing: border-box; padding: .5em; max-width: 100%; - min-width: 25em; + min-width: 30em; } .postmenu { @@ -880,6 +899,11 @@ input:invalid, textarea:invalid { float: left; padding-left: 10px; padding-right: 10px; + text-align: center; +} + +.nav-item:nth-of-type(3) { + line-height:1.5em; } .left { @@ -1000,20 +1024,20 @@ iframe.bypass { } .captcharefresh { - position: absolute; + position: absolute; bottom: 0; - left: 5px; - font-size: 18px; - cursor: pointer; + left: 5px; + font-size: 18px; + cursor: pointer; color: black; } .captcharefresh { - position: absolute; + position: absolute; bottom: 0; - left: 5px; - font-size: 18px; - cursor: pointer; + left: 5px; + font-size: 18px; + cursor: pointer; } .label, .rlabel { @@ -1145,7 +1169,7 @@ table, .boardtable { width: 100% } row.wrap.sb .col { - flex-basis: calc(50% - 5px); + flex-basis: calc(50% - 5px); } @media only screen and (max-height: 400px) { @@ -1263,7 +1287,7 @@ row.wrap.sb .col { .post-check { top: 1px; - margin-left: 2px!important; + margin-left: 2px; } .pages { diff --git a/gulp/res/css/themes/win95.css b/gulp/res/css/themes/win95.css index fa02d071..50d88e42 100644 --- a/gulp/res/css/themes/win95.css +++ b/gulp/res/css/themes/win95.css @@ -126,6 +126,9 @@ width:unset!important; input[type=submit], input[type=submit], label[for=file] { background: var(--post-color)!important; } +.footer { +padding-bottom: 3em; +} @media only screen and (max-width:600px) { .nav-item { padding-left: 5px; diff --git a/gulp/res/icons/favicon2.ico b/gulp/res/icons/favicon2.ico new file mode 100644 index 00000000..da3b01a7 Binary files /dev/null and b/gulp/res/icons/favicon2.ico differ diff --git a/gulp/res/js/captcha.js b/gulp/res/js/captcha.js index cd6fa5be..1ab74443 100644 --- a/gulp/res/js/captcha.js +++ b/gulp/res/js/captcha.js @@ -31,13 +31,13 @@ window.addEventListener('DOMContentLoaded', (event) => { }; const loadCaptcha = function(e) { - const captchaDiv = this.previousSibling; + const field = e.target; + const captchaDiv = field.previousSibling; const captchaImg = document.createElement('img'); const refreshDiv = document.createElement('div'); refreshDiv.classList.add('captcharefresh', 'noselect'); refreshDiv.addEventListener('click', refreshCaptchas, true); refreshDiv.textContent = '↻'; - const field = this; field.placeholder = 'loading'; captchaImg.src = '/captcha'; captchaImg.onload = function() { @@ -49,8 +49,12 @@ window.addEventListener('DOMContentLoaded', (event) => { }; for (let i = 0; i < captchaFields.length; i++) { - captchaFields[i].placeholder = 'focus to load captcha'; - captchaFields[i].addEventListener('focus', loadCaptcha, { once: true }); + const field = captchaFields[i]; + if (field.form.action.endsWith('/forms/blockbypass')) { + return loadCaptcha({target: field }) + } + field.placeholder = 'focus to load captcha'; + field.addEventListener('focus', loadCaptcha, { once: true }); } }); diff --git a/gulp/res/js/forms.js b/gulp/res/js/forms.js index 08d545d1..8bc921bc 100644 --- a/gulp/res/js/forms.js +++ b/gulp/res/js/forms.js @@ -4,32 +4,39 @@ function removeModal() { } function doModal(data, postcallback) { - const modalHtml = modal({ modal: data }); - let checkInterval; - document.body.insertAdjacentHTML('afterbegin', modalHtml); - document.getElementById('modalclose').onclick = () => { - removeModal(); - clearInterval(checkInterval); - }; - document.getElementsByClassName('modal-bg')[0].onclick = () => { - removeModal(); - clearInterval(checkInterval); - }; - const modalframe = document.getElementById('modalframe'); - modalframe.onload = () => { - if (localStorage.getItem('theme') === 'default') { - const currentTheme = document.head.querySelector('#theme').href; - modalframe.contentDocument.styleSheets[1].ownerNode.href = currentTheme; - } - } - if (modalframe && postcallback) { - checkInterval = setInterval(() => { - if (modalframe && modalframe.contentDocument.title == 'Success') { - clearInterval(checkInterval); - removeModal(); - postcallback(); + try { + const modalHtml = modal({ modal: data }); + let checkInterval; + document.body.insertAdjacentHTML('afterbegin', modalHtml); + document.getElementById('modalclose').onclick = () => { + removeModal(); + clearInterval(checkInterval); + }; + document.getElementsByClassName('modal-bg')[0].onclick = () => { + removeModal(); + clearInterval(checkInterval); + }; + const modalframe = document.getElementById('modalframe'); + if (modalframe) { + //if theres a modal frame and user has default theme, style it + if (localStorage.getItem('theme') === 'default') { + modalframe.onload = () => { + const currentTheme = document.head.querySelector('#theme').href; + modalframe.contentDocument.styleSheets[1].ownerNode.href = currentTheme; + } } - }, 100); + if (postcallback) { + checkInterval = setInterval(() => { + if (modalframe && modalframe.contentDocument.title == 'Success') { + clearInterval(checkInterval); + removeModal(); + postcallback(); + } + }, 100); + } + } + } catch(e) { + console.error(e) } } @@ -169,6 +176,9 @@ class formHandler { } else { if (json.message || json.messages || json.error || json.errors) { doModal(json); + if (json.message === 'Incorrect captcha answer') { + //todo: create captcha form, add method to captcha frontend code + } } else if (socket && socket.connected) { window.myPostId = json.postId; window.location.hash = json.postId diff --git a/gulp/res/js/live.js b/gulp/res/js/live.js index afefb36e..3da8db48 100644 --- a/gulp/res/js/live.js +++ b/gulp/res/js/live.js @@ -30,7 +30,7 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa lastPostId = data.postId; const postData = data; //create a new post - const postHtml = post({ post: postData, modview:isModView }); + const postHtml = post({ post: postData, modview:isModView, upLevel:isThread }); //add it to the end of the thread thread.insertAdjacentHTML('beforeend', postHtml); for (let j = 0; j < postData.quotes.length; j++) { @@ -42,14 +42,12 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa const quotedPostData = quotedPost.querySelector('.post-data'); const newRepliesDiv = document.createElement('div'); newRepliesDiv.textContent = 'Replies: '; - ['replies', 'mt-5', 'ml-5'].forEach(c => { - newRepliesDiv.classList.add(c); - }); + newRepliesDiv.classList.add('replies', 'mt-5', 'ml-5'); quotedPostData.appendChild(newRepliesDiv); replies = newRepliesDiv; } if (new RegExp(`>>${postData.postId}(\s|$)`).test(replies.innerText)) { - //reply link already exists (probably from a late catch up + //reply link already exists (probably from a late catch up) continue; } const newReply = document.createElement('a'); @@ -148,7 +146,9 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa const room = `${roomParts[1]}-${roomParts[roomParts.length-1]}`; socket = io({ transports: ['websocket'], - reconnectionAttempts: 3 + reconnectionAttempts: 3, + reconnectionDelay: 3000, + reconnectionDelayMax: 15000, }); socket.on('connect', async () => { console.log('socket connected'); diff --git a/gulp/res/js/time.js b/gulp/res/js/time.js index 4dbfadca..2fe68cfa 100644 --- a/gulp/res/js/time.js +++ b/gulp/res/js/time.js @@ -120,10 +120,12 @@ window.addEventListener('settingsReady', function(event) { window.addEventListener('addPost', function(e) { - const date = e.detail.post.querySelector('.reltime'); - if (!e.detail.hover) { - dates.push(date); + const dates = e.detail.post.querySelectorAll('.reltime'); + for (let date of dates) { + if (!e.detail.hover) { + dates.push(date); + } + changeDateFormat(date); } - changeDateFormat(date); }); diff --git a/gulp/res/js/titlescroll.js b/gulp/res/js/titlescroll.js index 27b91390..7ac2dcff 100644 --- a/gulp/res/js/titlescroll.js +++ b/gulp/res/js/titlescroll.js @@ -4,6 +4,16 @@ window.addEventListener('DOMContentLoaded', (event) => { let unread = []; const originalTitle = document.title; + const changeFavicon = (href) => { + const currentFav = document.head.querySelector('link[type="image/x-icon"]'); + const newFav = document.createElement('link'); + newFav.type = 'image/x-icon'; + newFav.rel = 'shortcut icon'; + newFav.href = href; + currentFav.remove(); + document.head.appendChild(newFav); + } + const isVisible = (e) => { const top = e.getBoundingClientRect().top; const bottom = e.getBoundingClientRect().bottom; @@ -14,8 +24,10 @@ window.addEventListener('DOMContentLoaded', (event) => { const updateTitle = () => { if (unread.length === 0) { document.title = originalTitle; + changeFavicon('/favicon.ico'); } else { document.title = `(${unread.length}) ${originalTitle}`; + changeFavicon('/file/favicon2.ico'); } } @@ -37,10 +49,7 @@ window.addEventListener('DOMContentLoaded', (event) => { } } - window.onfocus = () => { - focusChange(); - updateVisible(); - } + window.onfocus = focusChange; window.onblur = focusChange; window.addEventListener('scroll', updateVisible); diff --git a/gulpfile.js b/gulpfile.js index e197c6c3..648e10ae 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -208,6 +208,8 @@ function scripts() { `${paths.scripts.src}/post.js`, `${paths.scripts.src}/settings.js`, `${paths.scripts.src}/live.js`, + `${paths.scripts.src}/captcha.js`, + `${paths.scripts.src}/forms.js`, `${paths.scripts.src}/*.js`, `!${paths.scripts.src}/dragable.js`, `!${paths.scripts.src}/hide.js`, diff --git a/helpers/captcha/captchaverify.js b/helpers/captcha/captchaverify.js index 7ae6b2f0..f6b240c3 100644 --- a/helpers/captcha/captchaverify.js +++ b/helpers/captcha/captchaverify.js @@ -27,12 +27,12 @@ module.exports = async (req, res, next) => { if (isBypass) { return res.status(403).render('bypass', { 'minimal': req.body.minimal, - 'message': 'Incorrect captcha', + 'message': 'Incorrect captcha answer', }); } return dynamicResponse(req, res, 403, 'message', { 'title': 'Forbidden', - 'message': 'Incorrect captcha', + 'message': 'Incorrect captcha answer', 'redirect': req.headers.referer, }); } @@ -69,12 +69,12 @@ module.exports = async (req, res, next) => { if (isBypass) { return res.status(403).render('bypass', { 'minimal': req.body.minimal, - 'message': 'Incorrect captcha', + 'message': 'Incorrect captcha answer', }); } return dynamicResponse(req, res, 403, 'message', { 'title': 'Forbidden', - 'message': 'Incorrect captcha', + 'message': 'Incorrect captcha answer', 'redirect': req.headers.referer, }); } diff --git a/helpers/paramconverter.js b/helpers/paramconverter.js index 58baa4dd..750b5118 100644 --- a/helpers/paramconverter.js +++ b/helpers/paramconverter.js @@ -5,7 +5,7 @@ const { ObjectId } = require(__dirname+'/../db/db.js') 'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts']) //only these should be arrays, since express bodyparser can output arrays , trimFields = ['tags', 'uri', 'moderators', 'filters', 'announcement', 'description', 'message', 'name', 'subject', 'email', 'postpassword', 'password', 'default_name', 'report_reason', 'ban_reason', 'log_message', 'custom_css'] //trim if we dont want filed with whitespace - , numberFields = ['filter_mode', 'captcha_mode', 'tph_trigger', 'pph_trigger', 'trigger_action', 'reply_limit', 'move_to_thread',, 'postId', + , numberFields = ['filter_mode', 'lock_mode', 'captcha_mode', 'tph_trigger', 'pph_trigger', 'trigger_action', 'reply_limit', 'move_to_thread',, 'postId', 'max_files', 'thread_limit', 'thread', 'max_thread_message_length', 'max_reply_message_length', 'min_thread_message_length', 'min_reply_message_length', 'auth_level'] //convert these to numbers before they hit our routes , banDurationRegex = /^(?[\d]+y)?(?[\d]+m)?(?[\d]+w)?(?[\d]+d)?(?[\d]+h)?$/ , timeUtils = require(__dirname+'/timeutils.js') diff --git a/migrations/index.js b/migrations/index.js index affb1757..53c1c622 100644 --- a/migrations/index.js +++ b/migrations/index.js @@ -4,4 +4,5 @@ module.exports = { '0.0.1': require(__dirname+'/migration-0.0.1.js'), //add bypasses to database '0.0.2': require(__dirname+'/migration-0.0.2.js'), //rename ip field in posts '0.0.3': require(__dirname+'/migration-0.0.3.js'), //move files from /img to /file/ + '0.0.4': require(__dirname+'/migration-0.0.4.js'), //rename some fields for board lock mode and unlisting } diff --git a/migrations/migration-0.0.4.js b/migrations/migration-0.0.4.js new file mode 100644 index 00000000..2b7c5b3f --- /dev/null +++ b/migrations/migration-0.0.4.js @@ -0,0 +1,36 @@ +'use strict'; + +module.exports = async(db, redis) => { + console.log('Renaming some settings fields on boards'); + await db.collection('boards').updateMany({}, { + '$rename': { + 'settings.locked': 'settings.lockMode', + 'settings.unlisted': 'settings.unlistedLocal', + 'settings.webring': 'settings.unlistedWebring' + } + }); + console.log('upadting renamed fields to proper values') + await db.collection('boards').updateMany({ + 'settings.lockMode': true, + }, { + '$set': { + 'settings.lockMode': 2, + } + }); + await db.collection('boards').updateMany({ + 'settings.lockMode': false, + }, { + '$set': { + 'settings.lockMode': 0, + } + }); + await db.collection('boards').updateMany({ + 'settings.triggerAction': 3, + }, { + '$set': { + 'settings.triggerAction': 4, + } + }); + console.log('clearing boards cache'); + await redis.deletePattern('board:*') +}; diff --git a/models/forms/changeboardsettings.js b/models/forms/changeboardsettings.js index 6d206cab..58401247 100644 --- a/models/forms/changeboardsettings.js +++ b/models/forms/changeboardsettings.js @@ -19,7 +19,7 @@ const { Boards, Posts, Accounts } = require(__dirname+'/../../db/') return setting != null; } , arraySetting = (setting, oldSetting, limit) => { - return setting !== null ? setting.split('\r\n').filter(n => n).slice(0,limit) : oldSettings; + return setting !== null ? setting.split(/\r?\n/).filter(n => n).slice(0,limit) : oldSettings; }; module.exports = async (req, res, next) => { @@ -36,7 +36,7 @@ module.exports = async (req, res, next) => { markdownAnnouncement = message; //is there a destructure syntax for this? } - let moderators = req.body.moderators != null ? req.body.moderators.split('\r\n').filter(n => n && !(n == res.locals.board.owner)).slice(0,10) : []; + let moderators = req.body.moderators != null ? req.body.moderators.split(/\r?\n/).filter(n => n && !(n == res.locals.board.owner)).slice(0,10) : []; if (moderators.length === 0 && oldSettings.moderators.length > 0) { //remove all mods if mod list being emptied promises.push(Accounts.removeModBoard(oldSettings.moderators, req.params.board)); @@ -69,9 +69,8 @@ module.exports = async (req, res, next) => { 'theme': req.body.theme || oldSettings.theme, 'codeTheme': req.body.code_theme || oldSettings.codeTheme, 'sfw': booleanSetting(req.body.sfw), - 'unlisted': booleanSetting(req.body.unlisted), - 'webring': booleanSetting(req.body.webring), - 'locked': booleanSetting(req.body.locked), + 'unlistedLocal': booleanSetting(req.body.unlisted_local), + 'unlistedWebring': booleanSetting(req.body.unlisted_webring), 'early404': booleanSetting(req.body.early404), 'ids': booleanSetting(req.body.ids), 'flags': booleanSetting(req.body.flags), @@ -96,6 +95,7 @@ module.exports = async (req, res, next) => { 'minReplyMessageLength': numberSetting(req.body.min_reply_message_length, oldSettings.minReplyMessageLength), 'maxThreadMessageLength': numberSetting(req.body.max_thread_message_length, oldSettings.maxThreadMessageLength), 'maxReplyMessageLength': numberSetting(req.body.max_reply_message_length, oldSettings.maxReplyMessageLength), + 'lockMode': numberSetting(req.body.lock_mode, oldSettings.lockMode), 'filterMode': numberSetting(req.body.filter_mode, oldSettings.filterMode), 'filterBanDuration': numberSetting(req.body.ban_duration, oldSettings.filterBanDuration), 'tags': arraySetting(req.body.tags, oldSettings.tags, 10), diff --git a/models/forms/makepost.js b/models/forms/makepost.js index 21bad230..fe5d8f6b 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -47,12 +47,13 @@ module.exports = async (req, res, next) => { const { filterBanDuration, filterMode, filters, maxFiles, forceAnon, replyLimit, disableReplySubject, threadLimit, ids, userPostSpoiler, pphTrigger, tphTrigger, triggerAction, - captchaMode, locked, allowedFileTypes, flags } = res.locals.board.settings; - if (locked === true && res.locals.permLevel >= 4) { + captchaMode, lockMode, allowedFileTypes, flags } = res.locals.board.settings; + if ((lockMode === 2 || (lockMode === 1 && !req.body.thread)) //if board lock, or thread lock and its a new thread + && res.locals.permLevel >= 4) { //and not staff await deleteTempFiles(req).catch(e => console.error); return dynamicResponse(req, res, 400, 'message', { 'title': 'Bad request', - 'message': 'Board is locked.', + 'message': lockMode === 1 ? 'Thread creation locked' : 'Board locked', 'redirect': redirect }); } @@ -62,7 +63,7 @@ module.exports = async (req, res, next) => { await deleteTempFiles(req).catch(e => console.error); return dynamicResponse(req, res, 400, 'message', { 'title': 'Bad request', - 'message': 'Thread does not exist.', + 'message': 'Thread does not exist', 'redirect': redirect }); } @@ -150,7 +151,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 }); } @@ -226,10 +227,11 @@ module.exports = async (req, res, next) => { const videoData = await ffprobe(req.files.file[i].tempFilePath, null, true); videoData.streams = videoData.streams.filter(stream => stream.width != null); //filter to only video streams or something with a resolution if (videoData.streams.length <= 0) { + //corrupt, or audio only? await deleteTempFiles(req).catch(e => console.error); return dynamicResponse(req, res, 400, 'message', { 'title': 'Bad request', - 'message': 'Audio only video file not supported (yet)', + 'message': 'Audio only video file not supported', 'redirect': redirect }); } @@ -361,10 +363,11 @@ module.exports = async (req, res, next) => { const postId = await Posts.insertOne(res.locals.board, data, thread); let enableCaptcha = false; - if (triggerAction > 0 //trigger is enabled and not already been triggered - && (tphTrigger > 0 || pphTrigger > 0) - && ((triggerAction < 3 && captchaMode < triggerAction) - || (triggerAction === 3 && locked !== true))) { + if (triggerAction > 0 //if trigger is enabled + && (tphTrigger > 0 || pphTrigger > 0) //and has a threshold > 0 + && ((triggerAction < 3 && captchaMode < triggerAction) //and its triggering captcha and captcha isnt on + || (triggerAction === 3 && lockMode < 1) //or triggering locking and board isnt locked + || (triggerAction === 4 && lockMode < 2))) { //read stats to check number threads in past hour const hourPosts = await Stats.getHourPosts(res.locals.board._id); if (hourPosts //if stats exist for this hour and its above either trigger @@ -379,8 +382,11 @@ module.exports = async (req, res, next) => { update['$set']['settings.captchaMode'] = triggerAction; enableCaptcha = true; } else if (triggerAction === 3) { - res.locals.board.settings.locked = true; - update['$set']['settings.locked'] = true; + res.locals.board.settings.lockMode = 1; + update['$set']['settings.lockMode'] = 1; + } else if (triggerAction === 4) { + res.locals.board.settings.lockMode = 2; + update['$set']['settings.lockMode'] = 2; } //set it in the db await Boards.updateOne(res.locals.board._id, update); @@ -397,7 +403,7 @@ module.exports = async (req, res, next) => { }).skip(replyLimit).toArray(); if (cyclicOverflowPosts.length > 0) { await deletePosts(cyclicOverflowPosts, req.params.board); - const fileCount = cyclicOverflowPosts.reduce((post, acc) => { + const fileCount = cyclicOverflowPosts.reduce((acc, post) => { return acc + (post.files ? post.files.length : 0); }, 0); //reduce amount counted in post by number of posts deleted diff --git a/models/pages/captcha.js b/models/pages/captcha.js index 70ec840f..d182c6f7 100644 --- a/models/pages/captcha.js +++ b/models/pages/captcha.js @@ -13,7 +13,7 @@ module.exports = async (req, res, next) => { let captchaId; try { - const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.hash, 'captcha', rateLimitCost.captcha); + const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.single, 'captcha', rateLimitCost.captcha); if (ratelimit > 100) { return res.status(429).redirect('/file/ratelimit.png'); } diff --git a/models/pages/manage/catalog.js b/models/pages/manage/catalog.js new file mode 100644 index 00000000..ae6da08e --- /dev/null +++ b/models/pages/manage/catalog.js @@ -0,0 +1,23 @@ +'use strict'; + +const Posts = require(__dirname+'/../../../db/posts.js'); + +module.exports = async (req, res, next) => { + + let threads; + try { + threads = await Posts.getCatalog(req.params.board); + } catch (err) { + return next(err); + } + + res + .set('Cache-Control', 'private, max-age=5') + .render('catalog', { + modview: true, + threads, + board: res.locals.board, + csrf: req.csrfToken(), + }); + +} diff --git a/models/pages/manage/index.js b/models/pages/manage/index.js index 3c99af70..74e44e24 100644 --- a/models/pages/manage/index.js +++ b/models/pages/manage/index.js @@ -8,5 +8,6 @@ module.exports = { manageLogs: require(__dirname+'/logs.js'), manageBanners: require(__dirname+'/banners.js'), manageBoard: require(__dirname+'/board.js'), + manageCatalog: require(__dirname+'/catalog.js'), manageThread: require(__dirname+'/thread.js'), } diff --git a/package.json b/package.json index ec226bf5..3f401c6b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jschan", "version": "0.0.1", - "migrateVersion": "0.0.3", + "migrateVersion": "0.0.4", "description": "", "main": "server.js", "dependencies": { diff --git a/views/includes/navbar.pug b/views/includes/navbar.pug index 3fbbc6c8..f38c9dfa 100644 --- a/views/includes/navbar.pug +++ b/views/includes/navbar.pug @@ -2,7 +2,9 @@ unless minimal nav.navbar a.nav-item(href='/index.html') Home a.nav-item(href='/news.html') News - a.nav-item(href='/boards.html') Boards + a.nav-item(href='/boards.html') + | Boards + .rainbow +Webring a.nav-item(href='/account.html') Account if board a.nav-item(href=`/${board._id}/manage/reports.html`) Manage diff --git a/views/mixins/catalogtile.pug b/views/mixins/catalogtile.pug index b190f83b..17c0383e 100644 --- a/views/mixins/catalogtile.pug +++ b/views/mixins/catalogtile.pug @@ -2,6 +2,9 @@ mixin catalogtile(board, post, index) .catalog-tile(data-board=post.board data-post-id=post.postId data-user-id=post.userId) - const postURL = `/${board._id}/thread/${post.postId}.html#${post.postId}` .post-info + input.left.post-check(type='checkbox', name='checkedposts' value=post.postId) + if modview + a.left.ml-5.bold(href=`recent.html?postid=${post.postId}`) [+] include ../includes/posticons.pug a.no-decoration.post-subject(href=postURL) #{post.subject || 'No subject'} br diff --git a/views/mixins/managenav.pug b/views/mixins/managenav.pug index 1fe2d76f..e9e1dc70 100644 --- a/views/mixins/managenav.pug +++ b/views/mixins/managenav.pug @@ -6,6 +6,8 @@ mixin managenav(selected, upLevel) else a(href=`${upLevel ? '../' : ''}index.html` class=(selected === 'index' ? 'bold' : '')) [Mod Index] | + a(href=`${upLevel ? '../' : ''}catalog.html` class=(selected === 'catalog' ? 'bold' : '')) [Mod Catalog] + | a(href=`${upLevel ? '../' : ''}recent.html` class=(selected === 'recent' ? 'bold' : '')) [Recent] | a(href=`${upLevel ? '../' : ''}reports.html` class=(selected === 'reports' ? 'bold' : '')) [Reports] diff --git a/views/mixins/post.pug b/views/mixins/post.pug index 214eded9..cd64b047 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -4,7 +4,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false) div(class=`post-container ${post.thread || ban === true ? '' : 'op'}` data-board=post.board data-post-id=post.postId data-user-id=post.userId) - const postURL = `/${post.board}/${modview ? 'manage/' : ''}thread/${post.thread || post.postId}.html`; .post-info - span.noselect + span label if globalmanage input.post-check(type='checkbox', name='globalcheckedposts' value=post._id) diff --git a/views/pages/account.pug b/views/pages/account.pug index 402f44b5..c8defd7a 100644 --- a/views/pages/account.pug +++ b/views/pages/account.pug @@ -26,6 +26,8 @@ block content | - a(href=`/${b}/manage/index.html`) Mod Index | , + a(href=`/${b}/manage/catalog.html`) Mod Catalog + | , a(href=`/${b}/manage/recent.html`) Recent | , a(href=`/${b}/manage/reports.html`) Reports @@ -49,6 +51,8 @@ block content | - a(href=`/${b}/manage/index.html`) Mod Index | , + a(href=`/${b}/manage/catalog.html`) Mod Catalog + | , a(href=`/${b}/manage/recent.html`) Recent | , a(href=`/${b}/manage/reports.html`) Reports diff --git a/views/pages/board.pug b/views/pages/board.pug index 0919018f..fae3ab5b 100644 --- a/views/pages/board.pug +++ b/views/pages/board.pug @@ -15,11 +15,11 @@ block content include ../includes/announcements.pug include ../includes/stickynav.pug if modview - +managenav('index') + +managenav('index') else - .pages - include ../includes/boardpages.pug - +boardnav(null, false, false) + .pages + include ../includes/boardpages.pug + +boardnav(null, false, false) form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}actions` method='POST' enctype='application/x-www-form-urlencoded') if modview input(type='hidden' name='_csrf' value=csrf) diff --git a/views/pages/boardlist.pug b/views/pages/boardlist.pug index 3cfe4a51..0bc2b13a 100644 --- a/views/pages/boardlist.pug +++ b/views/pages/boardlist.pug @@ -32,7 +32,7 @@ block content if board.settings.sfw === true span(title='SFW') 💼 | - if board.settings.unlisted === true + if board.settings.unlistedLocal === true span(title='Unlisted') 👁️ | a(href=`/${board._id}/index.html`) /#{board._id}/ - #{board.settings.name} diff --git a/views/pages/catalog.pug b/views/pages/catalog.pug index 5a1e26c9..3c548425 100644 --- a/views/pages/catalog.pug +++ b/views/pages/catalog.pug @@ -1,6 +1,7 @@ extends ../layout.pug include ../mixins/catalogtile.pug include ../mixins/boardnav.pug +include ../mixins/managenav.pug include ../mixins/boardheader.pug block head @@ -13,15 +14,28 @@ block content br include ../includes/announcements.pug include ../includes/stickynav.pug - .pages - +boardnav('catalog', true, false) - hr(size=1) - if threads.length === 0 - p No posts. - else - .catalog - for thread, i in threads - +catalogtile(board, thread, i+1) - hr(size=1) - .pages - +boardnav('catalog', true, false) + if modview + +managenav('catalog') + else + .pages + +boardnav('catalog', true, false) + form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}actions` method='POST' enctype='application/x-www-form-urlencoded') + if modview + input(type='hidden' name='_csrf' value=csrf) + hr(size=1) + if threads.length === 0 + p No posts. + else + .catalog + for thread, i in threads + +catalogtile(board, thread, i+1) + hr(size=1) + if modview + +managenav('catalog') + else + .pages + +boardnav('catalog', true, false) + if modview + include ../includes/actionfooter_manage.pug + else + include ../includes/actionfooter.pug diff --git a/views/pages/managesettings.pug b/views/pages/managesettings.pug index 5b1be5d3..f6a62236 100644 --- a/views/pages/managesettings.pug +++ b/views/pages/managesettings.pug @@ -150,17 +150,19 @@ block content input(type='checkbox', name='flags', value='true' checked=board.settings.flags) .col .row - .label Board Locked - label.postform-style.ph-5 - input(type='checkbox', name='locked', value='true' checked=board.settings.locked) + .label Lock Mode + select(name='lock_mode') + option(value='0', selected=board.settings.lockMode === 0) Unlocked + option(value='1', selected=board.settings.lockMode === 1) Lock thread creation + option(value='2', selected=board.settings.lockMode === 2) Lock board .row - .label Unlisted + .label Unlist locally label.postform-style.ph-5 - input(type='checkbox', name='unlisted', value='true' checked=board.settings.unlisted) + input(type='checkbox', name='unlisted_local', value='true' checked=board.settings.unlistedLocal) .row - .label Webring + .label Unlist from webring label.postform-style.ph-5 - input(type='checkbox', name='webring', value='true' checked=board.settings.webring) + input(type='checkbox', name='unlisted_webring', value='true' checked=board.settings.unlistedWebring) .row .label SFW label.postform-style.ph-5 @@ -197,7 +199,8 @@ block content option(value='0', selected=board.settings.triggerAction === 0) Do nothing option(value='1', selected=board.settings.triggerAction === 1) Enable captcha for new thread option(value='2', selected=board.settings.triggerAction === 2) Enable captcha for all posts - option(value='3', selected=board.settings.triggerAction === 3) Lock Board + option(value='3', selected=board.settings.triggerAction === 3) Lock thread creation + option(value='4', selected=board.settings.triggerAction === 4) Lock board .row .label Early 404 label.postform-style.ph-5