Merge branch 'new-dev' into 'master'

New dev

Closes #278, #285, and #284

See merge request fatchan/jschan!190
merge-requests/208/head
Thomas Lynch 4 years ago
commit 1d0ae085cb
  1. 1
      .gitignore
  2. 10
      configs/main.js.example
  3. 4
      controllers/forms/actions.js
  4. 7
      controllers/pages.js
  5. 37
      db/posts.js
  6. 8
      gulp/res/css/style.css
  7. 2
      gulp/res/css/themes/win95.css
  8. 10
      gulp/res/js/captcha.js
  9. 9
      gulp/res/js/captchaformsection.js
  10. 22
      gulp/res/js/forms.js
  11. 2
      gulp/res/js/quote.js
  12. 17
      gulpfile.js
  13. 4
      helpers/captcha/verify.js
  14. 20
      helpers/checks/captcha.js
  15. 6
      helpers/posting/markdown.js
  16. 10
      helpers/posting/name.js
  17. 39
      helpers/posting/tripcode.js
  18. 3
      helpers/render.js
  19. 1
      models/forms/editpost.js
  20. 3
      models/forms/makepost.js
  21. 4
      models/pages/captcha.js
  22. 1
      models/pages/index.js
  23. 26
      models/pages/overboardcatalog.js
  24. 49
      package-lock.json
  25. 4
      package.json
  26. 4
      schedules/webring.js
  27. 3
      server.js
  28. 77
      views/custompages/faq.pug.example
  29. 7
      views/includes/captcha.pug
  30. 4
      views/includes/head.pug
  31. 2
      views/includes/navbar.pug
  32. 18
      views/mixins/catalogtile.pug
  33. 21
      views/mixins/post.pug
  34. 3
      views/pages/boardlist.pug
  35. 2
      views/pages/catalog.pug
  36. 3
      views/pages/editpost.pug
  37. 5
      views/pages/managesettings.pug
  38. 10
      views/pages/overboard.pug
  39. 33
      views/pages/overboardcatalog.pug

1
.gitignore vendored

@ -12,3 +12,4 @@ gulp/res/js/locals.js
gulp/res/js/post.js
gulp/res/js/modal.js
tmp/
.idea/

@ -41,12 +41,16 @@ module.exports = {
//settings for captchas
captchaOptions: {
type: 'grid', //"text", "grid" or "google". If using google, make sure your CSP header in nginx config allows the google domain.
type: 'grid', //"text", "grid", "hcaptcha" or "google". If using google/hcaptcha, make sure your CSP header in nginx config allows the google/hcaptcha domain.
generateLimit: 1000, //max number of captchas to have generated at any time, prevent mass unsolved captcha spam, especially on TOR.
google: { //options for google captcha, when captcha type is google
siteKey: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
secretKey: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
},
hcaptcha: {
siteKey: "10000000-ffff-ffff-ffff-000000000001",
secretKey: "0x0000000000000000000000000000000000000000"
},
grid: {
size: 4,
imageSize: 120,
@ -97,8 +101,10 @@ module.exports = {
editPost: 30,
},
//how many threads to show on overboard
//how many threads to show on overboard index view
overboardLimit: 20,
//how many threads to show on overboard catalog view
overboardCatalogLimit: 50,
//cache templates in memory. disable only if editing templates and doing dev work
cacheTemplates: true,

@ -117,13 +117,13 @@ module.exports = async (req, res, next) => {
});
} else if (req.body.move) {
res.locals.posts = res.locals.posts.filter(p => {
//filter to remove any posts already in the thread (or the OP) of move destionation
//filter to remove any posts already in the thread (or the OP) of move destination
return p.postId !== req.body.move_to_thread && p.thread !== req.body.move_to_thread;
});
if (res.locals.posts.length === 0) {
return dynamicResponse(req, res, 429, 'message', {
'title': 'Conflict',
'error': 'Destionation thread cannot match source thread for move action',
'error': 'Destination thread cannot match source thread for move action',
'redirect': `/${req.params.board}/`
});
}

@ -22,7 +22,7 @@ const express = require('express')
, { globalManageSettings, globalManageReports, globalManageBans, globalManageBoards,
globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/')
, { changePassword, blockBypass, home, register, login, create,
board, catalog, banners, randombanner, news, captchaPage, overboard,
board, catalog, banners, randombanner, news, captchaPage, overboard, overboardCatalog,
captcha, thread, modlog, modloglist, account, boardlist } = require(__dirname+'/../models/pages/');
//homepage
@ -41,7 +41,8 @@ router.get('/:board/catalog.(html|json)', Boards.exists, catalog); //catalog
router.get('/:board/logs.html', Boards.exists, modloglist);//modlog list
router.get('/:board/logs/:date(\\d{2}-\\d{2}-\\d{4}).html', Boards.exists, paramConverter, modlog); //daily log
router.get('/:board/banners.html', Boards.exists, banners); //banners
router.get('/all.html', overboard); //overboard
router.get('/overboard.html', overboard); //overboard
router.get('/catalog.html', overboardCatalog); //overboard catalog view
router.get('/create.html', useSession, sessionRefresh, isLoggedIn, create); //create new board
router.get('/randombanner', randombanner); //random banner
@ -68,7 +69,7 @@ router.get('/globalmanage/accounts.html', useSession, sessionRefresh, isLoggedIn
router.get('/globalmanage/settings.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageSettings);
//captcha
if (captchaOptions.type !== 'google') {
if (captchaOptions.type !== 'google' && captchaOptions.type !== 'hcaptcha') {
router.get('/captcha', geoAndTor, processIp, captcha); //get captcha image and cookie
}
router.get('/captcha.html', captchaPage); //iframed for noscript users

@ -250,13 +250,32 @@ module.exports = {
}).toArray();
},
getCatalog: (board) => {
getCatalog: (board, sortSticky=true, catalogLimit=0) => {
const threadsQuery = {
thread: null,
}
if (board) {
if (Array.isArray(board)) {
//array for overboard catalog
threadsQuery['board'] = {
'$in': board
}
} else {
threadsQuery['board'] = board;
}
}
let threadsSort = {
'bumped': -1,
};
if (sortSticky === true) {
threadsSort = {
'sticky': -1,
'bumped': -1
}
}
// get all threads for catalog
return db.find({
'thread': null,
'board': board
}, {
return db.find(threadsQuery, {
'projection': {
'salt': 0,
'password': 0,
@ -264,10 +283,10 @@ module.exports = {
'reports': 0,
'globalreports': 0,
}
}).sort({
'sticky': -1,
'bumped': -1,
}).toArray();
})
.limit(catalogLimit)
.sort(threadsSort)
.toArray();
},

@ -116,6 +116,14 @@ pre {
white-space: pre;
}
.aa {
font-family: Monapo, Mona, 'MS Pgothic', 'MS P繧エ繧キ繝<EFBFBD>け', IPAMonaPGothic, 'IPA 繝「繝翫<EFBFBD> P繧エ繧キ繝<EFBFBD>け', submona !important;
font-size: 16px;
display: block;
overflow-x: auto;
white-space: pre;
}
.code:not(.hljs) {
white-space: unset;
}

@ -65,7 +65,7 @@ font-weight: bold
.navbar a {
color:black;
}
.anchor:target+.post-container .post-info, .post-container.highlighted .post-info, .anchor:target+table tbody tr th {
.anchor:target+.post-container .post-info, .post-container.highlighted .post-info, .post-container.hoverhighlighted .post-info, .anchor:target+table tbody tr th {
background: darkblue!important;
color: white!important;
}

@ -80,6 +80,16 @@ class CaptchaController {
xhr.send(null);
}
removeCaptcha() {
const postForm = document.getElementById('postform');
const captchaField = postForm.querySelector('.captcha');
if (captchaField) {
//delete the whole row
const captchaRow = captchaField.closest('.row');
captchaRow.remove();
}
}
addMissingCaptcha() {
const postSubmitButton = document.getElementById('submitpost');
const captchaFormSectionHtml = captchaformsection({ captchaGridSize });

@ -3,7 +3,7 @@ function pug_escape(e){var a=""+e,t=pug_match_html.exec(a);if(!t)return e;var r,
var pug_match_html=/["&<>]/;function captchaformsection(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;
var locals_for_with = (locals || {});
(function (captchaGridSize, captchaType, googleRecaptchaSiteKey, minimal) {
(function (captchaGridSize, captchaType, googleRecaptchaSiteKey, hcaptchaSiteKey, minimal) {
pug_mixins["captchaexpand"] = pug_interp = function(){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cdetails class=\"row label mr-0\"\u003E\u003Csummary class=\"pv-5\"\u003ECaptcha\u003Cspan class=\"required\"\u003E*\u003C\u002Fspan\u003E\u003C\u002Fsummary\u003E";
@ -11,6 +11,9 @@ switch (captchaType){
case 'google':
pug_html = pug_html + "\u003Cdiv" + (" class=\"g-recaptcha\""+pug_attr("data-sitekey", `${googleRecaptchaSiteKey}`, true, false)+" data-theme=\"dark\" data-size=\"compact\" data-callback=\"recaptchaCallback\"") + "\u003E\u003C\u002Fdiv\u003E\u003Cnoscript\u003EPlease enable JavaScript to solve the captcha.\u003C\u002Fnoscript\u003E";
break;
case 'hcaptcha':
pug_html = pug_html + "\u003Cdiv" + (" class=\"h-captcha\""+pug_attr("data-sitekey", `${hcaptchaSiteKey}`, true, false)+" data-theme=\"dark\" data-size=\"compact\" data-callback=\"recaptchaCallback\"") + "\u003E\u003C\u002Fdiv\u003E\u003Cnoscript\u003EPlease enable JavaScript to solve the captcha.\u003C\u002Fnoscript\u003E";
break;
case 'text':
pug_html = pug_html + "\u003Cnoscript class=\"no-m-p\"\u003E\u003Ciframe" + (" class=\"captcha\""+" src=\"\u002Fcaptcha.html\""+pug_attr("width=210", true, true, false)+" height=\"80\" scrolling=\"no\" loading=\"lazy\"") + "\u003E\u003C\u002Fiframe\u003E\u003C\u002Fnoscript\u003E\u003Cdiv class=\"jsonly captcha\" style=\"display:none;\"\u003E\u003C\u002Fdiv\u003E\u003Cinput" + (" class=\"captchafield\""+" type=\"text\" name=\"captcha\" autocomplete=\"off\" placeholder=\"Captcha text\" pattern=\".{6}\""+pug_attr("required", true, true, false)+" title=\"6 characters\"") + "\u002F\u003E";
break;
@ -34,7 +37,9 @@ pug_mixins["captchaexpand"]();
locals_for_with.captchaType :
typeof captchaType !== 'undefined' ? captchaType : undefined, "googleRecaptchaSiteKey" in locals_for_with ?
locals_for_with.googleRecaptchaSiteKey :
typeof googleRecaptchaSiteKey !== 'undefined' ? googleRecaptchaSiteKey : undefined, "minimal" in locals_for_with ?
typeof googleRecaptchaSiteKey !== 'undefined' ? googleRecaptchaSiteKey : undefined, "hcaptchaSiteKey" in locals_for_with ?
locals_for_with.hcaptchaSiteKey :
typeof hcaptchaSiteKey !== 'undefined' ? hcaptchaSiteKey : undefined, "minimal" in locals_for_with ?
locals_for_with.minimal :
typeof minimal !== 'undefined' ? minimal : undefined));
;;return pug_html;}

@ -67,15 +67,13 @@ let recaptchaResponse = null;
function recaptchaCallback(response) {
recaptchaResponse = response;
}
class formHandler {
constructor(form) {
this.form = form;
this.enctype = this.form.getAttribute('enctype');
this.messageBox = form.querySelector('#message');
this.captchaField = form.querySelector('.captchafield') || form.querySelector('.g-recaptcha');
this.captchaField = form.querySelector('.captchafield') || form.querySelector('.g-recaptcha') || form.querySelector('.h-captcha');
this.submit = form.querySelector('input[type="submit"]');
if (this.submit) {
this.originalSubmitText = this.submit.value;
@ -126,11 +124,12 @@ class formHandler {
formSubmit(e) {
const xhr = new XMLHttpRequest();
let postData;
const captchaResponse = recaptchaResponse;
if (this.enctype === 'multipart/form-data') {
this.fileInput && (this.fileInput.disabled = true);
postData = new FormData(this.form);
if (recaptchaResponse) {
postData.append('captcha', recaptchaResponse);
if (captchaResponse) {
postData.append('captcha', captchaResponse);
}
this.fileInput && (this.fileInput.disabled = false);
if (this.files && this.files.length > 0) {
@ -141,8 +140,8 @@ class formHandler {
}
} else {
postData = new URLSearchParams([...(new FormData(this.form))]);
if (recaptchaResponse) {
postData.set('captcha', recaptchaResponse);
if (captchaResponse) {
postData.set('captcha', captchaResponse);
}
}
if (this.banned
@ -169,8 +168,15 @@ class formHandler {
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (recaptchaResponse && grecaptcha) {
if (captchaResponse && grecaptcha) {
grecaptcha.reset();
} else if(captchaResponse && hcaptcha) {
hcaptcha.reset();
}
if (xhr.getResponseHeader('x-captcha-enabled') === 'false') {
//remove captcha if it got disabled after you opened the page
captchaController.removeCaptcha();
this.captchaField = null;
}
this.submit.disabled = false;
this.submit.value = this.originalSubmitText;

@ -54,7 +54,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
}
const quote = function(e) {
const quoteNum = this.textContent.replace('[Reply]', '').split(' ')[0].trim();
const quoteNum = this.textContent.trim();
if (isThread && !e.ctrlKey) {
addQuote(quoteNum);
} else {

@ -138,15 +138,17 @@ async function wipe() {
async function css() {
try {
//a little more configurable
let bypassHeight = configs.captchaOptions.type === 'google' ? 500
: configs.captchaOptions.type === 'grid' ? 330
: 235;
let bypassHeight = (configs.captchaOptions.type === 'google' || configs.captchaOptions.type === 'hcaptcha')
? 500
: configs.captchaOptions.type === 'grid'
? 330
: 235;
let captchaHeight = configs.captchaOptions.type === 'text' ? 80
: configs.captchaOptions.type === 'grid' ? configs.captchaOptions.grid.imageSize+30
: 200; //'google' doesnt need this set
: 200; //google/hcaptcha doesnt need this set
let captchaWidth = configs.captchaOptions.type === 'text' ? 210
: configs.captchaOptions.type === 'grid' ? configs.captchaOptions.grid.imageSize+30
: 200; //'google' doesnt need this set
: 200; //google/hcaptcha doesnt need this set
const cssLocals = `:root {
--attachment-img: url('/file/attachment.png');
--spoiler-img: url('/file/spoiler.png');
@ -216,6 +218,8 @@ async function cache() {
Redis.deletePattern('banners:*'),
Redis.deletePattern('users:*'),
Redis.deletePattern('blacklisted:*'),
Redis.deletePattern('overboard'),
Redis.deletePattern('catalog'),
]);
Redis.redisClient.quit();
}
@ -235,6 +239,8 @@ function custompages() {
])
.pipe(gulppug({
locals: {
early404Fraction: configs.early404Fraction,
early404Replies: configs.early404Replies,
meta: configs.meta,
enableWebring: configs.enableWebring,
globalLimits: configs.globalLimits,
@ -244,6 +250,7 @@ function custompages() {
postFilesSize: formatSize(configs.globalLimits.postFilesSize.max),
captchaType: configs.captchaOptions.type,
googleRecaptchaSiteKey: configs.captchaOptions.google.siteKey,
hcaptchaSitekey: configs.captchaOptions.hcaptcha.siteKey,
captchaGridSize: configs.captchaOptions.grid.size,
commit,
}

@ -25,7 +25,7 @@ module.exports = async (req, res, next) => {
}
}
const captchaInput = req.body.captcha || req.body['g-recaptcha-response'];
const captchaInput = req.body.captcha || req.body['g-recaptcha-response'] || req.body["h-captcha-response"];
const captchaId = req.cookies.captchaid;
try {
await checkCaptcha(captchaInput, captchaId);
@ -45,7 +45,7 @@ module.exports = async (req, res, next) => {
//it was correct, so mark as solved for other middleware
res.locals.solvedCaptcha = true;
if (captchaOptions.type !== 'google') {
if (captchaOptions.type !== 'google' && captchaOptions.type !== 'hcaptcha') {
//for builtin captchas, clear captchaid cookie, delete file and reset quota
res.clearCookie('captchaid');
await Promise.all([

@ -16,7 +16,7 @@ module.exports = async (captchaInput, captchaId) => {
}
//make sure they have captcha cookie and its 24 chars
if (captchaOptions.type !== 'google'
if ((captchaOptions.type !== 'google' && captchaOptions.type !== 'hcaptcha')
&& (!captchaId || captchaId.length !== 24)) {
throw 'Captcha expired';
}
@ -69,6 +69,24 @@ module.exports = async (captchaInput, captchaId) => {
throw 'Incorrect captcha answer';
}
break;
case 'hcaptcha':
let hcaptchaResponse;
try {
const form = new FormData();
form.append('secret', captchaOptions.hcaptcha.secretKey);
form.append('sitekey', captchaOptions.hcaptcha.siteKey);
form.append('response', captchaInput[0]);
hcaptchaResponse = await fetch('https://hcaptcha.com/siteverify', {
method: 'POST',
body: form,
}).then(res => res.json());
} catch (e) {
throw 'Captcha error occurred';
}
if (!hcaptchaResponse || !hcaptchaResponse.success) {
throw 'Incorrect captcha answer';
}
break;
default:
throw 'Captcha config error';
break;

@ -12,8 +12,8 @@ const greentextRegex = /^&gt;((?!&gt;\d+|&gt;&gt;&#x2F;\w+(&#x2F;\d*)?).*)/gm
, detectedRegex = /(\(\(\(.+?\)\)\))/gm
, linkRegex = /https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^]+/g
, codeRegex = /(?:(?<language>[a-z+]{1,10})\r?\n)?(?<code>[\s\S]+)/i
, includeSplitRegex = /(```[\s\S]+?```)/gm
, splitRegex = /```([\s\S]+?)```/gm
, includeSplitRegex = /(\[code\][\s\S]+?\[\/code\])/gm
, splitRegex = /\[code\]([\s\S]+?)\[\/code\]/gm
, trimNewlineRegex = /^\s*(\r?\n)*|(\r?\n)*$/g
, getDomain = (string) => string.split(/\/\/|\//)[1] //unused atm
, escape = require(__dirname+'/escape.js')
@ -82,6 +82,8 @@ module.exports = {
} else if (lang !== 'plain' && highlightOptions.languageSubset.includes(lang)) {
const { value } = highlight(lang, trimFix, true);
return `<span class='code hljs'><small>language: ${lang}</small>\n${value}</span>`;
} else if (lang === 'aa') {
return `<span class='aa'>${escape(matches.groups.code)}</span>`;
}
return `<span class='code'>${escape(trimFix)}</span>`;
},

@ -1,7 +1,7 @@
'use strict';
const getTripCode = require(__dirname+'/tripcode.js')
, nameRegex = /^(?<name>(?!##).*?)?(?:##(?<tripcode>[^ ]{1}.*?))?(?<capcode>##(?<capcodetext> .*?)?)?$/
const { getInsecureTrip, getSecureTrip } = require(__dirname+'/tripcode.js')
, nameRegex = /^(?<name>(?!##|#).*?)?(?:(?<secure>##|#)(?<tripcode>[^# ].+?))?(?<capcode>##(?<capcodetext> .*?)?)?$/
, staffLevels = ['Admin', 'Global Staff', 'Board Owner', 'Board Mod']
, staffLevelsRegex = new RegExp(`(${staffLevels.join('|')})+`, 'igm')
@ -25,7 +25,11 @@ module.exports = async (inputName, permLevel, boardSettings, boardOwner, usernam
}
//tripcode
if (groups.tripcode) {
tripcode = `!!${(await getTripCode(groups.tripcode))}`;
if (groups.secure.length === 1) {
tripcode = `!${getInsecureTrip(groups.tripcode)}`;
} else {
tripcode = `!!${(await getSecureTrip(groups.tripcode))}`;
}
}
//capcode
if (permLevel < 4 && groups.capcode) {

@ -2,11 +2,42 @@
const { tripcodeSecret } = require(__dirname+'/../../configs/main.js')
, { createHash } = require('crypto')
, { encode } = require('iconv-lite')
, crypt = require('unix-crypt-td-js')
, replace = {
':': 'A',
';': 'B',
'<': 'C',
'=': 'D',
'>': 'E',
'?': 'F',
'@': 'G',
'[': 'a',
'\\': 'b',
']': 'c',
'^': 'd',
'_': 'e',
'`': 'f',
};
module.exports = async (password) => {
module.exports = {
const tripcodeHash = createHash('sha256').update(password + tripcodeSecret).digest('base64');
const tripcode = tripcodeHash.substring(tripcodeHash.length-10);
return tripcode;
getSecureTrip: async (password) => {
const tripcodeHash = createHash('sha256').update(password + tripcodeSecret).digest('base64');
const tripcode = tripcodeHash.substring(tripcodeHash.length-10);
return tripcode;
},
getInsecureTrip: (password) => {
const encoded = encode(password, 'SHIFT_JIS');
let salt = `${encoded}H..`
.substring(1, 3)
.replace(/[^.-z]/g, '.');
for (let find in replace) {
salt = salt.split(find).join(replace[find]);
}
const hashed = crypt(encoded, salt);
return hashed.slice(-10);
},
}

@ -29,6 +29,9 @@ switch (captchaOptions.type) {
case 'google':
renderLocals.googleRecaptchaSiteKey = captchaOptions.google.siteKey;
break;
case 'hcaptcha':
renderLocals.hcaptchaSitekey = captchaOptions.hcaptcha.siteKey;
break;
case 'grid':
renderLocals.captchaGridSize = captchaOptions.grid.size;
break;

@ -3,7 +3,6 @@
const { Posts, Bans, Modlogs } = require(__dirname+'/../../db/')
, { createHash } = require('crypto')
, Mongo = require(__dirname+'/../../db/db.js')
, getTripCode = require(__dirname+'/../../helpers/posting/tripcode.js')
, { prepareMarkdown } = require(__dirname+'/../../helpers/posting/markdown.js')
, messageHandler = require(__dirname+'/../../helpers/posting/message.js')
, nameHandler = require(__dirname+'/../../helpers/posting/name.js')

@ -553,6 +553,9 @@ module.exports = async (req, res, next) => {
'board': res.locals.board
};
//let frontend script know if captcha is still enabled
res.set('x-captcha-enabled', captchaMode > 0);
if (req.headers['x-using-live'] != null && data.thread) {
//defer build and post will come live
res.json({

@ -2,7 +2,9 @@
const { Captchas, Ratelimits } = require(__dirname+'/../../db/')
, { secureCookies, rateLimitCost, captchaOptions } = require(__dirname+'/../../configs/main.js')
, generateCaptcha = captchaOptions.type !== 'google' ? require(__dirname+`/../../helpers/captcha/generators/${captchaOptions.type}.js`) : null
, generateCaptcha = (captchaOptions.type !== 'google' && captchaOptions.type !== 'hcaptcha')
? require(__dirname+`/../../helpers/captcha/generators/${captchaOptions.type}.js`)
: null
, production = process.env.NODE_ENV === 'production';
module.exports = async (req, res, next) => {

@ -20,4 +20,5 @@ module.exports = {
modloglist: require(__dirname+'/modloglist.js'),
boardlist: require(__dirname+'/boardlist.js'),
overboard: require(__dirname+'/overboard.js'),
overboardCatalog: require(__dirname+'/overboardcatalog.js'),
}

@ -0,0 +1,26 @@
'use strict';
const { Posts, Boards } = require(__dirname+'/../../db/')
, cache = require(__dirname+'/../../redis.js')
, { overboardCatalogLimit } = require(__dirname+'/../../configs/main.js');
module.exports = async (req, res, next) => {
let threads = (await cache.get('catalog')) || [];
if (!threads || threads.length === 0) {
try {
const listedBoards = await Boards.getLocalListed();
threads = await Posts.getCatalog(listedBoards, false, overboardCatalogLimit);
cache.set('catalog', threads, 60);
} catch (err) {
return next(err);
}
}
res
.set('Cache-Control', 'public, max-age=60')
.render('overboardcatalog', {
threads,
});
}

49
package-lock.json generated

@ -1108,6 +1108,14 @@
"type-is": "~1.6.17"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
@ -3834,11 +3842,11 @@
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"ieee754": {
@ -4777,6 +4785,14 @@
"ms": "^2.1.1"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -4847,6 +4863,16 @@
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
"semver": {
@ -5999,6 +6025,16 @@
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
"rc": {
@ -7444,6 +7480,11 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug=="
},
"unix-crypt-td-js": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz",
"integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw=="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

@ -30,6 +30,7 @@
"gulp-uglify-es": "^2.0.0",
"highlight.js": "^10.2.0",
"i18n-iso-countries": "^6.0.0",
"iconv-lite": "^0.6.2",
"ioredis": "^4.14.1",
"ip6addr": "^0.2.3",
"mongodb": "^3.6.2",
@ -43,7 +44,8 @@
"semver": "^7.3.2",
"socket.io": "^2.3.0",
"socket.io-redis": "^5.4.0",
"socks-proxy-agent": "^5.0.0"
"socks-proxy-agent": "^5.0.0",
"unix-crypt-td-js": "^1.1.4"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",

@ -28,7 +28,9 @@ module.exports = async () => {
headers: {
'User-Agent':''
}
}).then(res => res.json()).catch(e => console.error);
})
.then(res => res.json())
.catch(e => {});
}));
for (let i = 0; i < rings.length; i++) {
const ring = rings[i];

@ -81,6 +81,9 @@ const express = require('express')
case 'google':
app.locals.googleRecaptchaSiteKey = captchaOptions.google.siteKey;
break;
case 'hcaptcha':
app.locals.hcaptchaSiteKey = captchaOptions.hcaptcha.siteKey;
break;
case 'grid':
app.locals.captchaGridSize = captchaOptions.grid.size;
break;

@ -30,6 +30,7 @@ block content
ul.mv-0
li: a(href='#make-a-board') How do I make my own board?
li: a(href='#staff-and-permissions') How do staff, users and permissions work?
li: a(href='#antispam') What do the board settings for antispam do?
.table-container.flex-center.mv-5
.anchor#whats-an-imageboard
table
@ -63,22 +64,27 @@ block content
p
| Names should be input like:
input(disabled='true' spellcheck='false' type='text' value='Name##Tripcode## Capcode')
| . Tripcode and capcode are optional components. Tripcodes and capcodes may not contain "##" and the whitespace before capcodes is significant.
| . Tripcode and capcode are optional components. Tripcodes and capcodes may not contain "#" and the whitespace before capcodes is significant.
p Valid examples:
ol.mv-0
li Name
li Name##tripcode
li Name## capcode
li Name##tripcode## capcode
li ##tripcode## capcode
li name
li #tripcode
li ##tripcode
li ## capcode
p The capcode can also be left blank to display just your role.
p From the examples, you can see that all components can be used in combination or independently. In a post number 4 would look like:
li name#tripcode
li name##tripcode
li name## capcode
li name#tripcode## capcode
li name##tripcode## capcode
li #tripcode## capcode
li ##tripcode## capcode
li ##
p The last example is considered a blank capcode and can be used as a shortcut to display your role.
p Each component can be used in combination or independently. In a post number 9 would look like:
-
const examplePost = {
"date" : new Date("2019-08-02T09:48:44.180Z"),
"name" : "Name",
"name" : "name",
"board" : "example",
"tripcode" : "!!X8NXmAS44=",
"capcode" : "##Board Owner capcode",
@ -97,13 +103,13 @@ block content
"postId" : 123
}
+post(examplePost)
p The name appears bold in the top left, followed by the tripcode in regular weight with a !! prefix, then the capcode in a different color, bold and with a ## prefix. The colours may vary between themes but are generally distinct from eachother
p The name appears bold in the top left, followed by the tripcode in regular weight with a !! prefix, then the capcode in a different color, bold and with a ## prefix. The colours may vary between themes but are generally distinct from each other
b Name
p The name is simply what name you want to be shown alongside your post. Other users can post with the same name so there is nothing preventing impersonation. This is not related to your username (for registered users).
b Tripcode
p A tripcode is a password of sorts, which users can provide in the tripcode component of their name. This tripcode is used in conjunction with a server-known secret to generate a unique* tripcode portion of the name. Long, unique tripcodes can be used as a form of identity. It is important that you keep tripcodes secret if you use them for some form of identity. A compromised tripcode can be used for impersonation and cannot be revoked in any way.
p A tripcode is a password of sorts, which users can provide in the tripcode component of their name. This tripcode is used in conjunction with a server-known secret to generate a unique* tripcode portion of the name. Long, unique tripcodes can be used as a form of identity. It is important that you keep tripcodes secret if you use them for some form of identity. A compromised tripcode can be used for impersonation and cannot be revoked in any way. Single # before tripcodes will use the traditional (what is now sometimes known as "insecure") tripcode algorithm shared by many imageboard softwares and websites. Double # before tripcodes will use a sha256 hash with server-side secret for a more secure, non-portable tripcode.
b Capcode
p A capcode is a component of the name field only available to authenticated users. This includes admins, global staff, board owners and board moderators. If there is no text after the ##, the role will be displayed alone. Leaving a space and putting custom text will be prefixed by the role name. This way, the role is always shown to prevent role impersonation.
@ -174,17 +180,29 @@ block content
span.mono inline monospace
tr
td
| ```language
| [code]language
br
| code block
| int main() {...}
br
| ```
| [/code]
td
span.code int main() {...}
tr
td
pre
| [code]aa
| ∧_∧
| ( ・ω・) Let's try that again.
| [&#x2F;code]
td
span.code code block
pre.aa
| ∧_∧
| ( ・ω・) Let's try that again.
tr
td(colspan=2)
| The "language" of code blocks is optional. Without it, automatic language detection is used.
| If the language is "plain", highlighting is disabled for the code block. Not all languages are supported, a subset of popular languages is used.
| If the language is "plain", highlighting is disabled for the code block. If "aa" is used, the font will be adjusted for Japanese Shift JIS art.
| Not all programming languages are supported, a subset of popular languages is used.
| If the language is not in the supported list, the code block will be rendered like "plain" with no highlighting.
| Languages supported: #{codeLanguages.join(', ')}
.table-container.flex-center.mv-5
@ -262,9 +280,32 @@ block content
li Board moderator: All below, move/merge, ban, delete-by-ip, sticky/sage/lock/cycle
li Regular user: Reports, and post spoiler/delete/unlink if the board has them enabled
| Administrators have the ability to assign a permission level directly to users through the global management page. Typically a user is level 4 (regular user), 1 (global staff) or 0 (administrator).
| Level 2 and 3 are usually only aplicable to users when on a board they are owner or moderator. However, level 2 or 3 can be assigned manually to create global board owners or global board moderators
| who have board owner or moderation permissions on all boards, but without access to the global moderation interfaces. If assigning global boad owners (level 2), they will have access to the board settings
| Level 2 and 3 are usually only applicable to users when on a board they are owner or moderator. However, level 2 or 3 can be assigned manually to create global board owners or global board moderators
| who have board owner or moderation permissions on all boards, but without access to the global moderation interfaces. If assigning global board owners (level 2), they will have access to the board settings
| and the ability to reassign board owners.
.table-container.flex-center.mv-5
.anchor#antispam
table
tr
th: a(href='#antispam') What do the board settings for antispam do?
tr
td
p Lock Mode: Choose to lock posting new threads or all posts.
p Captcha Mode: Choose to enforce captchas for posting threads or all posts.
p PPH Trigger Threshold: Trigger an action after a certain amount of PPH.
p PPH Trigger Action: The action to trigger.
p TPH Trigger Threshold: Trigger an action after a certain amount of TPH.
p TPH Trigger Action: The action to trigger.
p Trigger Reset Lock Mode: If a trigger threshold was reached, reset the lock mode to this at the end of the hour.
p Trigger Reset Captcha Mode: If a trigger threshold was reached, reset the captcha mode to this at the end of the hour.
p Early 404: When a new thread is posted, delete any existing threads with less than #{early404Replies} replies beyond the first 1/#{early404Fraction} of threads.
p Disable .onion file posting: Prevent users posting through a .onion hidden service posting images.
p Blocked Countries: Block country codes (based on geo Ip data) from posting.
p Filters: Newline separated list of words or phrases to match in posts. Checks name, message, filenames, subject, and filenames.
p Strict Filtering: More aggressively match filters, by normalising the input compared against the filters.
p Filter Mode: What to do when a post matches a filter.
p Filter Auto Ban Duration: How long to automatically ban for when filter mode is set to ban. Input the duration in time format described in the #[a(href='#moderation') moderation section].
.table-container.flex-center.mv-5
.anchor#contact
table

@ -1,12 +1,15 @@
case captchaType
when 'google'
div(class="g-recaptcha" data-sitekey=`${googleRecaptchaSiteKey}` data-theme="dark" data-size="compact" data-callback="recaptchaCallback")
div(class='g-recaptcha' data-sitekey=`${googleRecaptchaSiteKey}` data-theme='dark' data-size='compact' data-callback='recaptchaCallback')
noscript Please enable JavaScript to solve the captcha.
when 'hcaptcha'
div(class='h-captcha' data-sitekey=`${hcaptchaSiteKey}` data-theme='dark' data-size='compact' data-callback='recaptchaCallback')
noscript Please enable JavaScript to solve the captcha.
when 'text'
noscript.no-m-p
iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy')
.jsonly.captcha(style='display:none;')
input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='Captcha text' pattern=".{6}" required title='6 characters')
input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='Captcha text' pattern='.{6}' required title='6 characters')
when 'grid'
unless minimal
a.text-center(href='/faq.html#captcha') Instructions

@ -19,4 +19,6 @@ include ./favicon.pug
script(src=`/js/all.js?v=${commit}`)
if captchaType === 'google'
script(src="https://www.google.com/recaptcha/api.js" async defer)
script(src='https://www.google.com/recaptcha/api.js' async defer)
if captchaType === 'hcaptcha'
script(src='https://hcaptcha.com/1/api.js' async defer)

@ -1,11 +1,11 @@
unless minimal
nav.navbar
a.nav-item(href='/index.html') Home
//a.nav-item(href='/all.html') Overboard
a.nav-item(href='/boards.html' style=(enableWebring ? 'line-height: 1.5em' : null))
| Boards
if enableWebring
.rainbow +Webring
a.nav-item(href='/overboard.html') Overboard
a.nav-item(href='/account.html') Account
if board
a.nav-item(href=`/${board._id}/manage/reports.html`) Manage

@ -1,4 +1,4 @@
mixin catalogtile(board, post, index)
mixin catalogtile(post, index)
.catalog-tile(data-board=post.board
data-post-id=post.postId
data-user-id=post.userId
@ -9,9 +9,14 @@ mixin catalogtile(board, post, index)
data-date=post.date
data-replies=post.replyposts
data-bump=post.bumped)
- const postURL = `/${board._id}/${modview ? 'manage/' : ''}thread/${post.postId}.html#${post.postId}`
- const postURL = `/${post.board}/${modview ? 'manage/' : ''}thread/${post.postId}.html#${post.postId}`
.post-info
input.left.post-check(type='checkbox', name='checkedposts' value=post.postId)
if !index
div
| Thread from
a.no-decoration.post-subject(href=`/${post.board}/`) /#{post.board}/
else
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
@ -20,8 +25,9 @@ mixin catalogtile(board, post, index)
span(title='Replies') R: #{post.replyposts}
| /
span(title='Files') F: #{post.replyfiles}
| /
span(title='Page') P: #{Math.ceil(index/10)}
if index
| /
span(title='Page') P: #{Math.ceil(index/10)}
if post.files.length > 0
.post-file-src
a(href=postURL)
@ -31,7 +37,7 @@ mixin catalogtile(board, post, index)
else if file.hasThumb
img.catalog-thumb(src=`/file/thumb-${file.hash}${file.thumbextension}` width=file.geometry.thumbwidth height=file.geometry.thumbheight loading='lazy')
else if file.attachment
div.attachmentimg.catalog-thumb
div.attachmentimg.catalog-thumb(data-mimetype=file.mimetype)
else if file.mimetype.startsWith('audio')
div.audioimg.catalog-thumb
else

@ -1,16 +1,17 @@
include ./report.pug
mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
mixin post(post, truncate, manage=false, globalmanage=false, ban=false, overboard=false)
.anchor(id=post.postId)
div(class=`post-container ${post.thread || ban === true ? '' : 'op'}` data-board=post.board data-post-id=post.postId data-user-id=post.userId data-name=post.name data-tripcode=post.tripcode data-subject=post.subject)
- const postURL = `/${post.board}/${(modview || manage || globalmanage) ? 'manage/' : ''}thread/${post.thread || post.postId}.html`;
.post-info
span
label
if globalmanage
input.post-check(type='checkbox', name='globalcheckedposts' value=post._id)
else if !ban
input.post-check(type='checkbox', name='checkedposts' value=post.postId)
|
if !overboard
if globalmanage
input.post-check(type='checkbox', name='globalcheckedposts' value=post._id)
else if !ban
input.post-check(type='checkbox', name='checkedposts' value=post.postId)
|
if manage
- const ip = permLevel > ipHashPermLevel ? post.ip.single.slice(-10) : post.ip.raw;
a.bold(href=`${upLevel ? '../' : ''}recent.html?ip=${encodeURIComponent(ip)}`) [#{ip}]
@ -49,10 +50,10 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
a.noselect.no-decoration(href=`${postURL}#${post.postId}`) No.
span.post-quoters
a.no-decoration(href=`${postURL}#postform`) #{post.postId}
if !post.thread
|
span.noselect: a(href=`${postURL}#postform`) [Reply]
|
if !post.thread && (truncate || manage || globalmanage)
|
span.noselect: a(href=`${postURL}`) [Open]
select.jsonly.postmenu
option(value='single') Hide
if post.userId
@ -86,7 +87,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
else if file.hasThumb
img.file-thumb(src=`/file/thumb-${file.hash}${file.thumbextension}` height=file.geometry.thumbheight width=file.geometry.thumbwidth loading='lazy')
else if file.attachment
div.attachmentimg.file-thumb
div.attachmentimg.file-thumb(data-mimetype=file.mimetype)
else if type === 'audio'
div.audioimg.file-thumb
else

@ -5,9 +5,6 @@ block head
block content
h1.board-title Board List
h4.board-description
| or try the
a(href='/all.html') overboard
.flexcenter.mv-10
form.form-post(action='/boards.html' method='GET')
input(type='hidden' value=page)

@ -36,7 +36,7 @@ block content
else
.catalog
for thread, i in threads
+catalogtile(board, thread, i+1)
+catalogtile(thread, i+1)
hr(size=1)
if modview
+managenav('catalog')

@ -41,9 +41,6 @@ block content
a.noselect.no-decoration(href=`${postURL}#${post.postId}`) No.
span.post-quoters
a.no-decoration(href=`${postURL}#postform`) #{post.postId}
if !post.thread
|
span.noselect: a(href=`${postURL}#postform`) [Reply]
.post-data
pre.post-message
textarea.edit.fw(name='message' rows='15' placeholder='Message') #{post.nomarkup}

@ -202,7 +202,10 @@ block content
option(value='2', selected=board.settings.messageR9KMode === 2) Board Wide
.col.w900
.row
h4.mv-5 Antispam:
h4.mv-5 Antispam
| (
a(href='/faq.html#antispam') more info
| ):
.row
.label Lock Mode
select(name='lock_mode')

@ -6,8 +6,12 @@ block head
block content
.board-header
h1.board-title Overboard
h1.board-title Overboard Index
h4.board-description Recently bumped threads from all listed boards
|
| (
a(href='/catalog.html') Catalog View
| )
hr(size=1)
if threads.length === 0
p No posts.
@ -15,7 +19,7 @@ block content
for thread in threads
h4.no-m-p Thread from #[a(href=`/${thread.board}/index.html`) /#{thread.board}/]
.thread
+post(thread, true)
+post(thread, true, false, false, false, true)
for post in thread.replies
+post(post, true)
+post(post, true, false, false, false, true)
hr(size=1)

@ -0,0 +1,33 @@
extends ../layout.pug
include ../mixins/catalogtile.pug
block head
title Catalog
block content
.board-header.mb-5
h1.board-title Overboard Catalog
h4.board-description Recently bumped threads from all listed boards
|
| (
a(href='/overboard.html') Index View
| )
br
include ../includes/stickynav.pug
.wrapbar
.pages.jsonly
input#catalogfilter(type='text' placeholder='Filter')
select.ml-5.right#catalogsort
option(value="" disabled selected hidden) Sort by
option(value="bump") Bump order
option(value="date") Creation date
option(value="replies") Reply count
hr(size=1)
if threads.length === 0
p No posts.
else
.catalog
for thread, i in threads
+catalogtile(thread, false)
hr(size=1)
Loading…
Cancel
Save