Merge pull request #86 from fatchan/blockbypass-test

Merge blockbypass-test into master
merge-requests/208/head
Thomas Lynch 4 years ago committed by GitHub
commit 0a7bad8ea0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      README.md
  2. 30
      configs/main.js.example
  3. 24
      controllers/forms.js
  4. 6
      controllers/forms/boardsettings.js
  5. 13
      controllers/pages.js
  6. 2
      db/bans.js
  7. 35
      db/bypass.js
  8. 1
      db/files.js
  9. 1
      db/index.js
  10. 17
      db/posts.js
  11. 3
      gulp/res/js/captcha.js
  12. 7
      gulp/res/js/forms.js
  13. 2
      gulp/res/js/hide.js
  14. 7
      gulp/res/js/hover.js
  15. 12
      gulp/res/js/live.js
  16. 1
      gulp/res/js/localstorage.js
  17. 15
      gulp/res/js/modal.js
  18. 23
      gulp/res/js/post.js
  19. 86
      gulpfile.js
  20. 3
      helpers/captcha/captchagenerate.js
  21. 3
      helpers/captcha/captchaverify.js
  22. 62
      helpers/checks/blockbypass.js
  23. 5
      helpers/checks/dnsbl.js
  24. 6
      helpers/checks/spamcheck.js
  25. 8
      helpers/decodequeryip.js
  26. 6
      helpers/haship.js
  27. 46
      helpers/posting/markdown.js
  28. 12
      helpers/processip.js
  29. 4
      helpers/tasks.js
  30. 6
      migrations/index.js
  31. 8
      migrations/migration-0.0.1.js
  32. 10
      migrations/migration-0.0.2.js
  33. 6
      models/forms/actionhandler.js
  34. 2
      models/forms/appeal.js
  35. 6
      models/forms/banposter.js
  36. 24
      models/forms/blockbypass.js
  37. 5
      models/forms/deletepost.js
  38. 8
      models/forms/deletepostsfiles.js
  39. 13
      models/forms/makepost.js
  40. 2
      models/forms/reportpost.js
  41. 16
      models/pages/blockbypass.js
  42. 4
      models/pages/captcha.js
  43. 6
      models/pages/globalmanage/logs.js
  44. 2
      models/pages/globalmanage/recent.js
  45. 2
      models/pages/globalmanage/reports.js
  46. 1
      models/pages/index.js
  47. 9
      models/pages/manage/bans.js
  48. 31
      models/pages/manage/board.js
  49. 2
      models/pages/manage/index.js
  50. 26
      models/pages/manage/thread.js
  51. 1
      package.json
  52. 8
      schedules/index.js
  53. 27
      schedules/prune.js
  54. 3
      server.js
  55. 20
      views/includes/actionfooter_manage.pug
  56. 12
      views/includes/postform.pug
  57. 3
      views/mixins/ban.pug
  58. 16
      views/mixins/managenav.pug
  59. 10
      views/mixins/modal.pug
  60. 11
      views/mixins/post.pug
  61. 4
      views/mixins/report.pug
  62. 4
      views/pages/account.pug
  63. 30
      views/pages/board.pug
  64. 15
      views/pages/bypass.pug
  65. 13
      views/pages/globalmanagelogs.pug
  66. 2
      views/pages/globalmanagerecent.pug
  67. 68
      views/pages/thread.pug

@ -25,9 +25,9 @@ Demo site running at https://fatpeople.lol
## Setup
Please note:
##### 🚨 The software is still in development. If running in production, you may have to deal with breaking updates e.g. database schema changes. Automated systems to handle this will be made in future when things are more set in stone. 🚨
#### 🚨 The software is not production-ready. There may be bugs and WILL be breaking changes. If you insist on running your own instance, always ensure you have up-to-date configs and db schema after pulling as these will be common breaking changes until a stable version is reached. 🚨
- these instructions are not step-by-step or complete
- you should be able to read, be comfortable with a command line and have problem solving skills (aka search engine)
- you should be able to read, be comfortable with a command line and have problem solving skills
##### Requirements
- Linux (debian used in this example)
@ -76,11 +76,11 @@ geoip_city /usr/share/GeoIP/GeoIPCity.dat;
5. Clone ths repo, browse to the folder and set some things up
```bash
# copy example config and edit, some comments included
# copy example config file and edit it
$ cp configs/main.js.example configs/main.js && editor configs/main.js
# install dependencies and run build tasks
$ npm install
$ npm install
$ npm run-script setup
# setup the database and folder structure, creates admin account admin:changeme
@ -90,12 +90,13 @@ $ gulp reset
# start all the backend processes
$ npm run-script start
# some commands you may need to use in future/find helpful
# pm2 is a process manager for nodejs, gulp is used for various jobs like minifying and compiling scripts.
# pm2 is a process manager for nodejs
$ pm2 list #list running pm2 processes
$ pm2 logs #see logs
$ pm2 reload all #reload all backend processes
# gulp is used for various jobs like minifying and compiling scripts
$ gulp --tasks #list available gulp tasks
$ gulp #run default gulp task
```

@ -39,7 +39,8 @@ module.exports = {
min: 3,
max: 5
},
paintAmount: 2
paintAmount: 2,
fontPaths: [], //optional list of file paths to fonts for captchas
},
/* dnsbl, will add a small delay for uncached requests. You could also install some
@ -50,6 +51,29 @@ module.exports = {
cacheTime: 3600 //in seconds, idk whats a good value
},
//block bypasses
blockBypass: {
enabled: false,
expireAfterUses: 40, //however many (attempted) posts per block bypass captcha
expireAfterTime: 86400000, //expiry in ms regardless if the limit was reached, default 1 day
bypassDnsbl: false,
},
/* 0=not hashed
1=hash for non-global staff, visible for global staff and ban pages
2=hash for everything
its not recommended to change this setting after initial setup */
ipHashMode: 1,
/* delete files immediately rather than pruning later. usually disabled to prevent re-thumbnailing and processing commonly
uploaded files, but deleting immediately is better if you are concerned about "deleted" content not being immediately removed */
pruneImmediately: false,
rateLimitCost: { //Cost out of 100 per minute e.g. cost of 25 means 4 per minute. Cost is separate for each.
captcha: 10,
boardSettings: 30,
},
//cache templates in memory. disable only if editing templates and doing dev work
cacheTemplates: true,
@ -244,6 +268,6 @@ module.exports = {
audio: true,
other: false
}
}
},
}
};

@ -15,6 +15,7 @@ const express = require('express')
, csrf = require(__dirname+'/../helpers/checks/csrfmiddleware.js')
, sessionRefresh = require(__dirname+'/../helpers/sessionrefresh.js')
, dnsblCheck = require(__dirname+'/../helpers/checks/dnsbl.js')
, blockBypassCheck = require(__dirname+'/../helpers/checks/blockbypass.js')
, dynamicResponse = require(__dirname+'/../helpers/dynamic.js')
, uploadLimitFunction = (req, res, next) => {
return dynamicResponse(req, res, 413, 'message', {
@ -71,16 +72,21 @@ const express = require('express')
, globalSettingsController = require(__dirname+'/forms/globalsettings.js')
, createBoardController = require(__dirname+'/forms/create.js')
, makePostController = require(__dirname+'/forms/makepost.js')
, newcaptcha = require(__dirname+'/../models/forms/newcaptcha.js')
, newCaptcha = require(__dirname+'/../models/forms/newcaptcha.js')
, blockBypass = require(__dirname+'/../models/forms/blockbypass.js')
//make new post
router.post('/board/:board/post', dnsblCheck, sessionRefresh, Boards.exists, calcPerms, banCheck, postFiles, paramConverter, verifyCaptcha, numFiles, makePostController);
//router.post('/board/:board/modpost', dnsblCheck, sessionRefresh, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), postFiles, paramConverter, csrf, numFiles, makePostController); //mod post has token instead of captcha
router.post('/board/:board/post', sessionRefresh, Boards.exists, calcPerms, banCheck, postFiles,
paramConverter, verifyCaptcha, numFiles, blockBypassCheck, dnsblCheck, makePostController);
router.post('/board/:board/modpost', sessionRefresh, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), postFiles,
paramConverter, csrf, numFiles, blockBypassCheck, dnsblCheck, makePostController); //mod post has token instead of captcha
//post actions
router.post('/board/:board/actions', sessionRefresh, Boards.exists, calcPerms, banCheck, paramConverter, verifyCaptcha, actionController); //public, with captcha
router.post('/board/:board/modactions', sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), paramConverter, actionController); //board manage page
router.post('/global/actions', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(1), paramConverter, globalActionController); //global manage page
//appeal ban
router.post('/appeal', sessionRefresh, paramConverter, verifyCaptcha, appealController);
//board management forms
router.post('/board/:board/transfer', sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), paramConverter, transferController);
@ -97,19 +103,17 @@ router.post('/global/deletenews', sessionRefresh, csrf, calcPerms, isLoggedIn, h
router.post('/global/editaccounts', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), paramConverter, editAccountsController); //account editing
router.post('/global/settings', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), paramConverter, globalSettingsController); //global settings
//create board
router.post('/create', sessionRefresh, isLoggedIn, verifyCaptcha, calcPerms, hasPerms(4), createBoardController);
//accounts
router.post('/login', loginController);
router.post('/register', verifyCaptcha, registerController);
router.post('/changepassword', verifyCaptcha, changePasswordController);
//appeal ban
router.post('/appeal', sessionRefresh, paramConverter, verifyCaptcha, appealController);
//create board
router.post('/create', sessionRefresh, isLoggedIn, verifyCaptcha, calcPerms, hasPerms(4), createBoardController);
//removes captcha cookie, for refreshing for noscript users
router.post('/newcaptcha', newcaptcha);
router.post('/newcaptcha', newCaptcha);
//solve captcha for block bypass
router.post('/blockbypass', verifyCaptcha, blockBypass);
module.exports = router;

@ -3,7 +3,7 @@
const changeBoardSettings = require(__dirname+'/../../models/forms/changeboardsettings.js')
, { themes, codeThemes } = require(__dirname+'/../../helpers/themes.js')
, { Ratelimits } = require(__dirname+'/../../db/')
, { globalLimits } = require(__dirname+'/../../configs/main.js');
, { globalLimits, rateLimitCost } = require(__dirname+'/../../configs/main.js');
module.exports = async (req, res, next) => {
@ -126,8 +126,8 @@ 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', 50); //2 changes a minute
const ratelimitIp = await Ratelimits.incrmentQuota(res.locals.ip.hash, 'settings', 50);
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);
if (ratelimitBoard > 100 || ratelimitIp > 100) {
return res.status(429).render('message', {
'title': 'Ratelimited',

@ -12,10 +12,10 @@ const express = require('express')
, sessionRefresh = require(__dirname+'/../helpers/sessionrefresh.js')
, csrf = require(__dirname+'/../helpers/checks/csrfmiddleware.js')
//page models
, { manageReports, manageBanners, manageSettings, manageBans } = require(__dirname+'/../models/pages/manage/')
, { manageReports, manageBanners, manageSettings, manageBans, manageBoard, manageThread } = require(__dirname+'/../models/pages/manage/')
, { globalManageSettings, globalManageReports, globalManageBans,
globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/')
, { changePassword, home, register, login, logout, create,
, { changePassword, blockBypass, home, register, login, logout, create,
board, catalog, banners, randombanner, news, captchaPage,
captcha, thread, modlog, modloglist, account, boardlist } = require(__dirname+'/../models/pages/');
@ -43,11 +43,9 @@ router.get('/:board/manage/reports.html', sessionRefresh, isLoggedIn, Boards.exi
router.get('/:board/manage/bans.html', sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageBans);
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);
/*
todo: dynamic mod pages with no captcha required for mod forms
router.get('/:board/manage/:page(1[0-9]{0,}|[2-9]{1,}|index).html', sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(2), csrf, manageBoard);
router.get('/:board/manage/thread/:id(\\d+).html', sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(2), csrf, manageThread);
*/
// if (mod view enabled) {
router.get('/:board/manage/:page(1[0-9]{0,}|[2-9]{1,}|index).html', sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(3), csrf, manageBoard);
router.get('/:board/manage/thread/:id(\\d+).html', sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(3), csrf, Posts.exists, manageThread);
//global manage pages
router.get('/globalmanage/reports.html', sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageReports);
@ -61,6 +59,7 @@ router.get('/globalmanage/settings.html', sessionRefresh, isLoggedIn, calcPerms,
//captcha
router.get('/captcha', captcha); //get captcha image and cookie
router.get('/captcha.html', captchaPage); //iframed for noscript users
router.get('/bypass.html', blockBypass); //block bypass page
//accounts
router.get('/account.html', sessionRefresh, isLoggedIn, account); //page showing boards you are mod/owner of, links to password rese, logout, etc

@ -12,7 +12,7 @@ module.exports = {
let ipQuery;
if (typeof ip === 'object') { //object with hash and ranges in bancheck
ipQuery = {
'$in': Object.values(ip)
'$in': Object.values(ip) //gets values of ip object for single and range bans in 1 query
}
} else {
ipQuery = ip;

@ -0,0 +1,35 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, { blockBypass } = require(__dirname+'/../configs/main.js')
, db = Mongo.client.db('jschan').collection('bypass');
module.exports = {
db,
checkBypass: (id) => {
return db.findOneAndUpdate({
'_id': id,
'uses': {
'$lte': blockBypass.expireAfterUses
}
}, {
'$inc': {
'uses': 1,
}
}).then(r => r.value);
},
getBypass: () => {
return db.insertOne({
'uses': 0,
'expireAt': new Date(Date.now() + blockBypass.expireAfterTime)
});
},
deleteAll: () => {
return db.deleteMany({});
},
}

@ -10,7 +10,6 @@ module.exports = {
db,
increment: (file) => {
file.inced = true;
return db.updateOne({
'_id': file.filename
}, {

@ -13,5 +13,6 @@ module.exports = {
News: require(__dirname+'/news.js'),
Ratelimits: require(__dirname+'/ratelimits.js'),
Modlogs: require(__dirname+'/modlogs.js'),
Bypass: require(__dirname+'/bypass.js'),
}

@ -27,7 +27,7 @@ module.exports = {
//global recent posts for recent section of global manage page
const query = {};
if (ip !== null) {
query['ip.hash'] = ip;
query['ip.single'] = ip;
}
return db.find(query).sort({
'_id': -1
@ -140,7 +140,13 @@ module.exports = {
}
},
'bumped': {
'$max': '$date'
'$max': {
'$cond': [
{ '$ne': [ '$email', 'sage' ] },
'$date',
0 //still need to improve this to ignore bumplocked threads
]
}
}
}
}
@ -354,7 +360,7 @@ module.exports = {
//insert the post itself
const postMongoId = await db.insertOne(data).then(result => result.insertedId); //_id of post
await Stats.updateOne(board._id, data.ip.hash, data.thread == null);
await Stats.updateOne(board._id, data.ip.single, data.thread == null);
//add backlinks to the posts this post quotes
if (data.thread && data.quotes.length > 0) {
@ -397,7 +403,7 @@ module.exports = {
}
if (ip !== null) {
query['$or'] = [
{ 'ip.hash': ip },
{ 'ip.single': ip },
{ 'globalreports.ip': ip }
];
}
@ -445,6 +451,9 @@ module.exports = {
'$match': {
'board': {
'$in': boards
},
'email': {
'$ne': 'sage'
}
}
}, {

@ -34,6 +34,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
const captchaDiv = this.previousSibling;
const captchaImg = document.createElement('img');
const field = this;
field.placeholder = 'loading';
captchaImg.src = '/captcha';
captchaImg.onload = function() {
field.placeholder = 'double click image to refresh';
@ -44,7 +45,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
};
for (let i = 0; i < captchaFields.length; i++) {
captchaFields[i].placeholder = 'click to load captcha';
captchaFields[i].placeholder = 'focus to load captcha';
captchaFields[i].addEventListener('focus', loadCaptcha, { once: true });
}

@ -43,6 +43,7 @@ class formHandler {
this.fileInput = form.querySelector('input[type="file"]');
this.files = [];
if (this.fileInput) {
this.fileRequired = this.fileInput.required;
this.fileLabel = this.fileInput.previousSibling;
this.multipleFiles = this.fileLabel.parentNode.previousSibling.firstChild.textContent.endsWith('s');
this.fileLabelText = this.fileLabel.childNodes[0];
@ -178,6 +179,9 @@ class formHandler {
}
addFile(file) {
if (this.fileRequired) { //prevent drag+drop issues by removing required
this.fileInput.removeAttribute('required');
}
this.files.push(file);
}
@ -197,6 +201,9 @@ class formHandler {
clearFiles() {
this.files = []; //empty file list
this.fileInput.value = null; //remove the files for real
if (this.fileRequired) { //reset to required if clearing files
this.fileInput.setAttribute('required', true)
}
this.updateFilesText();
}

@ -153,7 +153,7 @@ const heightlimitCss = `img, video { max-height: unset; }`;
const crispCss = `img { image-rendering: crisp-edges; }`;
//make classes with css
//new CssToggle('hidestubs-setting', 'hidestubs', false, hideStubsCss);
new CssToggle('hiderecursive-setting', 'hiderecursive', false, hideRecursiveCss);
new CssToggle('hiderecursive-setting', 'hiderecursive', true, hideRecursiveCss);
new CssToggle('heightlimit-setting', 'heightlimit', false, heightlimitCss);
new CssToggle('crispimages-setting', 'crispimages', false, crispCss);
new CssToggle('hideimages-setting', 'hideimages', false, hideImagesCss);

@ -62,7 +62,12 @@ window.addEventListener('DOMContentLoaded', (event) => {
const toggleHighlightPost = async function (e) {
hovering = e.type === 'mouseover';
const jsonPath = this.pathname.replace(/html$/, 'json');
let jsonParts = this.pathname.replace(/\.html$/, '.json').split('/');
let jsonPath;
if (isModView) {
jsonParts.splice(2,1); //remove manage from json url
}
jsonPath = jsonParts.join('/');
if (!this.hash) {
return; //non-post number board quote
}

@ -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 });
const postHtml = post({ post: postData, modview:isModView });
//add it to the end of the thread
thread.insertAdjacentHTML('beforeend', postHtml);
for (let j = 0; j < postData.quotes.length; j++) {
@ -83,7 +83,12 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
window.dispatchEvent(newPostEvent);
}
const jsonPath = window.location.pathname.replace(/\.html$/, '.json');
let jsonParts = window.location.pathname.replace(/\.html$/, '.json').split('/');
let jsonPath;
if (isModView) {
jsonParts.splice(2,1); //remove manage from json url
}
jsonPath = jsonParts.join('/');
const fetchNewPosts = async () => {
console.log('fetching posts from api');
updateLive('Fetching posts...', 'yellow');
@ -127,7 +132,7 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
if (supportsWebSockets) {
updateButton.style.display = 'none';
const roomParts = window.location.pathname.replace(/\.html$/, '').split('/');
const room = `${roomParts[1]}-${roomParts[3]}`;
const room = `${roomParts[1]}-${roomParts[roomParts.length-1]}`;
socket = io({
transports: ['websocket'],
reconnectionAttempts: 5
@ -178,7 +183,6 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
socket.close();
supportsWebSockets = false;
enableLive();
});
socket.on('newPost', newPost);
} else {

@ -1,5 +1,6 @@
const isCatalog = window.location.pathname.endsWith('catalog.html');
const isThread = /\/\w+\/thread\/\d+.html/.test(window.location.pathname);
const isModView = /\/\w+\/manage\/(thread\/)?(index|\d+).html/.test(window.location.pathname);
function setLocalStorage(key, value) {
try {

@ -4,9 +4,9 @@ var pug_has_own_property=Object.prototype.hasOwnProperty;
var pug_match_html=/["&<>]/;
function pug_style(r){if(!r)return"";if("object"==typeof r){var t="";for(var e in r)pug_has_own_property.call(r,e)&&(t=t+e+":"+r[e]+";");return t}return r+""}function modal(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;var locals_for_with = (locals || {});(function (modal) {pug_mixins["modal"] = pug_interp = function(data){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cdiv" + (" class=\"modal-bg\""+pug_attr("style", pug_style(data.hidden?'display:none':''), true, false)) + "\u003E\u003C\u002Fdiv\u003E\u003Cdiv" + (" class=\"modal\""+pug_attr("style", pug_style(data.hidden?'display:none':''), true, false)) + "\u003E\u003Cdiv class=\"row\"\u003E\u003Cp class=\"bold\"\u003E" + (pug_escape(null == (pug_interp = data.title) ? "" : pug_interp)) + "\u003C\u002Fp\u003E\u003Ca class=\"close postform-style\" id=\"modalclose\"\u003EX\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E";
pug_html = pug_html + "\u003Cdiv" + (" class=\"modal-bg\""+pug_attr("style", pug_style(data.hidden?'display:none':''), true, false)) + "\u003E\u003C\u002Fdiv\u003E\u003Cdiv" + (" class=\"modal\""+pug_attr("style", pug_style(data.hidden?'display:none':''), true, false)) + "\u003E\u003Cdiv class=\"row\"\u003E\u003Cp class=\"bold\"\u003E" + (pug_escape(null == (pug_interp = data.title) ? "" : pug_interp)) + "\u003C\u002Fp\u003E\u003Ca class=\"close postform-style\" id=\"modalclose\"\u003EX\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E";
if (data.message || data.messages || data.error || data.errors) {
pug_html = pug_html + "\u003Cul class=\"nomarks\"\u003E";
pug_html = pug_html + "\u003Cdiv class=\"row\"\u003E\u003Cul class=\"nomarks\"\u003E";
if (data.message) {
pug_html = pug_html + "\u003Cli\u003E" + (pug_escape(null == (pug_interp = data.message) ? "" : pug_interp)) + "\u003C\u002Fli\u003E";
}
@ -53,11 +53,14 @@ pug_html = pug_html + "\u003Cli\u003E" + (pug_escape(null == (pug_interp = error
}).call(this);
}
pug_html = pug_html + "\u003C\u002Ful\u003E";
pug_html = pug_html + "\u003C\u002Ful\u003E\u003C\u002Fdiv\u003E";
if (data.link) {
pug_html = pug_html + "\u003Cdiv class=\"row\"\u003E\u003Ca" + (pug_attr("href", data.link, true, false)+" target=\"_blank\"") + "\u003E" + (pug_escape(null == (pug_interp = data.link) ? "" : pug_interp)) + "\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E";
}
}
else
if (data.settings) {
pug_html = pug_html + "\u003Cdiv class=\"form-wrapper flexleft mt-10\"\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ETheme\u003C\u002Fdiv\u003E\u003Cselect id=\"theme-setting\"\u003E\u003Coption value=\"default\"\u003Edefault\u003C\u002Foption\u003E";
pug_html = pug_html + "\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"form-wrapper flexleft mt-10\"\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ETheme\u003C\u002Fdiv\u003E\u003Cselect id=\"theme-setting\"\u003E\u003Coption value=\"default\"\u003Edefault\u003C\u002Foption\u003E";
// iterate data.settings.themes
;(function(){
var $$obj = data.settings.themes;
@ -95,8 +98,8 @@ pug_html = pug_html + "\u003Coption" + (pug_attr("value", theme, true, false)) +
}
}).call(this);
pug_html = pug_html + "\u003C\u002Fselect\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ELive posts\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"live-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ENotifications\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"notification-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EScroll to new posts\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"scroll-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ELocal time\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"localtime-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003E24h time\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"24hour-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EShow relative time\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"relative-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EHide Thumbnails\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"hideimages-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ERecursive Post Hide\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"hiderecursive-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EVideo\u002FAudio Volume\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"volume-setting\" type=\"range\" min=\"0\" max=\"100\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ELoop audio\u002Fvideo\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"loop-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EUnlimit expand height\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"heightlimit-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ECrisp image rendering\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"crispimages-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EPost Password\u003C\u002Fdiv\u003E\u003Cinput id=\"postpassword-setting\" type=\"password\" name=\"postpassword\"\u002F\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ECustom CSS\u003C\u002Fdiv\u003E\u003Ctextarea id=\"customcss-setting\"\u003E\u003C\u002Ftextarea\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u003E";
pug_html = pug_html + "\u003C\u002Fselect\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ELive posts\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"live-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ENotifications\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"notification-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EScroll to new posts\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"scroll-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ELocal time\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"localtime-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003E24h time\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"24hour-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EShow relative time\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"relative-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EHide Thumbnails\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"hideimages-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ERecursive Post Hide\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"hiderecursive-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EVideo\u002FAudio Volume\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"volume-setting\" type=\"range\" min=\"0\" max=\"100\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ELoop audio\u002Fvideo\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"loop-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EUnlimit expand height\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"heightlimit-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ECrisp image rendering\u003C\u002Fdiv\u003E\u003Clabel class=\"postform-style ph-5\"\u003E\u003Cinput id=\"crispimages-setting\" type=\"checkbox\"\u002F\u003E\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003EPost Password\u003C\u002Fdiv\u003E\u003Cinput id=\"postpassword-setting\" type=\"password\" name=\"postpassword\"\u002F\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row\"\u003E\u003Cdiv class=\"label\"\u003ECustom CSS\u003C\u002Fdiv\u003E\u003Ctextarea id=\"customcss-setting\"\u003E\u003C\u002Ftextarea\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u003E";
}
pug_html = pug_html + "\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u003E";
pug_html = pug_html + "\u003C\u002Fdiv\u003E";
};
pug_mixins["modal"](modal);}.call(this,"modal" in locals_for_with?locals_for_with.modal:typeof modal!=="undefined"?modal:undefined));;return pug_html;}

@ -5,12 +5,12 @@ function pug_classes_object(r){var a="",n="";for(var o in r)o&&r[o]&&pug_has_own
function pug_escape(e){var a=""+e,t=pug_match_html.exec(a);if(!t)return e;var r,c,n,s="";for(r=t.index,c=0;r<a.length;r++){switch(a.charCodeAt(r)){case 34:n="&quot;";break;case 38:n="&amp;";break;case 60:n="&lt;";break;case 62:n="&gt;";break;default:continue}c!==r&&(s+=a.substring(c,r)),c=r+1,s+=n}return c!==r?s+a.substring(c,r):s}
var pug_has_own_property=Object.prototype.hasOwnProperty;
var pug_match_html=/["&<>]/;
function pug_style(r){if(!r)return"";if("object"==typeof r){var t="";for(var e in r)pug_has_own_property.call(r,e)&&(t=t+e+":"+r[e]+";");return t}return r+""}function post(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;var locals_for_with = (locals || {});(function (Date, encodeURIComponent, ipHashSub, post) {pug_mixins["report"] = pug_interp = function(r, globalmanage=false){
function pug_style(r){if(!r)return"";if("object"==typeof r){var t="";for(var e in r)pug_has_own_property.call(r,e)&&(t=t+e+":"+r[e]+";");return t}return r+""}function post(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;var locals_for_with = (locals || {});(function (Date, RegExp, encodeURIComponent, ipHashMode, modview, permLevel, post) {pug_mixins["report"] = pug_interp = function(r, globalmanage=false){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cdiv class=\"reports post-container\"\u003E\u003Cinput" + (" class=\"post-check\""+" type=\"checkbox\" name=\"checkedreports\""+pug_attr("value", r.id, true, false)) + "\u002F\u003E ";
if (globalmanage) {
ipHashSub = r.ip.slice(-10);
pug_html = pug_html + "\u003Ca" + (" class=\"bold\""+pug_attr("href", `?ip=${encodeURIComponent(ipHashSub)}`, true, false)) + "\u003E" + (pug_escape(null == (pug_interp = ipHashSub) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
const ip = ipHashMode === 2 || (ipHashMode === 1 && permLevel > 1) ? r.ip.slice(-10) : r.ip;
pug_html = pug_html + "\u003Ca" + (" class=\"bold\""+pug_attr("href", `?ip=${encodeURIComponent(ip)}`, true, false)) + "\u003E" + (pug_escape(null == (pug_interp = ip) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
}
const reportDate = new Date(r.date);
pug_html = pug_html + "\u003Ctime" + (" class=\"reltime\""+pug_attr("datetime", reportDate.toISOString(), true, false)) + "\u003E" + (pug_escape(null == (pug_interp = reportDate.toLocaleString(undefined, { hour12:false })) ? "" : pug_interp)) + "\u003C\u002Ftime\u003E | Reason: " + (pug_escape(null == (pug_interp = r.reason) ? "" : pug_interp)) + "\u003C\u002Fdiv\u003E";
@ -18,12 +18,12 @@ pug_html = pug_html + "\u003Ctime" + (" class=\"reltime\""+pug_attr("datetime",
pug_mixins["post"] = pug_interp = function(post, truncate, manage=false, globalmanage=false, ban=false){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cdiv" + (" class=\"anchor\""+pug_attr("id", post.postId, true, false)) + "\u003E\u003C\u002Fdiv\u003E\u003Cdiv" + (pug_attr("class", pug_classes([`post-container ${post.thread || ban === true ? '' : 'op'}`], [true]), false, false)+pug_attr("data-board", post.board, true, false)+pug_attr("data-post-id", post.postId, true, false)+pug_attr("data-user-id", post.userId, true, false)) + "\u003E";
const postURL = `/${post.board}/thread/${post.thread || post.postId}.html`;
const postURL = `/${post.board}/${modview ? 'manage/' : ''}thread/${post.thread || post.postId}.html`;
pug_html = pug_html + "\u003Cdiv class=\"post-info\"\u003E\u003Cspan class=\"noselect\"\u003E\u003Clabel\u003E";
if (globalmanage) {
pug_html = pug_html + "\u003Cinput" + (" class=\"post-check\""+" type=\"checkbox\" name=\"globalcheckedposts\""+pug_attr("value", post._id, true, false)) + "\u002F\u003E ";
ipHashSub = post.ip.hash.slice(-10);
pug_html = pug_html + "\u003Ca" + (" class=\"bold\""+pug_attr("href", `?ip=${encodeURIComponent(ipHashSub)}`, true, false)) + "\u003E" + (pug_escape(null == (pug_interp = ipHashSub) ? "" : pug_interp)) + "\u003C\u002Fa\u003E";
const ip = ipHashMode === 2 ? post.ip.single.slice(-10) : post.ip.single;
pug_html = pug_html + "\u003Ca" + (" class=\"bold\""+pug_attr("href", `?ip=${encodeURIComponent(ip)}`, true, false)) + "\u003E" + (pug_escape(null == (pug_interp = ip) ? "" : pug_interp)) + "\u003C\u002Fa\u003E";
}
else
if (!ban) {
@ -159,6 +159,7 @@ pug_html = pug_html + "\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u
pug_html = pug_html + "\u003C\u002Fdiv\u003E";
}
if (post.message && modview) { post.message = post.message.replace(new RegExp(`<a class="quote" href="/${post.board}`, 'g'), `<a class="quote" href="/${post.board}/manage`); } //quick & dirty solution to a bigger problem/design issue
let truncatedMessage = post.message;
if (post.message) {
if (truncate) {
@ -199,14 +200,14 @@ pug_html = pug_html + "\u003Cdiv class=\"replies mt-5 ml-5\"\u003EReplies: ";
if ('number' == typeof $$obj.length) {
for (var pug_index1 = 0, $$l = $$obj.length; pug_index1 < $$l; pug_index1++) {
var backlink = $$obj[pug_index1];
pug_html = pug_html + "\u003Ca" + (" class=\"quote\""+pug_attr("href", `/${post.board}/thread/${post.thread || post.postId}.html#${backlink.postId}`, true, false)) + "\u003E&gt;&gt;" + (pug_escape(null == (pug_interp = backlink.postId) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
pug_html = pug_html + "\u003Ca" + (" class=\"quote\""+pug_attr("href", `${postURL}#${backlink.postId}`, true, false)) + "\u003E&gt;&gt;" + (pug_escape(null == (pug_interp = backlink.postId) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
}
} else {
var $$l = 0;
for (var pug_index1 in $$obj) {
$$l++;
var backlink = $$obj[pug_index1];
pug_html = pug_html + "\u003Ca" + (" class=\"quote\""+pug_attr("href", `/${post.board}/thread/${post.thread || post.postId}.html#${backlink.postId}`, true, false)) + "\u003E&gt;&gt;" + (pug_escape(null == (pug_interp = backlink.postId) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
pug_html = pug_html + "\u003Ca" + (" class=\"quote\""+pug_attr("href", `${postURL}#${backlink.postId}`, true, false)) + "\u003E&gt;&gt;" + (pug_escape(null == (pug_interp = backlink.postId) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
}
}
}).call(this);
@ -227,14 +228,14 @@ pug_html = pug_html + "\u003Cdiv class=\"replies mt-5 ml-5\"\u003EReplies: ";
if ('number' == typeof $$obj.length) {
for (var pug_index2 = 0, $$l = $$obj.length; pug_index2 < $$l; pug_index2++) {
var backlink = $$obj[pug_index2];
pug_html = pug_html + "\u003Ca" + (" class=\"quote\""+pug_attr("href", `/${post.board}/thread/${post.thread || post.postId}.html#${backlink.postId}`, true, false)) + "\u003E&gt;&gt;" + (pug_escape(null == (pug_interp = backlink.postId) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
pug_html = pug_html + "\u003Ca" + (" class=\"quote\""+pug_attr("href", `${postURL}#${backlink.postId}`, true, false)) + "\u003E&gt;&gt;" + (pug_escape(null == (pug_interp = backlink.postId) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
}
} else {
var $$l = 0;
for (var pug_index2 in $$obj) {
$$l++;
var backlink = $$obj[pug_index2];
pug_html = pug_html + "\u003Ca" + (" class=\"quote\""+pug_attr("href", `/${post.board}/thread/${post.thread || post.postId}.html#${backlink.postId}`, true, false)) + "\u003E&gt;&gt;" + (pug_escape(null == (pug_interp = backlink.postId) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
pug_html = pug_html + "\u003Ca" + (" class=\"quote\""+pug_attr("href", `${postURL}#${backlink.postId}`, true, false)) + "\u003E&gt;&gt;" + (pug_escape(null == (pug_interp = backlink.postId) ? "" : pug_interp)) + "\u003C\u002Fa\u003E ";
}
}
}).call(this);
@ -283,4 +284,4 @@ pug_mixins["report"](r, true);
}
};
pug_mixins["post"](post);}.call(this,"Date" in locals_for_with?locals_for_with.Date:typeof Date!=="undefined"?Date:undefined,"encodeURIComponent" in locals_for_with?locals_for_with.encodeURIComponent:typeof encodeURIComponent!=="undefined"?encodeURIComponent:undefined,"ipHashSub" in locals_for_with?locals_for_with.ipHashSub:typeof ipHashSub!=="undefined"?ipHashSub:undefined,"post" in locals_for_with?locals_for_with.post:typeof post!=="undefined"?post:undefined));;return pug_html;}
pug_mixins["post"](post);}.call(this,"Date" in locals_for_with?locals_for_with.Date:typeof Date!=="undefined"?Date:undefined,"RegExp" in locals_for_with?locals_for_with.RegExp:typeof RegExp!=="undefined"?RegExp:undefined,"encodeURIComponent" in locals_for_with?locals_for_with.encodeURIComponent:typeof encodeURIComponent!=="undefined"?encodeURIComponent:undefined,"ipHashMode" in locals_for_with?locals_for_with.ipHashMode:typeof ipHashMode!=="undefined"?ipHashMode:undefined,"modview" in locals_for_with?locals_for_with.modview:typeof modview!=="undefined"?modview:undefined,"permLevel" in locals_for_with?locals_for_with.permLevel:typeof permLevel!=="undefined"?permLevel:undefined,"post" in locals_for_with?locals_for_with.post:typeof post!=="undefined"?post:undefined));;return pug_html;}

@ -12,6 +12,7 @@ const gulp = require('gulp')
, del = require('del')
, pug = require('pug')
, gulppug = require('gulp-pug')
, migrateVersion = require(__dirname+'/package.json').migrateVersion
, paths = {
styles: {
src: 'gulp/res/css/**/*.css',
@ -34,7 +35,7 @@ const gulp = require('gulp')
async function wipe() {
const Mongo = require(__dirname+'/db/db.js')
, cache = require(__dirname+'/redis.js')
const Redis = require(__dirname+'/redis.js')
await Mongo.connect();
const db = Mongo.client.db('jschan');
@ -50,12 +51,13 @@ async function wipe() {
await db.createCollection('poststats');
await db.createCollection('ratelimit');
await db.createCollection('webring');
await db.createCollection('bypass');
const { Webring, Boards, Posts, Captchas, Ratelimits, Accounts, Files, Stats, Modlogs, Bans } = require(__dirname+'/db/');
const { Webring, Boards, Posts, Captchas, Ratelimits, Accounts, Files, Stats, Modlogs, Bans, Bypass } = require(__dirname+'/db/');
//wipe db shit
await Promise.all([
cache.deletePattern('*'),
Redis.deletePattern('*'),
Captchas.deleteAll(),
Ratelimits.deleteAll(),
Accounts.deleteAll(),
@ -65,7 +67,8 @@ async function wipe() {
Bans.deleteAll(),
Files.deleteAll(),
Stats.deleteAll(),
Modlogs.deleteAll()
Modlogs.deleteAll(),
Bypass.deleteAll(),
]);
//add indexes - should profiled and changed at some point if necessary
@ -84,17 +87,27 @@ async function wipe() {
await Modlogs.db.createIndex({ 'board': 1 })
await Files.db.createIndex({ 'count': 1 })
await Bans.db.createIndex({ 'ip': 1 , 'board': 1 })
await Bans.db.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 0 }) //custom expiry, i.e. it will expire when current date > than this date
await Captchas.db.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 300 }) //captchas valid for 5 minutes
await Ratelimits.db.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 60 }) //per minute captcha ratelimit
await Bans.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }) //custom expiry, i.e. it will expire when current date > than this date
await Bypass.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 })
await Captchas.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 300 }) //captchas valid for 5 minutes
await Ratelimits.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 60 }) //per minute captcha ratelimit
await Posts.db.createIndex({ 'postId': 1,'board': 1,})
await Posts.db.createIndex({ 'board': 1, 'thread': 1, 'bumped': -1 })
await Posts.db.createIndex({ 'board': 1, 'reports.0': 1 }, { 'partialFilterExpression': { 'reports.0': { '$exists': true } } })
await Posts.db.createIndex({ 'globalreports.0': 1 }, { 'partialFilterExpression': { 'globalreports.0': { '$exists': true } } })
await Accounts.insertOne('admin', 'admin', 'changeme', 0);
await db.collection('version').replaceOne({
'_id': 'version'
}, {
'_id': 'version',
'version': migrateVersion
}, {
upsert: true
});
await Mongo.client.close();
cache.redisClient.quit();
Redis.redisClient.quit();
//delete all the static files
return Promise.all([
@ -132,13 +145,13 @@ function images() {
}
async function cache() {
const cache = require(__dirname+'/redis.js');
const Redis = require(__dirname+'/redis.js')
await Promise.all([
cache.deletePattern('board:*'),
cache.deletePattern('banners:*'),
cache.deletePatterh('blacklisted:*'),
Redis.deletePattern('board:*'),
Redis.deletePattern('banners:*'),
Redis.deletePattern('blacklisted:*'),
]);
cache.redisClient.quit();
Redis.redisClient.quit();
}
function deletehtml() {
@ -198,8 +211,52 @@ function scripts() {
.pipe(gulp.dest(paths.scripts.dest));
}
async function migrate() {
const Mongo = require(__dirname+'/db/db.js')
const Redis = require(__dirname+'/redis.js')
await Mongo.connect();
const db = Mongo.client.db('jschan');
//get current version from db if present (set in 'reset' task in recent versions)
let currentVersion = await db.collection('version').findOne({
'_id': 'version'
}).then(res => res ? res.version : '0.0.0'); // 0.0.0 for old versions
if (currentVersion < migrateVersion) {
console.log(`Current version: ${currentVersion}`);
const migrations = require(__dirname+'/migrations/');
const migrationVersions = Object.keys(migrations).sort().filter(v => v > currentVersion);
console.log(`Migrations needed: ${currentVersion} -> ${migrationVersions.join(' -> ')}`);
for (let ver of migrationVersions) {
console.log(`=====\nStarting migration to version ${ver}`);
try {
await migrations[ver](db, Redis);
await db.collection('version').replaceOne({
'_id': 'version'
}, {
'_id': 'version',
'version': ver
}, {
upsert: true
});
} catch (e) {
console.error(e);
console.warn(`Migration to ${ver} encountered an error`);
}
console.log(`Finished migrating to version ${ver}`);
}
} else {
console.log(`Migration not required, you are already on the current version (${migrateVersion})`)
}
await Mongo.client.close();
Redis.redisClient.quit();
}
const build = gulp.parallel(css, scripts, images, gulp.series(deletehtml, custompages));
const reset = gulp.parallel(wipe, build);
const reset = gulp.series(wipe, build);
const html = gulp.series(deletehtml, custompages);
module.exports = {
@ -211,5 +268,6 @@ module.exports = {
scripts,
wipe,
cache,
migrate,
default: build,
};

@ -53,6 +53,9 @@ module.exports = async () => {
const captcha = gm(width,height,'#ffffff')
.fill('#000000')
.fontSize(65);
if (captchaOptions.fontPaths && captchaOptions.fontPaths.length > 0) {
captcha.font(captchaOptions.fontPaths[Math.floor(Math.random() * captchaOptions.fontPaths.length)]);
}
const startX = (width-totalWidth(text))/2;
let charX = startX;
for (let i = 0; i < 6; i++) {

@ -56,9 +56,10 @@ module.exports = async (req, res, next) => {
}
//it was correct, so delete the file, the cookie and reset their quota
res.locals.solvedCaptcha = true;
res.clearCookie('captchaid');
await Promise.all([
Ratelimits.resetQuota(res.locals.ip.hash, 'captcha'),
Ratelimits.resetQuota(res.locals.ip.single, 'captcha'),
remove(`${uploadDirectory}/captcha/${captchaId}.jpg`)
]);

@ -0,0 +1,62 @@
'use strict';
const { Bypass } = require(__dirname+'/../../db/')
, { ObjectId } = require(__dirname+'/../../db/db.js')
, { secureCookies, blockBypass } = require(__dirname+'/../../configs/main.js')
, dynamicResponse = require(__dirname+'/../dynamic.js')
, production = process.env.NODE_ENV === 'production';
module.exports = async (req, res, next) => {
if (!blockBypass.enabled) {
return next();
}
//check if blockbypass exists and right length
const bypassId = req.cookies.bypassid;
if (!res.locals.solvedCaptcha && (!bypassId || bypassId.length !== 24)) {
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Missing or invalid block bypass',
'redirect': '/bypass.html',
'link': '/bypass.html',
});
}
//try to get bypass from db and make sure uses < maxUses
let bypass;
if (bypassId && bypassId.length === 24) {
try {
const bypassMongoId = ObjectId(bypassId);
bypass = await Bypass.checkBypass(bypassMongoId);
res.locals.blockBypass = bypass;
} catch (err) {
return next(err);
}
}
if (bypass && bypass.uses < blockBypass.expireAfterUses) {
return next();
}
if (res.locals.solvedCaptcha) {
//they dont have a valid bypass, but just solved board captcha, so give them a new one
const newBypass = await Bypass.getBypass();
const newBypassId = newBypass.insertedId;
res.locals.blockBypass = newBypass.ops[0];
res.cookie('bypassid', newBypassId.toString(), {
'maxAge': blockBypass.expireAfterTime,
'secure': production && secureCookies,
'sameSite': 'strict'
});
return next();
}
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Block bypass expired or exceeded max uses',
'redirect': '/bypass.html',
'link': '/bypass.html',
});
}

@ -2,12 +2,13 @@
const cache = require(__dirname+'/../../redis.js')
, dynamicResponse = require(__dirname+'/../dynamic.js')
, { dnsbl } = require(__dirname+'/../../configs/main.js')
, { dnsbl, blockBypass } = require(__dirname+'/../../configs/main.js')
, { batch } = require('dnsbl');
module.exports = async (req, res, next) => {
if (dnsbl.enabled && dnsbl.blacklists.length > 0) {
if (dnsbl.enabled && dnsbl.blacklists.length > 0 //if dnsbl enabled and has more than 0 blacklists
&& (!res.locals.blockBypass || !blockBypass.bypassDnsbl)) { //and there is no valid block bypass, or they do not bypass dnsbl
const ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
let isBlacklisted = await cache.get(`blacklisted:${ip}`);
if (isBlacklisted === null) { //not cached

@ -44,15 +44,15 @@ module.exports = async (req, res) => {
'_id': {
'$gt': last120id
},
'ip.hash': res.locals.ip.hash,
'ip.single': res.locals.ip.single,
'$or': contentOr
});
//any posts from same IP in past 5 seconds
//any posts from same IP in past 5 seconds TODO: make this just use a redis key of IP and expire after 5 seconds
ors.push({
'_id': {
'$gt': last5id
},
'ip.hash': res.locals.ip.hash
'ip.single': res.locals.ip.single
})
let flood = await Posts.db.find({

@ -1,11 +1,13 @@
'use strict';
const escapeRegExp = require(__dirname+'/escaperegexp.js')
const escapeRegExp = require(__dirname+'/escaperegexp.js')
, { ipHashMode } = require(__dirname+'/../configs/main.js')
module.exports = (query) => {
module.exports = (query, permLevel) => {
if (query.ip) {
const decoded = decodeURIComponent(query.ip);
if (decoded.length === 10) {
const hashed = ipHashMode === 2 || (ipHashMode === 1 && permLevel > 1)
if (!hashed || decoded.length === 10) {
return new RegExp(`${escapeRegExp(decoded)}$`);
}
}

@ -0,0 +1,6 @@
'use strict';
const { ipHashSecret } = require(__dirname+'/../configs/main.js')
, { createHash } = require('crypto');
module.exports = (ip) => createHash('sha256').update(ipHashSecret + ip).digest('base64');

@ -6,7 +6,7 @@ const greentextRegex = /^&gt;((?!&gt;).+)/gm
, titleRegex = /&#x3D;&#x3D;(.+)&#x3D;&#x3D;/gm
, monoRegex = /&#x60;(.+?)&#x60;/gm
, underlineRegex = /__(.+?)__/gm
, strikethroughRegex = /~~(.+?)~~/gm
, strikeRegex = /~~(.+?)~~/gm
, italicRegex = /\*\*(.+?)\*\*/gm
, spoilerRegex = /\|\|([\s\S]+?)\|\|/gm
, detectedRegex = /(\(\(\(.+?\)\)\))/gm
@ -16,10 +16,23 @@ const greentextRegex = /^&gt;((?!&gt;).+)/gm
, trimNewlineRegex = /^\s*(\r?\n)*|(\r?\n)*$/g
, diceRegex = /##(?<numdice>\d+)d(?<numsides>\d+)(?:(?<operator>[+-])(?<modifier>\d+))?/gmi
, getDomain = (string) => string.split(/\/\/|\//)[1] //unused atm
, diceRoll = require(__dirname+'/diceroll.js')
, escape = require(__dirname+'/escape.js')
, { highlight, highlightAuto } = require('highlight.js')
, { highlightOptions } = require(__dirname+'/../../configs/main.js');
, { highlightOptions } = require(__dirname+'/../../configs/main.js')
, replacements = [
{ regex: pinktextRegex, cb: (match, pinktext) => `<span class='pinktext'>&lt;${pinktext}</span>` },
{ regex: greentextRegex, cb: (match, greentext) => `<span class='greentext'>&gt;${greentext}</span>` },
{ regex: boldRegex, cb: (match, bold) => `<span class='bold'>${bold}</span>` },
{ regex: underlineRegex, cb: (match, underline) => `<span class='underline'>${underline}</span>` },
{ regex: strikeRegex, cb: (match, strike) => `<span class='strike'>${strike}</span>` },
{ regex: titleRegex, cb: (match, title) => `<span class='title'>${title}</span>` },
{ regex: italicRegex, cb: (match, italic) => `<span class='em'>${italic}</span>` },
{ regex: spoilerRegex, cb: (match, spoiler) => `<span class='spoiler'>${spoiler}</span>` },
{ regex: monoRegex, cb: (match, mono) => `<span class='mono'>${mono}</span>` },
{ regex: detectedRegex, cb: (match, detected) => `<span class='detected'>${detected}</span>` },
{ regex: linkRegex, cb: (match) => `<a rel='nofollow' referrerpolicy='same-origin' target='_blank' href='${match}'>${match}</a>` },
{ regex: diceRegex, cb: require(__dirname+'/diceroll.js') },
];
module.exports = {
@ -58,29 +71,10 @@ module.exports = {
},
processRegularChunk: (text) => {
return text.replace(pinktextRegex, (match, pinktext) => {
return `<span class='pinktext'>&lt;${pinktext}</span>`;
}).replace(greentextRegex, (match, greentext) => {
return `<span class='greentext'>&gt;${greentext}</span>`;
}).replace(boldRegex, (match, bold) => {
return `<span class='bold'>${bold}</span>`;
}).replace(underlineRegex, (match, underline) => {
return `<span class='underline'>${underline}</span>`;
}).replace(strikethroughRegex, (match, strike) => {
return `<span class='strikethrough'>${strike}</span>`;
}).replace(titleRegex, (match, title) => {
return `<span class='title'>${title}</span>`;
}).replace(italicRegex, (match, italic) => {
return `<span class='em'>${italic}</span>`;
}).replace(spoilerRegex, (match, spoiler) => {
return `<span class='spoiler'>${spoiler}</span>`;
}).replace(monoRegex, (match, mono) => {
return `<span class='mono'>${mono}</span>`;
}).replace(detectedRegex, (match, detected) => {
return `<span class='detected'>${detected}</span>`;
}).replace(linkRegex, (match) => {
return `<a rel='nofollow' referrerpolicy='same-origin' target='_blank' href='${match}'>${match}</a>`;
}).replace(diceRegex, diceRoll);
for (let i = 0; i < replacements.length; i++) {
text = text.replace(replacements[i].regex, replacements[i].cb);
}
return text;
},
}

@ -1,8 +1,8 @@
'use strict';
const { ipHashSecret } = require(__dirname+'/../configs/main.js')
const { ipHashMode } = require(__dirname+'/../configs/main.js')
, { isIP } = require('net')
, { createHash } = require('crypto');
, hashIp = require(__dirname+'/haship.js');
module.exports = (req, res, next) => {
@ -11,10 +11,12 @@ module.exports = (req, res, next) => {
if (ipVersion) {
const delimiter = ipVersion === 4 ? '.' : ':';
let split = ip.split(delimiter);
const qrange = split.slice(0,Math.floor(split.length*0.75)).join(delimiter);
const hrange = split.slice(0,Math.floor(split.length*0.5)).join(delimiter);
res.locals.ip = {
hash: createHash('sha256').update(ipHashSecret + ip).digest('base64'),
qrange: createHash('sha256').update(ipHashSecret + split.slice(0,Math.floor(split.length*0.75)).join(delimiter)).digest('base64'),
hrange: createHash('sha256').update(ipHashSecret + split.slice(0,Math.floor(split.length*0.5)).join(delimiter)).digest('base64'),
single: ipHashMode === 2 ? hashIp(ip) : ip,
qrange: ipHashMode === 2 ? hashIp(qrange) : qrange,
hrange: ipHashMode === 2 ? hashIp(hrange) : hrange,
}
next();
} else {

@ -228,6 +228,10 @@ module.exports = {
return render('register.html', 'register.pug');
},
buildBypass: () => {
return render('bypass.html', 'bypass.pug');
},
buildCreate: () => {
return render('create.html', 'create.pug');
},

@ -0,0 +1,6 @@
'use strict';
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 +1,8 @@
'use strict';
module.exports = async(db, redis) => {
console.log('Creating bypass collection');
await db.createCollection('bypass');
console.log('Creating bypass collection index');
await db.collection('bypass').createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 });
};

@ -0,0 +1,10 @@
'use strict';
module.exports = async(db, redis) => {
console.log('Renaming IP fields on posts');
await db.collection('posts').updateMany({}, {
'$rename': {
'ip.hash': 'ip.single'
}
});
};

@ -79,7 +79,7 @@ module.exports = async (req, res, next) => {
if (deleting) {
const postsBefore = res.locals.posts.length;
if (req.body.delete_ip_board || req.body.delete_ip_global || req.body.delete_ip_thread) {
const deletePostIps = res.locals.posts.map(x => x.ip.hash);
const deletePostIps = res.locals.posts.map(x => x.ip.single);
const deletePostMongoIds = res.locals.posts.map(x => x._id)
let query = {
'_id': {
@ -269,9 +269,10 @@ module.exports = async (req, res, next) => {
postIds: [],
actions: modlogActions,
date: logDate,
user: logUser,
showUser: req.body.show_name || logUser === 'Unregistered User' ? true : false,
message: message,
user: logUser,
ip: res.locals.ip.single,
};
}
//push each post id
@ -369,6 +370,7 @@ module.exports = async (req, res, next) => {
});
//get replies, files, bump date, from threads
const threadAggregates = await Posts.getThreadAggregates(threadOrs);
//TODO: change query to fetch threads and group into bumplocked/normal, and only reset bump date on non-bumplocked and ignore sages
const bulkWrites = [];
for (let i = 0; i < threadAggregates.length; i++) {
const threadAggregate = threadAggregates[i];

@ -4,6 +4,6 @@ const { Bans } = require(__dirname+'/../../db/');
module.exports = async (req, res, next) => {
return Bans.appeal(res.locals.ip.hash, req.body.checkedbans, req.body.message).then(r => r.modifiedCount);
return Bans.appeal(res.locals.ip.single, req.body.checkedbans, req.body.message).then(r => r.modifiedCount);
}

@ -15,10 +15,10 @@ module.exports = async (req, res, next) => {
if (req.body.ban || req.body.global_ban) {
const banBoard = req.body.global_ban ? null : req.params.board;
const ipPosts = res.locals.posts.reduce((acc, post) => {
if (!acc[post.ip.hash]) {
acc[post.ip.hash] = [];
if (!acc[post.ip.single]) {
acc[post.ip.single] = [];
}
acc[post.ip.hash].push(post);
acc[post.ip.single].push(post);
return acc;
}, {});
for (let ip in ipPosts) {

@ -0,0 +1,24 @@
'use strict';
const { Bypass } = require(__dirname+'/../../db/')
, { secureCookies, blockBypass } = require(__dirname+'/../../configs/main.js')
, production = process.env.NODE_ENV === 'production';
module.exports = async (req, res, next) => {
const bypass = await Bypass.getBypass();
const bypassId = bypass.insertedId;
res.locals.blockBypass = bypass.ops[0];
return res
.cookie('bypassid', bypassId.toString(), {
'maxAge': blockBypass.expireAfterTime,
'secure': production && secureCookies,
'sameSite': 'strict'
})
.render('message', {
'title': 'Success',
'message': 'Completed block bypass, you may go back and make your post.',
});
}

@ -6,6 +6,8 @@ const uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.
, { Posts, Files } = require(__dirname+'/../../db/')
, linkQuotes = require(__dirname+'/../../helpers/posting/quotes.js')
, { markdown } = require(__dirname+'/../../helpers/posting/markdown.js')
, { pruneImmediately } = require(__dirname+'/../../configs/main.js')
, pruneFiles = require(__dirname+'/../../schedules/prune.js')
, sanitize = require('sanitize-html')
, sanitizeOptions = require(__dirname+'/../../helpers/posting/sanitizeoptions.js');
@ -58,6 +60,9 @@ module.exports = async (posts, board, all=false) => {
if (postFiles.length > 0) {
const fileNames = postFiles.map(x => x.filename)//[...new Set(postFiles.map(x => x.filename))];
await Files.decrement(fileNames);
if (pruneImmediately) {
await pruneFiles(fileNames);
}
}
const bulkWrites = [];

@ -1,6 +1,8 @@
'use strict';
const { Files } = require(__dirname+'/../../db/')
, { pruneImmediately } = require(__dirname+'/../../configs/main.js')
, pruneFiles = require(__dirname+'/../../schedules/prune.js')
, deletePostFiles = require(__dirname+'/../../helpers/files/deletepostfiles.js');
module.exports = async (posts, unlinkOnly) => {
@ -28,7 +30,11 @@ module.exports = async (posts, unlinkOnly) => {
}
if (files.length > 0) {
await Files.decrement(files.map(x => x.filename));
const fileNames = files.map(x => x.filename);
await Files.decrement(fileNames);
if (pruneImmediately) {
await pruneFiles(fileNames);
}
}
if (unlinkOnly) {

@ -125,7 +125,7 @@ module.exports = async (req, res, next) => {
const banDate = new Date();
const banExpiry = new Date(useFilterBanDuration + banDate.getTime());
const ban = {
'ip': res.locals.ip.hash,
'ip': res.locals.ip.single,
'reason': `${hitGlobalFilter ? 'global ' :''}word filter auto ban`,
'board': banBoard,
'posts': null,
@ -136,7 +136,7 @@ module.exports = async (req, res, next) => {
'seen': false
};
await Bans.insertOne(ban);
const bans = await Bans.find(res.locals.ip.hash, banBoard); //need to query db so it has _id field for appeal checkmark
const bans = await Bans.find(res.locals.ip.single, banBoard); //need to query db so it has _id field for appeal checkmark
return res.status(403).render('ban', {
bans: bans
});
@ -193,6 +193,7 @@ module.exports = async (req, res, next) => {
//increment file count
await Files.increment(processedFile);
req.files.file[i].inced = true;
//check if already exists
const existsFull = await pathExists(`${uploadDirectory}/img/${processedFile.filename}`);
processedFile.sizeString = formatSize(processedFile.size)
@ -291,7 +292,7 @@ module.exports = async (req, res, next) => {
salt = (await randomBytes(128)).toString('base64');
}
if (ids === true) {
const fullUserIdHash = createHash('sha256').update(salt + res.locals.ip.hash).digest('hex');
const fullUserIdHash = createHash('sha256').update(salt + res.locals.ip.single).digest('hex');
userId = fullUserIdHash.substring(fullUserIdHash.length-6);
}
let country = null;
@ -466,7 +467,7 @@ module.exports = async (req, res, next) => {
}
}
const successRedirect = `/${req.params.board}/thread/${req.body.thread || postId}.html#${postId}`;
const successRedirect = `/${req.params.board}/${req.path.endsWith('/modpost') ? 'manage/' : ''}thread/${req.body.thread || postId}.html#${postId}`;
const buildOptions = {
'threadId': data.thread || postId,
@ -475,7 +476,7 @@ module.exports = async (req, res, next) => {
if (req.headers['x-using-live'] != null && data.thread) {
//defer build and post will come live
res.json({
res.json({
'postId': postId,
'redirect': successRedirect
});
@ -487,7 +488,7 @@ module.exports = async (req, res, next) => {
//build immediately and refresh when built
await buildThread(buildOptions);
if (req.headers['x-using-xhr'] != null) {
res.json({
res.json({
'postId': postId,
'redirect': successRedirect
});

@ -8,7 +8,7 @@ module.exports = (req, res) => {
'id': ObjectId(),
'reason': req.body.report_reason,
'date': new Date(),
'ip': res.locals.ip.hash //just hash for now, no rangeban reporters
'ip': res.locals.ip.single
}
const ret = {

@ -0,0 +1,16 @@
'use strict';
const { buildBypass } = require(__dirname+'/../../helpers/tasks.js');
module.exports = async (req, res, next) => {
let html;
try {
html = await buildBypass();
} catch (err) {
return next(err);
}
return res.send(html);
}

@ -2,7 +2,7 @@
const { Ratelimits } = require(__dirname+'/../../db/')
, generateCaptcha = require(__dirname+'/../../helpers/captcha/captchagenerate.js')
, { secureCookies } = require(__dirname+'/../../configs/main.js')
, { secureCookies, rateLimitCost } = require(__dirname+'/../../configs/main.js')
, production = process.env.NODE_ENV === 'production';
module.exports = async (req, res, next) => {
@ -13,7 +13,7 @@ module.exports = async (req, res, next) => {
let captchaId;
try {
const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.hash, 'captcha', 10);
const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.hash, 'captcha', rateLimitCost.captcha);
if (ratelimit > 100) {
return res.status(429).redirect('/img/ratelimit.png');
}

@ -2,6 +2,7 @@
const { Modlogs } = require(__dirname+'/../../../db/')
, pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js')
, decodeQueryIP = require(__dirname+'/../../../helpers/decodequeryip.js')
, limit = 50;
module.exports = async (req, res, next) => {
@ -17,6 +18,10 @@ module.exports = async (req, res, next) => {
if (uri && !Array.isArray(uri)) {
filter.board = uri;
}
const ipMatch = decodeQueryIP(req.query, res.locals.permLevel);
if (ipMatch) {
filter.ip = ipMatch;
}
let logs, maxPage;
try {
@ -36,6 +41,7 @@ module.exports = async (req, res, next) => {
queryString,
username,
uri,
ip: ipMatch ? req.query.ip : null,
logs,
page,
maxPage,

@ -8,7 +8,7 @@ const { Posts } = require(__dirname+'/../../../db/')
module.exports = async (req, res, next) => {
const { page, offset, queryString } = pageQueryConverter(req.query, limit);
let ipMatch = decodeQueryIP(req.query);
let ipMatch = decodeQueryIP(req.query, res.locals.permLevel);
let posts;
try {

@ -8,7 +8,7 @@ const { Posts } = require(__dirname+'/../../../db/')
module.exports = async (req, res, next) => {
const { page, offset, queryString } = pageQueryConverter(req.query, limit);
let ipMatch = decodeQueryIP(req.query);
let ipMatch = decodeQueryIP(req.query, res.locals.permLevel);
let reports;
try {

@ -2,6 +2,7 @@
module.exports = {
changePassword: require(__dirname+'/changepassword.js'),
blockBypass: require(__dirname+'/blockbypass.js'),
register: require(__dirname+'/register.js'),
account: require(__dirname+'/account.js'),
home: require(__dirname+'/home.js'),

@ -1,6 +1,8 @@
'use strict';
const Bans = require(__dirname+'/../../../db/bans.js');
const Bans = require(__dirname+'/../../../db/bans.js')
, { ipHashMode } = require(__dirname+'/../../../configs/main.js')
, hashIp = require(__dirname+'/../../../helpers/haship.js');
module.exports = async (req, res, next) => {
@ -10,6 +12,11 @@ module.exports = async (req, res, next) => {
} catch (err) {
return next(err)
}
if (ipHashMode === 1 && res.locals.permLevel > 1) {
for (let i = 0; i < bans.length; i++) {
bans[i].ip = hashIp(bans[i].ip);
}
}
res
.set('Cache-Control', 'private, max-age=5')

@ -0,0 +1,31 @@
'use strict';
const Posts = require(__dirname+'/../../../db/posts.js');
module.exports = async (req, res, next) => {
const page = req.params.page === 'index' ? 1 : Number(req.params.page);
let maxPage;
let threads;
try {
maxPage = Math.min(Math.ceil((await Posts.getPages(req.params.board)) / 10), Math.ceil(res.locals.board.settings.threadLimit/10)) || 1;
if (page > maxPage) {
return next();
}
threads = await Posts.getRecent(req.params.board, page);
} catch (err) {
return next(err);
}
res
.set('Cache-Control', 'private, max-age=5')
.render('board', {
modview: true,
page,
maxPage,
threads,
board: res.locals.board,
csrf: req.csrfToken(),
});
}

@ -5,4 +5,6 @@ module.exports = {
manageSettings: require(__dirname+'/settings.js'),
manageBans: require(__dirname+'/bans.js'),
manageBanners: require(__dirname+'/banners.js'),
manageBoard: require(__dirname+'/board.js'),
manageThread: require(__dirname+'/thread.js'),
}

@ -0,0 +1,26 @@
'use strict';
const Posts = require(__dirname+'/../../../db/posts.js');
module.exports = async (req, res, next) => {
let thread;
try {
thread = await Posts.getThread(res.locals.board._id, res.locals.thread.postId);
if (!thread) {
return next(); //deleted between exists
}
} catch (err) {
return next(err);
}
res
.set('Cache-Control', 'private, max-age=5')
.render('thread', {
modview: true,
board: res.locals.board,
thread,
csrf: req.csrfToken(),
});
}

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

@ -6,7 +6,7 @@ process
const timeUtils = require(__dirname+'/../helpers/timeutils.js')
, Mongo = require(__dirname+'/../db/db.js')
, { debugLogs, enableWebring } = require(__dirname+'/../configs/main.js')
, { pruneImmediately, debugLogs, enableWebring } = require(__dirname+'/../configs/main.js')
, doInterval = require(__dirname+'/../helpers/dointerval.js');
(async () => {
@ -31,8 +31,10 @@ const timeUtils = require(__dirname+'/../helpers/timeutils.js')
doInterval(deleteCaptchas, timeUtils.MINUTE*5, true);
//file pruning
const pruneFiles = require(__dirname+'/prune.js');
doInterval(pruneFiles, timeUtils.DAY, true);
if (!pruneImmediately) {
const pruneFiles = require(__dirname+'/prune.js');
doInterval(pruneFiles, timeUtils.DAY, true);
}
//update the webring
if (enableWebring) {

@ -5,25 +5,26 @@ const Files = require(__dirname+'/../db/files.js')
, { remove } = require('fs-extra')
, uploadDirectory = require(__dirname+'/../helpers/files/uploadDirectory.js');
module.exports = async() => {
//todo: make this not a race condition, but it only happens daily so ¯\_(ツ)_/¯
const files = await Files.db.find({
module.exports = async(fileNames) => {
const query = {
'count': {
'$lt': 1
'$lte': 0
}
}, {
}
if (fileNames) {
query['_id'] = {
'$in': fileNames
};
}
const unreferenced = await Files.db.find(query, {
'projection': {
'count': 0,
'size': 0
}
}).toArray()
await Files.db.removeMany({
'count': {
'$lte': 0
}
});
await Promise.all(files.map(async file => {
debugLogs && console.log(file._id)
}).toArray();
await Files.db.removeMany(query);
await Promise.all(unreferenced.map(async file => {
debugLogs && console.log('Pruning', file._id);
return Promise.all(
[remove(`${uploadDirectory}/img/${file._id}`)]
.concat(file.exts ? file.exts.filter(ext => ext).map(ext => {

@ -12,7 +12,7 @@ const express = require('express')
, server = require('http').createServer(app)
, cookieParser = require('cookie-parser')
, { cacheTemplates, boardDefaults, sessionSecret, globalLimits,
secureCookies, debugLogs, meta, port } = require(__dirname+'/configs/main.js')
secureCookies, debugLogs, ipHashMode, meta, port } = require(__dirname+'/configs/main.js')
, processIp = require(__dirname+'/helpers/processip.js')
, referrerCheck = require(__dirname+'/helpers/referrercheck.js')
, { themes, codeThemes } = require(__dirname+'/helpers/themes.js')
@ -85,6 +85,7 @@ const express = require('express')
app.locals.defaultTheme = boardDefaults.theme;
app.locals.defaultCodeTheme = boardDefaults.codeTheme;
app.locals.globalLimits = globalLimits;
app.locals.ipHashMode = ipHashMode;
app.locals.meta = meta;
// routes

@ -16,6 +16,9 @@ details.toggle-label
| Global Report
label
input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off')
label
input.post-check(type='checkbox', name='delete_ip_thread' value='1')
| Delete from IP in thread
label
input.post-check(type='checkbox', name='delete_ip_board' value='1')
| Delete from IP on board
@ -55,4 +58,21 @@ details.toggle-label
input(type='text', name='ban_duration', placeholder='ban duration e.g. 7d' autocomplete='off')
label
input(type='text', name='log_message', placeholder='modlog message' autocomplete='off')
label
input.post-check(type='checkbox', name='move' value='1')
| Move
label
input(type='number', name='move_to_thread', placeholder='destination thread No.' autocomplete='off')
label
input.post-check(type='checkbox', name='sticky' value='1')
| Toggle Sticky
label
input.post-check(type='checkbox', name='lock' value='1')
| Toggle Lock
label
input.post-check(type='checkbox', name='bumplock' value='1')
| Toggle Bumplock
label
input.post-check(type='checkbox', name='cyclic' value='1')
| Toggle Cycle
input(type='submit', value='submit')

@ -3,11 +3,13 @@
- const messageRequired = (!isThread && board.settings.forceThreadMessage) || (isThread && board.settings.forceReplyMessage);
- const fileRequired = (!isThread && board.settings.forceThreadFile) || (isThread && board.settings.forceReplyFile);
section.form-wrapper.flex-center
form.form-post#postform(action=`/forms/board/${board._id}/post`, enctype='multipart/form-data', method='POST')
form.form-post#postform(action=`/forms/board/${board._id}/${modview ? 'mod' : ''}post`, enctype='multipart/form-data', method='POST')
if modview
input(type='hidden' name='_csrf' value=csrf)
input(type='hidden' name='thread' value=isThread ? thread.postId : null)
section.row.jsonly
.noselect#dragHandle
if board.settings.forceAnon
if board.settings.forceAnon && !modview
section.row
.label Sage
label.postform-style.ph-5
@ -49,11 +51,11 @@ section.form-wrapper.flex-center
label.postform-style.ph-5.ml-1.fh
input(type='checkbox', name='spoiler', value='true')
| Spoiler
if board.settings.userPostSpoiler || board.settings.userPostDelete || board.settings.userPostUnlink
if board.settings.userPostSpoiler || board.settings.userPostDelete || board.settings.userPostUnlink || modview
section.row
.label Password
input(type='password', name='postpassword' placeholder='password to delete/spoiler/unlink later' maxlength=globalLimits.fieldLength.postpassword)
if (board.settings.captchaMode === 1 && !isThread) || board.settings.captchaMode === 2
input(type='password', name='postpassword', placeholder='password to delete/spoiler/unlink later' maxlength='50')
if ((board.settings.captchaMode === 1 && !isThread) || board.settings.captchaMode === 2) && !modview
section.row
.label
span Captcha

@ -10,7 +10,8 @@ mixin ban(ban, banpage)
else
| Global
td= ban.reason
td ...#{ban.ip.slice(-10)}
- const ip = ipHashMode === 2 || (ipHashMode === 1 && permLevel > 1) ? ban.ip.slice(-10) : ban.ip;
td #{ip}
td= ban.issuer
- const banDate = new Date(ban.date);
td: time.right.reltime(datetime=banDate.toISOString()) #{banDate.toLocaleString(undefined, {hour12:false})}

@ -1,10 +1,16 @@
mixin managenav(selected)
mixin managenav(selected, upLevel)
nav.pages
a(href='reports.html' class=(selected === 'reports' ? 'bold' : '')) [Reports]
if selected === 'index'
include ../includes/boardpages.pug
|
else
a(href=`${upLevel ? '../' : ''}index.html` class=(selected === 'index' ? 'bold' : '')) [Mod Index]
|
a(href=`${upLevel ? '../' : ''}reports.html` class=(selected === 'reports' ? 'bold' : '')) [Reports]
|
a(href='bans.html' class=(selected === 'bans' ? 'bold' : '')) [Bans]
a(href=`${upLevel ? '../' : ''}bans.html` class=(selected === 'bans' ? 'bold' : '')) [Bans]
|
if permLevel < 3
a(href='settings.html' class=(selected === 'settings' ? 'bold' : '')) [Settings]
a(href=`${upLevel ? '../' : ''}settings.html` class=(selected === 'settings' ? 'bold' : '')) [Settings]
|
a(href='banners.html' class=(selected === 'banners' ? 'bold' : '')) [Banners]
a(href=`${upLevel ? '../' : ''}banners.html` class=(selected === 'banners' ? 'bold' : '')) [Banners]

@ -4,8 +4,8 @@ mixin modal(data)
.row
p.bold #{data.title}
a.close.postform-style#modalclose X
.row
if data.message || data.messages || data.error || data.errors
if data.message || data.messages || data.error || data.errors
.row
ul.nomarks
if data.message
li #{data.message}
@ -17,7 +17,11 @@ mixin modal(data)
if data.errors
each error in data.errors
li #{error}
else if data.settings
if data.link
.row
a(href=data.link target='_blank') #{data.link}
else if data.settings
.row
.form-wrapper.flexleft.mt-10
.row
.label Theme

@ -2,15 +2,15 @@ include ./report.pug
mixin post(post, truncate, manage=false, globalmanage=false, ban=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)
- const postURL = `/${post.board}/thread/${post.thread || post.postId}.html`;
- const postURL = `/${post.board}/${modview ? 'manage/' : ''}thread/${post.thread || post.postId}.html`;
.post-info
span.noselect
label
if globalmanage
input.post-check(type='checkbox', name='globalcheckedposts' value=post._id)
|
- ipHashSub = post.ip.hash.slice(-10);
a.bold(href=`?ip=${encodeURIComponent(ipHashSub)}`) #{ipHashSub}
- const ip = ipHashMode === 2 ? post.ip.single.slice(-10) : post.ip.single;
a.bold(href=`?ip=${encodeURIComponent(ip)}`) #{ip}
else if !ban
input.post-check(type='checkbox', name='checkedposts' value=post.postId)
|
@ -79,6 +79,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
img.file-thumb(src=`/img/thumb-${file.hash}${file.thumbextension}` height=file.geometry.thumbheight width=file.geometry.thumbwidth)
else
img.file-thumb(src=`/img/${file.filename}` height=file.geometry.height width=file.geometry.width)
- if (post.message && modview) { post.message = post.message.replace(new RegExp(`<a class="quote" href="/${post.board}`, 'g'), `<a class="quote" href="/${post.board}/manage`); } //quick & dirty solution to a bigger problem/design issue
- let truncatedMessage = post.message;
if post.message
if truncate
@ -111,7 +112,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
if post.previewbacklinks.length > 0
div.replies.mt-5.ml-5 Replies:
each backlink in post.previewbacklinks
a.quote(href=`/${post.board}/thread/${post.thread || post.postId}.html#${backlink.postId}`) &gt;&gt;#{backlink.postId}
a.quote(href=`${postURL}#${backlink.postId}`) &gt;&gt;#{backlink.postId}
|
if post.previewbacklinks.length < post.backlinks.length
- const ombls = post.backlinks.length-post.previewbacklinks.length;
@ -119,7 +120,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
else if post.backlinks && post.backlinks.length > 0
div.replies.mt-5.ml-5 Replies:
each backlink in post.backlinks
a.quote(href=`/${post.board}/thread/${post.thread || post.postId}.html#${backlink.postId}`) &gt;&gt;#{backlink.postId}
a.quote(href=`${postURL}#${backlink.postId}`) &gt;&gt;#{backlink.postId}
|
if manage === true
each r in post.reports

@ -3,8 +3,8 @@ mixin report(r, globalmanage=false)
input.post-check(type='checkbox', name='checkedreports' value=r.id)
|
if globalmanage
- ipHashSub = r.ip.slice(-10);
a.bold(href=`?ip=${encodeURIComponent(ipHashSub)}`) #{ipHashSub}
- const ip = ipHashMode === 2 || (ipHashMode === 1 && permLevel > 1) ? r.ip.slice(-10) : r.ip;
a.bold(href=`?ip=${encodeURIComponent(ip)}`) #{ip}
|
- const reportDate = new Date(r.date);
time.reltime(datetime=reportDate.toISOString()) #{reportDate.toLocaleString(undefined, { hour12:false })}

@ -25,6 +25,8 @@ block content
li
a(href=`/${b}/index.html`) /#{b}/
| -
a(href=`/${b}/manage/index.html`) Mod Index
| ,
a(href=`/${b}/manage/reports.html`) Reports
| ,
a(href=`/${b}/manage/bans.html`) Bans
@ -42,6 +44,8 @@ block content
li
a(href=`/${b}/index.html`) /#{b}/
| -
a(href=`/${b}/manage/index.html`) Mod Index
| ,
a(href=`/${b}/manage/reports.html`) Reports
| ,
a(href=`/${b}/manage/bans.html`) Bans

@ -1,6 +1,7 @@
extends ../layout.pug
include ../mixins/post.pug
include ../mixins/boardnav.pug
include ../mixins/managenav.pug
include ../mixins/boardheader.pug
block head
@ -8,16 +9,21 @@ block head
title /#{board._id}/ - #{board.settings.name} - page #{page}
block content
+boardheader()
+boardheader(modview ? 'Mod View' : null)
br
include ../includes/postform.pug
br
include ../includes/announcements.pug
include ../includes/stickynav.pug
.pages
include ../includes/boardpages.pug
+boardnav(null, false, false)
form(action='/forms/board/'+board._id+'/actions' method='POST' enctype='application/x-www-form-urlencoded')
if modview
+managenav('index')
else
.pages
include ../includes/boardpages.pug
+boardnav(null, false, false)
form(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.
@ -28,7 +34,13 @@ block content
for post in thread.replies
+post(post, true)
hr(size=1)
.pages
include ../includes/boardpages.pug
+boardnav(null, false, false)
include ../includes/actionfooter.pug
if modview
+managenav('index')
else
.pages
include ../includes/boardpages.pug
+boardnav(null, false, false)
if modview
include ../includes/actionfooter_manage.pug
else
include ../includes/actionfooter.pug

@ -0,0 +1,15 @@
extends ../layout.pug
block head
script(src='/js/all.js')
title Block Bypass
block content
h1.board-title Block Bypass
.form-wrapper.flex-center.mv-10
form.form-post(action='/forms/blockbypass' method='POST')
.row
.label Captcha
span.col
include ../includes/captcha.pug
input(type='submit', value='Submit')

@ -21,6 +21,9 @@ block content
.row
.label Username
input(type='text' name='username' value=username)
.row
.label IP
input(type='text' name='ip' value=ip)
input(type='submit', value='Filter')
h4.no-m-p Global Logs:
if logs && logs.length > 0
@ -30,6 +33,7 @@ block content
th Date
th Board
th User
th IP
th Actions
th Post IDs
th Log Message
@ -43,11 +47,16 @@ block content
a(href=`?uri=${log.board}`) [+]
td
if log.user !== 'Unregistered User'
a(href=`accounts.html?username=${log.user}`) #{log.user}
else
a(href=`accounts.html?username=${log.user}`) #{log.user}
else
| #{log.user}
|
a(href=`?username=${log.user}`) [+]
td
- const logIp = ipHashMode === 2 ? log.ip.slice(-10) : log.ip;
a(href=`recent.html?ip=${encodeURIComponent(logIp)}`) #{logIp}
|
a(href=`?ip=${encodeURIComponent(logIp)}`) [+]
td #{log.actions}
td #{log.postIds}
td #{log.message || '-'}

@ -18,7 +18,7 @@ block content
else
hr(size=1)
if ip
h4.no-m-p Post history for ...#{ip}
h4.no-m-p Post history for #{ip}
hr(size=1)
for p in posts
.thread

@ -1,42 +1,50 @@
extends ../layout.pug
include ../mixins/post.pug
include ../mixins/boardnav.pug
include ../mixins/managenav.pug
include ../mixins/boardheader.pug
block head
script(src='/js/all.js')
title /#{board._id}/ - #{thread.subject||thread.postId}
meta(property='og:site_name', value=meta.siteName)
meta(property='og:title', content=thread.subject)
meta(property='og:url', content=meta.url)
meta(property='og:description', content=thread.nomarkup)
if thread.files.length > 0
if thread.spoiler
meta(property='og:image', content='/img/spoiler.png')
else
- const file = thread.files[0];
- const maintype = file.mimetype.split('/')[0];
case maintype
when 'image'
meta(property='og:image', content=`/img/${file.filename}`)
when 'video'
meta(property='og:video', content=`/img/${file.filename}`)
when 'audio'
meta(property='og:audio', content=`/img/${file.filename}`)
default
- break
- const subjectString = thread.subject || thread.nomarkup ? `${thread.nomarkup.substring(0, globalLimits.fieldLength.subject)}${thread.nomarkup.length > globalLimits.fieldLength.subject ? '...' : ''}` : thread.postId;
title /#{board._id}/ - #{subjectString}
if !modview
meta(property='og:site_name', value=meta.siteName)
meta(property='og:title', content=thread.subject)
meta(property='og:url', content=meta.url)
meta(property='og:description', content=thread.nomarkup)
if thread.files.length > 0
if thread.spoiler
meta(property='og:image', content='/img/spoiler.png')
else
- const file = thread.files[0];
- const maintype = file.mimetype.split('/')[0];
case maintype
when 'image'
meta(property='og:image', content=`/img/${file.filename}`)
when 'video'
meta(property='og:video', content=`/img/${file.filename}`)
when 'audio'
meta(property='og:audio', content=`/img/${file.filename}`)
default
- break
block content
+boardheader()
+boardheader(modview ? 'Mod View' : null)
br
include ../includes/postform.pug
br
include ../includes/announcements.pug
include ../includes/stickynav.pug
.pages
+boardnav(null, true, true)
if modview
+managenav(null, true)
else
.pages
+boardnav(null, true, true)
- const uids = board.settings.ids ? new Set() : void 0;
form(action=`/forms/board/${board._id}/actions` method='POST' enctype='application/x-www-form-urlencoded')
form(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)
.thread
- uids && thread.userId && uids.add(thread.userId)
@ -46,8 +54,11 @@ block content
+post(post)
hr(size=1)
.statwrap
.pages
+boardnav(null, true, true)
if modview
+managenav(null, true)
else
.pages
+boardnav(null, true, true)
#threadstats
span #{thread.replyposts} repl#{thread.replyposts === 1 ? 'y' : 'ies'}
| |
@ -59,4 +70,7 @@ block content
.dot#livecolor
| Connecting...
input.postform-style.ml-5.di(type='button' value='Update')
include ../includes/actionfooter.pug
if modview
include ../includes/actionfooter_manage.pug
else
include ../includes/actionfooter.pug

Loading…
Cancel
Save