Merge branch 'dev'

merge-requests/208/head
fatchan 4 years ago
commit 456c8fdfcb
  1. 13
      configs/main.js.example
  2. 2
      controllers/forms/actions.js
  3. 7
      controllers/forms/boardsettings.js
  4. 5
      controllers/forms/globalactions.js
  5. 3
      controllers/pages.js
  6. 10
      db/boards.js
  7. 56
      gulp/res/css/style.css
  8. 3
      gulp/res/css/themes/win95.css
  9. BIN
      gulp/res/icons/favicon2.ico
  10. 12
      gulp/res/js/captcha.js
  11. 60
      gulp/res/js/forms.js
  12. 12
      gulp/res/js/live.js
  13. 10
      gulp/res/js/time.js
  14. 17
      gulp/res/js/titlescroll.js
  15. 2
      gulpfile.js
  16. 8
      helpers/captcha/captchaverify.js
  17. 2
      helpers/paramconverter.js
  18. 1
      migrations/index.js
  19. 36
      migrations/migration-0.0.4.js
  20. 10
      models/forms/changeboardsettings.js
  21. 32
      models/forms/makepost.js
  22. 2
      models/pages/captcha.js
  23. 23
      models/pages/manage/catalog.js
  24. 1
      models/pages/manage/index.js
  25. 2
      package.json
  26. 4
      views/includes/navbar.pug
  27. 3
      views/mixins/catalogtile.pug
  28. 2
      views/mixins/managenav.pug
  29. 2
      views/mixins/post.pug
  30. 4
      views/pages/account.pug
  31. 8
      views/pages/board.pug
  32. 2
      views/pages/boardlist.pug
  33. 38
      views/pages/catalog.pug
  34. 19
      views/pages/managesettings.pug

@ -146,13 +146,14 @@ module.exports = {
//subset of languages to allow. //subset of languages to allow.
languageSubset: [ languageSubset: [
'javascript', 'javascript',
'js',
'typescript', 'typescript',
'perl',
'js',
'c++',
'c',
'java', 'java',
'kotlin', 'kotlin',
'php', 'php',
'c++',
'c',
'h', 'h',
'csharp', 'csharp',
'bash', 'bash',
@ -233,9 +234,9 @@ module.exports = {
theme: 'lain', theme: 'lain',
codeTheme: 'ir-black', codeTheme: 'ir-black',
sfw: false, //safe for work board sfw: false, //safe for work board
locked: false, //board locked, only staff can post lockMode: 0, //board lock mode
unlisted: false, //board hidden from on-site board list and frontpage unlistedLocal: false, //board hidden from on-site board list and frontpage
webring: true, //board shown on webring unlistedWebring: false, //board hidden from webring
captchaMode: 0, //0=disabled, 1=for threads, 2=for all posts captchaMode: 0, //0=disabled, 1=for threads, 2=for all posts
tphTrigger: 0, //numebr of threads in an hour before trigger action is activated tphTrigger: 0, //numebr of threads in an hour before trigger action is activated
pphTrigger: 0, //number of posts in an hour before ^ pphTrigger: 0, //number of posts in an hour before ^

@ -27,6 +27,8 @@ module.exports = async (req, res, next) => {
//50 because checked posts is max 10 and 5 reports max per post //50 because checked posts is max 10 and 5 reports max per post
errors.push('Cannot check more than 50 reports'); 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); res.locals.actions = actionChecker(req);

@ -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})`); 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)) { if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) {
errors.push('Invalid captcha mode'); errors.push('Invalid captcha mode');
} }
if (typeof req.body.tph_trigger === 'number' && (req.body.tph_trigger < 0 || req.body.tph_trigger > 10000)) { if (typeof req.body.tph_trigger === 'number' && (req.body.tph_trigger < 0 || req.body.tph_trigger > 10000)) {
errors.push('Invalid tph trigger threshold'); 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'); errors.push('Invalid tph trigger action');
} }
if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) { 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 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 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) { if (ratelimitBoard > 100 || ratelimitIp > 100) {
return dynamicResponse(req, res, 429, 'message', { return dynamicResponse(req, res, 429, 'message', {
'title': 'Ratelimited', 'title': 'Ratelimited',

@ -24,7 +24,9 @@ module.exports = async (req, res, next) => {
//50 because checked posts is max 10 and 5 reports max per post //50 because checked posts is max 10 and 5 reports max per post
errors.push('Cannot check more than 50 reports'); 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); res.locals.actions = actionChecker(req);
@ -74,6 +76,7 @@ module.exports = async (req, res, next) => {
//edit post, only allowing one //edit post, only allowing one
return res.render('editpost', { return res.render('editpost', {
'post': res.locals.posts[0], 'post': res.locals.posts[0],
'csrf': req.csrfToken(),
}); });
} }

@ -14,7 +14,7 @@ const express = require('express')
, setMinimal = require(__dirname+'/../helpers/setminimal.js') , setMinimal = require(__dirname+'/../helpers/setminimal.js')
//page models //page models
, { manageRecent, manageReports, manageBanners, manageSettings, manageBans, , { 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, , { globalManageSettings, globalManageReports, globalManageBans,
globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/') globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/')
, { changePassword, blockBypass, home, register, login, logout, create, , { 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/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); router.get('/:board/manage/banners.html', sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageBanners);
// if (mod view enabled) { // 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/: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); router.get('/:board/manage/thread/:id([1-9][0-9]{0,}).html', sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(3), csrf, Posts.exists, manageThread);

@ -103,7 +103,7 @@ module.exports = {
boardSort: (skip=0, limit=50, sort={ ips:-1, pph:-1, sequence_value:-1 }, filter={}, showUnlisted=false) => { boardSort: (skip=0, limit=50, sort={ ips:-1, pph:-1, sequence_value:-1 }, filter={}, showUnlisted=false) => {
const addedFilter = {}; const addedFilter = {};
if (!showUnlisted) { if (!showUnlisted) {
addedFilter['settings.unlisted'] = false; addedFilter['settings.unlistedLocal'] = false;
} }
if (filter.search) { if (filter.search) {
addedFilter['$or'] = [ addedFilter['$or'] = [
@ -122,7 +122,7 @@ module.exports = {
'settings.description': 1, 'settings.description': 1,
'settings.name': 1, 'settings.name': 1,
'settings.tags': 1, 'settings.tags': 1,
'settings.unlisted': 1, 'settings.unlistedLocal': 1,
} }
}) })
.sort(sort) .sort(sort)
@ -133,7 +133,7 @@ module.exports = {
webringBoards: () => { webringBoards: () => {
return db.find({ return db.find({
'settings.webring': true 'settings.unlistedWebring': false
}, { }, {
'projection': { 'projection': {
'_id': 1, '_id': 1,
@ -152,7 +152,7 @@ module.exports = {
count: (filter, showUnlisted=false) => { count: (filter, showUnlisted=false) => {
const addedFilter = {}; const addedFilter = {};
if (!showUnlisted) { if (!showUnlisted) {
addedFilter['settings.unlisted'] = false; addedFilter['settings.unlistedLocal'] = false;
} }
if (filter.search) { if (filter.search) {
addedFilter['$or'] = [ addedFilter['$or'] = [
@ -179,7 +179,7 @@ module.exports = {
}, },
'unlisted': { 'unlisted': {
'$sum': { '$sum': {
'$cond': ['$settings.unlisted', 1, 0] '$cond': ['$settings.unlistedLocal', 1, 0]
} }
}, },
//removed ips because sum is inaccurate //removed ips because sum is inaccurate

@ -62,6 +62,25 @@ main.minimal {
text-decoration: line-through; 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 { .underline {
text-decoration: underline; text-decoration: underline;
} }
@ -207,8 +226,8 @@ a, a:visited, a.post-name {
} }
.invalid-quote { .invalid-quote {
cursor:pointer; cursor:pointer;
text-decoration: line-through; text-decoration: line-through;
} }
.post-message a { .post-message a {
@ -277,7 +296,7 @@ p {
} }
/* /*
.upload-list::-webkit-scrollbar { .upload-list::-webkit-scrollbar {
display: none; display: none;
} }
*/ */
.upload-item { .upload-item {
@ -384,7 +403,7 @@ p {
} }
.edited { .edited {
font-style: italic; font-style: italic;
} }
.close { .close {
@ -629,7 +648,7 @@ details.actions div {
.post-check { .post-check {
position: relative; position: relative;
top: 2px; top: 2px;
margin: -3px 1px !important; margin: -3px 1px;
} }
.post-files { .post-files {
@ -773,7 +792,7 @@ input:invalid, textarea:invalid {
box-sizing: border-box; box-sizing: border-box;
padding: .5em; padding: .5em;
max-width: 100%; max-width: 100%;
min-width: 25em; min-width: 30em;
} }
.postmenu { .postmenu {
@ -880,6 +899,11 @@ input:invalid, textarea:invalid {
float: left; float: left;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
text-align: center;
}
.nav-item:nth-of-type(3) {
line-height:1.5em;
} }
.left { .left {
@ -1000,20 +1024,20 @@ iframe.bypass {
} }
.captcharefresh { .captcharefresh {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 5px; left: 5px;
font-size: 18px; font-size: 18px;
cursor: pointer; cursor: pointer;
color: black; color: black;
} }
.captcharefresh { .captcharefresh {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 5px; left: 5px;
font-size: 18px; font-size: 18px;
cursor: pointer; cursor: pointer;
} }
.label, .rlabel { .label, .rlabel {
@ -1145,7 +1169,7 @@ table, .boardtable {
width: 100% width: 100%
} }
row.wrap.sb .col { row.wrap.sb .col {
flex-basis: calc(50% - 5px); flex-basis: calc(50% - 5px);
} }
@media only screen and (max-height: 400px) { @media only screen and (max-height: 400px) {
@ -1263,7 +1287,7 @@ row.wrap.sb .col {
.post-check { .post-check {
top: 1px; top: 1px;
margin-left: 2px!important; margin-left: 2px;
} }
.pages { .pages {

@ -126,6 +126,9 @@ width:unset!important;
input[type=submit], input[type=submit], label[for=file] { input[type=submit], input[type=submit], label[for=file] {
background: var(--post-color)!important; background: var(--post-color)!important;
} }
.footer {
padding-bottom: 3em;
}
@media only screen and (max-width:600px) { @media only screen and (max-width:600px) {
.nav-item { .nav-item {
padding-left: 5px; padding-left: 5px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

@ -31,13 +31,13 @@ window.addEventListener('DOMContentLoaded', (event) => {
}; };
const loadCaptcha = function(e) { const loadCaptcha = function(e) {
const captchaDiv = this.previousSibling; const field = e.target;
const captchaDiv = field.previousSibling;
const captchaImg = document.createElement('img'); const captchaImg = document.createElement('img');
const refreshDiv = document.createElement('div'); const refreshDiv = document.createElement('div');
refreshDiv.classList.add('captcharefresh', 'noselect'); refreshDiv.classList.add('captcharefresh', 'noselect');
refreshDiv.addEventListener('click', refreshCaptchas, true); refreshDiv.addEventListener('click', refreshCaptchas, true);
refreshDiv.textContent = '↻'; refreshDiv.textContent = '↻';
const field = this;
field.placeholder = 'loading'; field.placeholder = 'loading';
captchaImg.src = '/captcha'; captchaImg.src = '/captcha';
captchaImg.onload = function() { captchaImg.onload = function() {
@ -49,8 +49,12 @@ window.addEventListener('DOMContentLoaded', (event) => {
}; };
for (let i = 0; i < captchaFields.length; i++) { for (let i = 0; i < captchaFields.length; i++) {
captchaFields[i].placeholder = 'focus to load captcha'; const field = captchaFields[i];
captchaFields[i].addEventListener('focus', loadCaptcha, { once: true }); if (field.form.action.endsWith('/forms/blockbypass')) {
return loadCaptcha({target: field })
}
field.placeholder = 'focus to load captcha';
field.addEventListener('focus', loadCaptcha, { once: true });
} }
}); });

@ -4,32 +4,39 @@ function removeModal() {
} }
function doModal(data, postcallback) { function doModal(data, postcallback) {
const modalHtml = modal({ modal: data }); try {
let checkInterval; const modalHtml = modal({ modal: data });
document.body.insertAdjacentHTML('afterbegin', modalHtml); let checkInterval;
document.getElementById('modalclose').onclick = () => { document.body.insertAdjacentHTML('afterbegin', modalHtml);
removeModal(); document.getElementById('modalclose').onclick = () => {
clearInterval(checkInterval); removeModal();
}; clearInterval(checkInterval);
document.getElementsByClassName('modal-bg')[0].onclick = () => { };
removeModal(); document.getElementsByClassName('modal-bg')[0].onclick = () => {
clearInterval(checkInterval); removeModal();
}; clearInterval(checkInterval);
const modalframe = document.getElementById('modalframe'); };
modalframe.onload = () => { const modalframe = document.getElementById('modalframe');
if (localStorage.getItem('theme') === 'default') { if (modalframe) {
const currentTheme = document.head.querySelector('#theme').href; //if theres a modal frame and user has default theme, style it
modalframe.contentDocument.styleSheets[1].ownerNode.href = currentTheme; if (localStorage.getItem('theme') === 'default') {
} modalframe.onload = () => {
} const currentTheme = document.head.querySelector('#theme').href;
if (modalframe && postcallback) { modalframe.contentDocument.styleSheets[1].ownerNode.href = currentTheme;
checkInterval = setInterval(() => { }
if (modalframe && modalframe.contentDocument.title == 'Success') {
clearInterval(checkInterval);
removeModal();
postcallback();
} }
}, 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 { } else {
if (json.message || json.messages || json.error || json.errors) { if (json.message || json.messages || json.error || json.errors) {
doModal(json); doModal(json);
if (json.message === 'Incorrect captcha answer') {
//todo: create captcha form, add method to captcha frontend code
}
} else if (socket && socket.connected) { } else if (socket && socket.connected) {
window.myPostId = json.postId; window.myPostId = json.postId;
window.location.hash = json.postId window.location.hash = json.postId

@ -30,7 +30,7 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
lastPostId = data.postId; lastPostId = data.postId;
const postData = data; const postData = data;
//create a new post //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 //add it to the end of the thread
thread.insertAdjacentHTML('beforeend', postHtml); thread.insertAdjacentHTML('beforeend', postHtml);
for (let j = 0; j < postData.quotes.length; j++) { 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 quotedPostData = quotedPost.querySelector('.post-data');
const newRepliesDiv = document.createElement('div'); const newRepliesDiv = document.createElement('div');
newRepliesDiv.textContent = 'Replies: '; newRepliesDiv.textContent = 'Replies: ';
['replies', 'mt-5', 'ml-5'].forEach(c => { newRepliesDiv.classList.add('replies', 'mt-5', 'ml-5');
newRepliesDiv.classList.add(c);
});
quotedPostData.appendChild(newRepliesDiv); quotedPostData.appendChild(newRepliesDiv);
replies = newRepliesDiv; replies = newRepliesDiv;
} }
if (new RegExp(`>>${postData.postId}(\s|$)`).test(replies.innerText)) { 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; continue;
} }
const newReply = document.createElement('a'); const newReply = document.createElement('a');
@ -148,7 +146,9 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
const room = `${roomParts[1]}-${roomParts[roomParts.length-1]}`; const room = `${roomParts[1]}-${roomParts[roomParts.length-1]}`;
socket = io({ socket = io({
transports: ['websocket'], transports: ['websocket'],
reconnectionAttempts: 3 reconnectionAttempts: 3,
reconnectionDelay: 3000,
reconnectionDelayMax: 15000,
}); });
socket.on('connect', async () => { socket.on('connect', async () => {
console.log('socket connected'); console.log('socket connected');

@ -120,10 +120,12 @@ window.addEventListener('settingsReady', function(event) {
window.addEventListener('addPost', function(e) { window.addEventListener('addPost', function(e) {
const date = e.detail.post.querySelector('.reltime'); const dates = e.detail.post.querySelectorAll('.reltime');
if (!e.detail.hover) { for (let date of dates) {
dates.push(date); if (!e.detail.hover) {
dates.push(date);
}
changeDateFormat(date);
} }
changeDateFormat(date);
}); });

@ -4,6 +4,16 @@ window.addEventListener('DOMContentLoaded', (event) => {
let unread = []; let unread = [];
const originalTitle = document.title; 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 isVisible = (e) => {
const top = e.getBoundingClientRect().top; const top = e.getBoundingClientRect().top;
const bottom = e.getBoundingClientRect().bottom; const bottom = e.getBoundingClientRect().bottom;
@ -14,8 +24,10 @@ window.addEventListener('DOMContentLoaded', (event) => {
const updateTitle = () => { const updateTitle = () => {
if (unread.length === 0) { if (unread.length === 0) {
document.title = originalTitle; document.title = originalTitle;
changeFavicon('/favicon.ico');
} else { } else {
document.title = `(${unread.length}) ${originalTitle}`; document.title = `(${unread.length}) ${originalTitle}`;
changeFavicon('/file/favicon2.ico');
} }
} }
@ -37,10 +49,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
} }
} }
window.onfocus = () => { window.onfocus = focusChange;
focusChange();
updateVisible();
}
window.onblur = focusChange; window.onblur = focusChange;
window.addEventListener('scroll', updateVisible); window.addEventListener('scroll', updateVisible);

@ -208,6 +208,8 @@ function scripts() {
`${paths.scripts.src}/post.js`, `${paths.scripts.src}/post.js`,
`${paths.scripts.src}/settings.js`, `${paths.scripts.src}/settings.js`,
`${paths.scripts.src}/live.js`, `${paths.scripts.src}/live.js`,
`${paths.scripts.src}/captcha.js`,
`${paths.scripts.src}/forms.js`,
`${paths.scripts.src}/*.js`, `${paths.scripts.src}/*.js`,
`!${paths.scripts.src}/dragable.js`, `!${paths.scripts.src}/dragable.js`,
`!${paths.scripts.src}/hide.js`, `!${paths.scripts.src}/hide.js`,

@ -27,12 +27,12 @@ module.exports = async (req, res, next) => {
if (isBypass) { if (isBypass) {
return res.status(403).render('bypass', { return res.status(403).render('bypass', {
'minimal': req.body.minimal, 'minimal': req.body.minimal,
'message': 'Incorrect captcha', 'message': 'Incorrect captcha answer',
}); });
} }
return dynamicResponse(req, res, 403, 'message', { return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden', 'title': 'Forbidden',
'message': 'Incorrect captcha', 'message': 'Incorrect captcha answer',
'redirect': req.headers.referer, 'redirect': req.headers.referer,
}); });
} }
@ -69,12 +69,12 @@ module.exports = async (req, res, next) => {
if (isBypass) { if (isBypass) {
return res.status(403).render('bypass', { return res.status(403).render('bypass', {
'minimal': req.body.minimal, 'minimal': req.body.minimal,
'message': 'Incorrect captcha', 'message': 'Incorrect captcha answer',
}); });
} }
return dynamicResponse(req, res, 403, 'message', { return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden', 'title': 'Forbidden',
'message': 'Incorrect captcha', 'message': 'Incorrect captcha answer',
'redirect': req.headers.referer, 'redirect': req.headers.referer,
}); });
} }

@ -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 'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts']) //only these should be arrays, since express bodyparser can output arrays
, trimFields = ['tags', 'uri', 'moderators', 'filters', 'announcement', 'description', 'message', , 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 '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 '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 = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+m)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?$/ , banDurationRegex = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+m)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?$/
, timeUtils = require(__dirname+'/timeutils.js') , timeUtils = require(__dirname+'/timeutils.js')

@ -4,4 +4,5 @@ module.exports = {
'0.0.1': require(__dirname+'/migration-0.0.1.js'), //add bypasses to database '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.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.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
} }

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

@ -19,7 +19,7 @@ const { Boards, Posts, Accounts } = require(__dirname+'/../../db/')
return setting != null; return setting != null;
} }
, arraySetting = (setting, oldSetting, limit) => { , 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) => { 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? 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) { if (moderators.length === 0 && oldSettings.moderators.length > 0) {
//remove all mods if mod list being emptied //remove all mods if mod list being emptied
promises.push(Accounts.removeModBoard(oldSettings.moderators, req.params.board)); 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, 'theme': req.body.theme || oldSettings.theme,
'codeTheme': req.body.code_theme || oldSettings.codeTheme, 'codeTheme': req.body.code_theme || oldSettings.codeTheme,
'sfw': booleanSetting(req.body.sfw), 'sfw': booleanSetting(req.body.sfw),
'unlisted': booleanSetting(req.body.unlisted), 'unlistedLocal': booleanSetting(req.body.unlisted_local),
'webring': booleanSetting(req.body.webring), 'unlistedWebring': booleanSetting(req.body.unlisted_webring),
'locked': booleanSetting(req.body.locked),
'early404': booleanSetting(req.body.early404), 'early404': booleanSetting(req.body.early404),
'ids': booleanSetting(req.body.ids), 'ids': booleanSetting(req.body.ids),
'flags': booleanSetting(req.body.flags), '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), 'minReplyMessageLength': numberSetting(req.body.min_reply_message_length, oldSettings.minReplyMessageLength),
'maxThreadMessageLength': numberSetting(req.body.max_thread_message_length, oldSettings.maxThreadMessageLength), 'maxThreadMessageLength': numberSetting(req.body.max_thread_message_length, oldSettings.maxThreadMessageLength),
'maxReplyMessageLength': numberSetting(req.body.max_reply_message_length, oldSettings.maxReplyMessageLength), '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), 'filterMode': numberSetting(req.body.filter_mode, oldSettings.filterMode),
'filterBanDuration': numberSetting(req.body.ban_duration, oldSettings.filterBanDuration), 'filterBanDuration': numberSetting(req.body.ban_duration, oldSettings.filterBanDuration),
'tags': arraySetting(req.body.tags, oldSettings.tags, 10), 'tags': arraySetting(req.body.tags, oldSettings.tags, 10),

@ -47,12 +47,13 @@ module.exports = async (req, res, next) => {
const { filterBanDuration, filterMode, filters, const { filterBanDuration, filterMode, filters,
maxFiles, forceAnon, replyLimit, disableReplySubject, maxFiles, forceAnon, replyLimit, disableReplySubject,
threadLimit, ids, userPostSpoiler, pphTrigger, tphTrigger, triggerAction, threadLimit, ids, userPostSpoiler, pphTrigger, tphTrigger, triggerAction,
captchaMode, locked, allowedFileTypes, flags } = res.locals.board.settings; captchaMode, lockMode, allowedFileTypes, flags } = res.locals.board.settings;
if (locked === true && res.locals.permLevel >= 4) { 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); await deleteTempFiles(req).catch(e => console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': 'Bad request',
'message': 'Board is locked.', 'message': lockMode === 1 ? 'Thread creation locked' : 'Board locked',
'redirect': redirect 'redirect': redirect
}); });
} }
@ -62,7 +63,7 @@ module.exports = async (req, res, next) => {
await deleteTempFiles(req).catch(e => console.error); await deleteTempFiles(req).catch(e => console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': 'Bad request',
'message': 'Thread does not exist.', 'message': 'Thread does not exist',
'redirect': redirect 'redirect': redirect
}); });
} }
@ -150,7 +151,7 @@ module.exports = async (req, res, next) => {
await deleteTempFiles(req).catch(e => console.error); await deleteTempFiles(req).catch(e => console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', '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 'redirect': redirect
}); });
} }
@ -226,10 +227,11 @@ module.exports = async (req, res, next) => {
const videoData = await ffprobe(req.files.file[i].tempFilePath, null, true); 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 videoData.streams = videoData.streams.filter(stream => stream.width != null); //filter to only video streams or something with a resolution
if (videoData.streams.length <= 0) { if (videoData.streams.length <= 0) {
//corrupt, or audio only?
await deleteTempFiles(req).catch(e => console.error); await deleteTempFiles(req).catch(e => console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': 'Bad request',
'message': 'Audio only video file not supported (yet)', 'message': 'Audio only video file not supported',
'redirect': redirect 'redirect': redirect
}); });
} }
@ -361,10 +363,11 @@ module.exports = async (req, res, next) => {
const postId = await Posts.insertOne(res.locals.board, data, thread); const postId = await Posts.insertOne(res.locals.board, data, thread);
let enableCaptcha = false; let enableCaptcha = false;
if (triggerAction > 0 //trigger is enabled and not already been triggered if (triggerAction > 0 //if trigger is enabled
&& (tphTrigger > 0 || pphTrigger > 0) && (tphTrigger > 0 || pphTrigger > 0) //and has a threshold > 0
&& ((triggerAction < 3 && captchaMode < triggerAction) && ((triggerAction < 3 && captchaMode < triggerAction) //and its triggering captcha and captcha isnt on
|| (triggerAction === 3 && locked !== true))) { || (triggerAction === 3 && lockMode < 1) //or triggering locking and board isnt locked
|| (triggerAction === 4 && lockMode < 2))) {
//read stats to check number threads in past hour //read stats to check number threads in past hour
const hourPosts = await Stats.getHourPosts(res.locals.board._id); const hourPosts = await Stats.getHourPosts(res.locals.board._id);
if (hourPosts //if stats exist for this hour and its above either trigger 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; update['$set']['settings.captchaMode'] = triggerAction;
enableCaptcha = true; enableCaptcha = true;
} else if (triggerAction === 3) { } else if (triggerAction === 3) {
res.locals.board.settings.locked = true; res.locals.board.settings.lockMode = 1;
update['$set']['settings.locked'] = true; 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 //set it in the db
await Boards.updateOne(res.locals.board._id, update); await Boards.updateOne(res.locals.board._id, update);
@ -397,7 +403,7 @@ module.exports = async (req, res, next) => {
}).skip(replyLimit).toArray(); }).skip(replyLimit).toArray();
if (cyclicOverflowPosts.length > 0) { if (cyclicOverflowPosts.length > 0) {
await deletePosts(cyclicOverflowPosts, req.params.board); 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); return acc + (post.files ? post.files.length : 0);
}, 0); }, 0);
//reduce amount counted in post by number of posts deleted //reduce amount counted in post by number of posts deleted

@ -13,7 +13,7 @@ module.exports = async (req, res, next) => {
let captchaId; let captchaId;
try { 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) { if (ratelimit > 100) {
return res.status(429).redirect('/file/ratelimit.png'); return res.status(429).redirect('/file/ratelimit.png');
} }

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

@ -8,5 +8,6 @@ module.exports = {
manageLogs: require(__dirname+'/logs.js'), manageLogs: require(__dirname+'/logs.js'),
manageBanners: require(__dirname+'/banners.js'), manageBanners: require(__dirname+'/banners.js'),
manageBoard: require(__dirname+'/board.js'), manageBoard: require(__dirname+'/board.js'),
manageCatalog: require(__dirname+'/catalog.js'),
manageThread: require(__dirname+'/thread.js'), manageThread: require(__dirname+'/thread.js'),
} }

@ -1,7 +1,7 @@
{ {
"name": "jschan", "name": "jschan",
"version": "0.0.1", "version": "0.0.1",
"migrateVersion": "0.0.3", "migrateVersion": "0.0.4",
"description": "", "description": "",
"main": "server.js", "main": "server.js",
"dependencies": { "dependencies": {

@ -2,7 +2,9 @@ unless minimal
nav.navbar nav.navbar
a.nav-item(href='/index.html') Home a.nav-item(href='/index.html') Home
a.nav-item(href='/news.html') News 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 a.nav-item(href='/account.html') Account
if board if board
a.nav-item(href=`/${board._id}/manage/reports.html`) Manage a.nav-item(href=`/${board._id}/manage/reports.html`) Manage

@ -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) .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}` - const postURL = `/${board._id}/thread/${post.postId}.html#${post.postId}`
.post-info .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 include ../includes/posticons.pug
a.no-decoration.post-subject(href=postURL) #{post.subject || 'No subject'} a.no-decoration.post-subject(href=postURL) #{post.subject || 'No subject'}
br br

@ -6,6 +6,8 @@ mixin managenav(selected, upLevel)
else else
a(href=`${upLevel ? '../' : ''}index.html` class=(selected === 'index' ? 'bold' : '')) [Mod Index] 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 ? '../' : ''}recent.html` class=(selected === 'recent' ? 'bold' : '')) [Recent]
| |
a(href=`${upLevel ? '../' : ''}reports.html` class=(selected === 'reports' ? 'bold' : '')) [Reports] a(href=`${upLevel ? '../' : ''}reports.html` class=(selected === 'reports' ? 'bold' : '')) [Reports]

@ -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) 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`; - const postURL = `/${post.board}/${modview ? 'manage/' : ''}thread/${post.thread || post.postId}.html`;
.post-info .post-info
span.noselect span
label label
if globalmanage if globalmanage
input.post-check(type='checkbox', name='globalcheckedposts' value=post._id) input.post-check(type='checkbox', name='globalcheckedposts' value=post._id)

@ -26,6 +26,8 @@ block content
| - | -
a(href=`/${b}/manage/index.html`) Mod Index 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/recent.html`) Recent
| , | ,
a(href=`/${b}/manage/reports.html`) Reports 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/index.html`) Mod Index
| , | ,
a(href=`/${b}/manage/catalog.html`) Mod Catalog
| ,
a(href=`/${b}/manage/recent.html`) Recent a(href=`/${b}/manage/recent.html`) Recent
| , | ,
a(href=`/${b}/manage/reports.html`) Reports a(href=`/${b}/manage/reports.html`) Reports

@ -15,11 +15,11 @@ block content
include ../includes/announcements.pug include ../includes/announcements.pug
include ../includes/stickynav.pug include ../includes/stickynav.pug
if modview if modview
+managenav('index') +managenav('index')
else else
.pages .pages
include ../includes/boardpages.pug include ../includes/boardpages.pug
+boardnav(null, false, false) +boardnav(null, false, false)
form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}actions` method='POST' enctype='application/x-www-form-urlencoded') form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}actions` method='POST' enctype='application/x-www-form-urlencoded')
if modview if modview
input(type='hidden' name='_csrf' value=csrf) input(type='hidden' name='_csrf' value=csrf)

@ -32,7 +32,7 @@ block content
if board.settings.sfw === true if board.settings.sfw === true
span(title='SFW') &#x1F4BC; span(title='SFW') &#x1F4BC;
| |
if board.settings.unlisted === true if board.settings.unlistedLocal === true
span(title='Unlisted') &#x1F441;&#xFE0F; span(title='Unlisted') &#x1F441;&#xFE0F;
| |
a(href=`/${board._id}/index.html`) /#{board._id}/ - #{board.settings.name} a(href=`/${board._id}/index.html`) /#{board._id}/ - #{board.settings.name}

@ -1,6 +1,7 @@
extends ../layout.pug extends ../layout.pug
include ../mixins/catalogtile.pug include ../mixins/catalogtile.pug
include ../mixins/boardnav.pug include ../mixins/boardnav.pug
include ../mixins/managenav.pug
include ../mixins/boardheader.pug include ../mixins/boardheader.pug
block head block head
@ -13,15 +14,28 @@ block content
br br
include ../includes/announcements.pug include ../includes/announcements.pug
include ../includes/stickynav.pug include ../includes/stickynav.pug
.pages if modview
+boardnav('catalog', true, false) +managenav('catalog')
hr(size=1) else
if threads.length === 0 .pages
p No posts. +boardnav('catalog', true, false)
else form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}actions` method='POST' enctype='application/x-www-form-urlencoded')
.catalog if modview
for thread, i in threads input(type='hidden' name='_csrf' value=csrf)
+catalogtile(board, thread, i+1) hr(size=1)
hr(size=1) if threads.length === 0
.pages p No posts.
+boardnav('catalog', true, false) 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

@ -150,17 +150,19 @@ block content
input(type='checkbox', name='flags', value='true' checked=board.settings.flags) input(type='checkbox', name='flags', value='true' checked=board.settings.flags)
.col .col
.row .row
.label Board Locked .label Lock Mode
label.postform-style.ph-5 select(name='lock_mode')
input(type='checkbox', name='locked', value='true' checked=board.settings.locked) 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 .row
.label Unlisted .label Unlist locally
label.postform-style.ph-5 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 .row
.label Webring .label Unlist from webring
label.postform-style.ph-5 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 .row
.label SFW .label SFW
label.postform-style.ph-5 label.postform-style.ph-5
@ -197,7 +199,8 @@ block content
option(value='0', selected=board.settings.triggerAction === 0) Do nothing 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='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='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 .row
.label Early 404 .label Early 404
label.postform-style.ph-5 label.postform-style.ph-5

Loading…
Cancel
Save