Merge branch 'new-dev' into 'master'

- remove some hardcoded limits, for forms and elsewhere
- board edit: rebuild pages on name/description change
- separate trigger action tph/pph
- hourly reset option for captcha or lock mode
- better error handler for urlencoded form
- audio thumbnails
- support flac files
- fix grid captcha loading in frontend script when added on the fly

See merge request fatchan/jschan!186
merge-requests/208/head
Thomas Lynch 4 years ago
commit 8e11342a77
  1. 4
      README.md
  2. 39
      configs/main.js.example
  3. 2
      configs/nginx/nginx.example
  4. 2
      configs/nginx/nginx_no_https.example
  5. 2
      configs/nginx/snippets/jschan_routes.conf
  6. 2
      configs/nginx/snippets/jschan_routes_tor.conf
  7. 21
      controllers/forms/actions.js
  8. 25
      controllers/forms/boardsettings.js
  9. 4
      controllers/forms/deletebanners.js
  10. 4
      controllers/forms/deletenews.js
  11. 6
      controllers/forms/editaccounts.js
  12. 4
      controllers/forms/editbans.js
  13. 19
      controllers/forms/globalactions.js
  14. 6
      controllers/forms/uploadbanners.js
  15. 2
      db/accounts.js
  16. 2
      db/bans.js
  17. 16
      db/boards.js
  18. 2
      db/bypass.js
  19. 2
      db/captchas.js
  20. 4
      db/db.js
  21. 2
      db/files.js
  22. 2
      db/modlogs.js
  23. 2
      db/news.js
  24. 2
      db/posts.js
  25. 2
      db/ratelimits.js
  26. 2
      db/stats.js
  27. 2
      db/webring.js
  28. 4
      gulp/res/css/style.css
  29. 10
      gulp/res/js/captcha.js
  30. 14
      gulp/res/js/expand.js
  31. 9
      gulpfile.js
  32. 22
      helpers/files/audiothumbnail.js
  33. 1
      helpers/files/mimetypes.js
  34. 3
      helpers/paramconverter.js
  35. 4
      helpers/posting/markdown.js
  36. 15
      helpers/tasks.js
  37. 1
      migrations/index.js
  38. 25
      migrations/migration-0.0.16.js
  39. 14
      models/forms/changeboardsettings.js
  40. 2
      models/forms/editpost.js
  41. 101
      models/forms/makepost.js
  42. 10
      models/pages/thread.js
  43. 2
      package.json
  44. 2
      schedules/webring.js
  45. 42
      server.js
  46. 4
      views/mixins/catalogtile.pug
  47. 4
      views/mixins/post.pug
  48. 2
      views/pages/board.pug
  49. 2
      views/pages/catalog.pug
  50. 1
      views/pages/editpost.pug
  51. 151
      views/pages/managesettings.pug
  52. 2
      views/pages/thread.pug

@ -124,7 +124,7 @@ $ gulp reset
# NOTE: dont run gulp reset again unless you want to completely irreversibly wipe everything
# make pm2 (process manager) start on server restart
$ pm2 startup #and follow any promps
$ pm2 startup #and follow any prompts
# save the process list so jschan is started with pm2
$ pm2 save
```
@ -152,7 +152,7 @@ $ gulp #run default gulp task
EITHER:
- Install docker and run torproxy in a container: https://github.com/dperson/torproxy (of course, audit the docker image yourself)
- Install docker and run torproxy in a container: https://github.com/dperson/torproxy (of course, audit the docker image yourself). This is the easiest, free way to get a proxy setup and means you can also follow .onions in your webring list since requests will go through tor.
- Use your own socks proxy
Edit configs/webring.json with your proxy address and set enabled: true

@ -2,6 +2,7 @@ module.exports = {
//mongodb connection string
dbURL: 'mongodb://username:password@localhost:27017',
dbName: 'jschan',
//redis connection info
redis: {
@ -58,7 +59,7 @@ module.exports = {
distortion: 9,
},
/* dnsbl, will add a small delay for uncached requests. You could also install some
/* dnsbl, will add a small delay for uncached requests. You could also install some
kind of dns cache e.g. unbound to improve performance. DNSBL only checked for posting */
dnsbl: {
enabled: false,
@ -127,6 +128,9 @@ module.exports = {
//if animatedGifThumbnails is true, use ffmpeg for better animated gif thumbnailing. RECOMMENDED
ffmpegGifThumbnails: true,
//generate waveform thumbnails for audio
audioThumbnails: false,
//max thumb dimensions (square) in px
thumbSize: 250,
@ -134,9 +138,9 @@ module.exports = {
videoThumbPercentage: 5,
/* extra mime types for files to be uploaded as attachments (no thumbnails) e.g. text files/archives
NOTE: appropriate extensions will need to be added to nginx configuration, and uncommend the provided
NOTE: appropriate extensions will need to be added to nginx configuration, and uncomment the provided
"other files" section which includes an example configuration for .txt files to match this default config.
mime types and file extention does not always correspond exactly this cant be done automatically. */
mime types and file extension does not always correspond exactly this cant be done automatically. */
otherMimeTypes: [
'text/plain',
'application/pdf'
@ -153,7 +157,7 @@ module.exports = {
//max number of quotes that will be linked in a post. 0 for unlimited (not recommended)
quoteLimit: 25,
//aply global filters more aggressively, trying against extra text that strips diacritics and some ZWS chars
//apply global filters more aggressively, trying against extra text that strips diacritics and some ZWS chars
strictFiltering: false,
//how many replies to show on index pages under each OP
@ -236,8 +240,9 @@ module.exports = {
postFilesSize: { //in bytes, 10MB default
max: 10485760
},
bannerFiles: { //max number of banners uploadable at once
max: 10
bannerFiles: {
max: 10, //max number of banners uploadable at once
total: 100, //max number of banners for a board in total
},
bannerFilesSize: { //in bytes, 10MB default
max: 10485760
@ -261,6 +266,14 @@ module.exports = {
boardname: 50,
description: 100,
},
multiInputs: {
//limits on certain requests that have multi-select. set any to 0 for unlimited
posts: { //post actions (report, delete, spoiler, ban, etc).
anon: 20, //separate for non-staff to prevent performance issues or abuse e.g. user mass self-deleting or reporting posts with single captcha
staff: 100, //but to still allow mass moderation for staff
},
//note: room for more limits here if they are needed for other checkbox forms in future
},
customCss: {
enabled: true, //allow custom css by board owners
max: 10000, //max number of characters to allow
@ -307,10 +320,14 @@ module.exports = {
unlistedLocal: false, //board hidden from on-site board list and frontpage
unlistedWebring: false, //board hidden from webring
captchaMode: 0, //0=disabled, 1=for threads, 2=for all posts
tphTrigger: 0, //numebr of threads in an hour before trigger action is activated
pphTrigger: 0, //number of posts in an hour before ^
triggerAction: 0, //0=nothing, 1=captcha enable for threads, 2=captcha enable for all posts, 3=lock board
resetTrigger: false, //reset captcha/lock settings back to original at the end of hour
tphTrigger: 10, //numebr of threads in an hour before trigger action is activated
pphTrigger: 50, //number of posts in an hour before ^
//0=none, 1=captcha enable for threads, 2=captcha enable for all posts, 3=lock board
tphTriggerAction: 1,
pphTriggerAction: 2,
//0=dont change, 1=unlock board/disable captcha, 2=lock thread creation/enable captcha for thread creation
captchaReset: 0,
lockReset: 0,
forceAnon: false, //disable name and subject, only allow sage email
sageOnlyEmail: false, //only allow sage email
early404: true, //delete threads beyond the first 1/3 of pages with less than 5 replies
@ -318,7 +335,7 @@ module.exports = {
flags: false, //show geo flags, requires nginx setup
userPostDelete: true, //allow users to delete their posts
userPostSpoiler: true, //allow user to spoiler their post files
userPostUnlink: true, //alow user to unlink files fomr their post
userPostUnlink: true, //allow user to unlink files from their post
threadLimit: 200,
replyLimit: 500,
bumpLimit: 500,

@ -147,7 +147,7 @@ server {
}
# Files (image, video, audio, other)
location ~* \.(png|jpg|jpeg|bmp|gif|apng|webp|jfif|pjpeg|mkv|mp4|webm|mov|svg|mp3|ogg|wav|opus)$ {
location ~* \.(png|jpg|jpeg|bmp|gif|apng|webp|jfif|pjpeg|mkv|mp4|webm|mov|svg|flac|mp3|ogg|wav|opus)$ {
access_log off;
expires max;
root /path/to/jschan/static;

@ -147,7 +147,7 @@ server {
}
# Files (image, video, audio, other)
location ~* \.(png|jpg|jpeg|bmp|gif|apng|webp|pjpeg|jfif|mkv|mp4|webm|mov|svg|mp3|ogg|wav|opus)$ {
location ~* \.(png|jpg|jpeg|bmp|gif|apng|webp|pjpeg|jfif|mkv|mp4|webm|mov|svg|flac|mp3|ogg|wav|opus)$ {
access_log off;
expires max;
root /path/to/jschan/static;

@ -103,7 +103,7 @@ location ~* \.js$ {
}
# Files (image, video, audio, other)
location ~* \.(png|jpg|jpeg|webmanifest|apng|bmp|webp|pjpeg|jfif|gif|mp4|webm|mov|mkv|svg|mp3|ogg|wav|opus)$ {
location ~* \.(png|jpg|jpeg|webmanifest|apng|bmp|webp|pjpeg|jfif|gif|mp4|webm|mov|mkv|svg|flac|mp3|ogg|wav|opus)$ {
access_log off;
expires max;
root /path/to/jschan/static;

@ -103,7 +103,7 @@ location ~* \.js$ {
}
# Files (image, video, audio, other)
location ~* \.(png|jpg|jpeg|webmanifest|apng|bmp|webp|pjpeg|jfif|gif|mp4|webm|mov|mkv|svg|mp3|ogg|wav|opus)$ {
location ~* \.(png|jpg|jpeg|webmanifest|apng|bmp|webp|pjpeg|jfif|gif|mp4|webm|mov|mkv|svg|flac|mp3|ogg|wav|opus)$ {
access_log off;
expires max;
root /path/to/jschan/static;

@ -12,23 +12,25 @@ module.exports = async (req, res, next) => {
//make sure they checked 1-10 posts
if (!req.body.checkedposts || req.body.checkedposts.length === 0) {
errors.push("You didn't check any posts");
} else if (res.locals.permLevel >= 4 && req.body.checkedposts.length > 10) {
errors.push('Must select <10 posts per action');
} else if (req.body.checkedposts.length > 50) {
errors.push('Must select <50 posts per action');
errors.push('Must select at least one post');
} else if (res.locals.permLevel >= 4 && globalLimits.multiInputs.posts.anon
&& req.body.checkedposts.length > globalLimits.multiInputs.posts.anon) {
errors.push(`Must not select >${globalLimits.multiInputs.posts.anon} posts per request`);
} else if (globalLimits.multiInputs.posts.staff
&& req.body.checkedposts.length > globalLimits.multiInputs.posts.staff) {
errors.push(`Must not select >${globalLimits.multiInputs.posts.staff} posts per request`);
}
//checked reports
if (req.body.checkedreports) {
if (!req.body.report_ban) {
errors.push('Must select a report action if checked reports');
}
if (req.body.checkedreports.length > 50) {
//50 because checked posts is max 10 and 5 reports max per post
errors.push('Cannot check more than 50 reports');
if (req.body.checkedreports.length > req.body.checkedposts.length*5) {
//5 reports max per post
errors.push('Invalid number of reports checked');
}
} else if (!req.body.checkedreports && req.body.report_ban) {
errors.push('Must select posts+reports to report ban');
errors.push('Must select post and reports to ban reporter');
}
res.locals.actions = actionChecker(req);
@ -111,6 +113,7 @@ module.exports = async (req, res, next) => {
return res.render('editpost', {
'post': res.locals.posts[0],
'csrf': req.csrfToken(),
'referer': (req.headers.referer || `/${res.locals.posts[0].board}/manage/thread/${res.locals.posts[0].thread || res.locals.posts[0].postId}.html`) + `#${res.locals.posts[0].postId}`,
});
} else if (req.body.move) {
res.locals.posts = res.locals.posts.filter(p => {

@ -111,12 +111,6 @@ module.exports = async (req, res, next) => {
if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) {
errors.push('Invalid captcha mode');
}
if (typeof req.body.tph_trigger === 'number' && (req.body.tph_trigger < 0 || req.body.tph_trigger > 10000)) {
errors.push('Invalid tph trigger threshold');
}
if (typeof req.body.tph_trigger_action === 'number' && (req.body.tph_trigger_action < 0 || req.body.tph_trigger_action > 4)) {
errors.push('Invalid tph trigger action');
}
if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) {
errors.push('Invalid filter mode');
}
@ -130,6 +124,25 @@ module.exports = async (req, res, next) => {
errors.push('Invalid code theme');
}
if (typeof req.body.tph_trigger === 'number' && (req.body.tph_trigger < 0 || req.body.tph_trigger > 10000)) {
errors.push('Invalid tph trigger threshold');
}
if (typeof req.body.tph_trigger_action === 'number' && (req.body.tph_trigger_action < 0 || req.body.tph_trigger_action > 4)) {
errors.push('Invalid tph trigger action');
}
if (typeof req.body.pph_trigger === 'number' && (req.body.pph_trigger < 0 || req.body.pph_trigger > 10000)) {
errors.push('Invalid pph trigger threshold');
}
if (typeof req.body.pph_trigger_action === 'number' && (req.body.pph_trigger_action < 0 || req.body.pph_trigger_action > 4)) {
errors.push('Invalid pph trigger action');
}
if (typeof req.body.lock_reset === 'number' && (req.body.lock_reset < 0 || req.body.lock_reset > 2)) {
errors.push('Invalid trigger reset lock');
}
if (typeof req.body.captcha_reset === 'number' && (req.body.captcha_reset < 0 || req.body.captcha_reset > 2)) {
errors.push('Invalid trigger reset captcha');
}
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',

@ -7,8 +7,8 @@ module.exports = async (req, res, next) => {
const errors = [];
if (!req.body.checkedbanners || req.body.checkedbanners.length === 0 || req.body.checkedbanners.length > 10) {
errors.push('Must select 1-10 banners to delete');
if (!req.body.checkedbanners || req.body.checkedbanners.length === 0) {
errors.push('Must select at least one banner to delete');
}
if (errors.length > 0) {

@ -7,8 +7,8 @@ module.exports = async (req, res, next) => {
const errors = [];
if (!req.body.checkednews || req.body.checkednews.length === 0 || req.body.checkednews.length > 10) {
errors.push('Must select 1-10 newsposts delete');
if (!req.body.checkednews || req.body.checkednews.length === 0) {
errors.push('Must select at least one newspost to delete');
}
if (errors.length > 0) {

@ -7,9 +7,9 @@ module.exports = async (req, res, next) => {
const errors = [];
if (!req.body.checkedaccounts || req.body.checkedaccounts.length === 0 || req.body.checkedaccounts.length > 10) {
errors.push('Must select 1-10 accounts');
}
if (!req.body.checkedaccounts || req.body.checkedaccounts.length === 0) {
errors.push('Must select at least one account');
}
if (typeof req.body.auth_level !== 'number' && !req.body.delete_account) {
errors.push('Missing auth level or delete action');
}

@ -8,8 +8,8 @@ module.exports = async (req, res, next) => {
const errors = [];
if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) {
errors.push('Must select 1-10 bans')
if (!req.body.checkedbans || req.body.checkedbans.length === 0) {
errors.push('Must select at least one ban');
}
if (!req.body.option || (req.body.option !== 'unban' && req.body.option !== 'deny_appeal')) {
errors.push('Invalid ban action')

@ -10,9 +10,11 @@ module.exports = async (req, res, next) => {
const errors = [];
//make sure they checked 1-10 posts
if (!req.body.globalcheckedposts || req.body.globalcheckedposts.length === 0 || req.body.globalcheckedposts.length > 100) {
errors.push('Must select <100 posts')
if (!req.body.globalcheckedposts || req.body.globalcheckedposts.length === 0) {
errors.push(`Must select at least one post`);
} else if (globalLimits.multiInputs.posts.staff
&& req.body.globalcheckedposts.length > globalLimits.multiInputs.posts.staff) {
errors.push(`Must not select >${globalLimits.multiInputs.posts.staff} posts per request`);
}
//checked reports
@ -20,13 +22,13 @@ module.exports = async (req, res, next) => {
if (!req.body.global_report_ban) {
errors.push('Must select a report action if checked reports');
}
if (req.body.checkedreports.length > 50) {
//50 because checked posts is max 10 and 5 reports max per post
errors.push('Cannot check more than 50 reports');
if (req.body.checkedreports.length > req.body.globalcheckedposts.length*5) {
//5 reports max per post
errors.push('Invalid number of reports checked');
}
} else if (!req.body.checkedreports && req.body.global_report_ban) {
errors.push('Must select posts+reports to report ban');
}
errors.push('Must select posts+reports to report ban');
}
res.locals.actions = actionChecker(req);
@ -77,6 +79,7 @@ module.exports = async (req, res, next) => {
return res.render('editpost', {
'post': res.locals.posts[0],
'csrf': req.csrfToken(),
'referer': (req.headers.referer || `/${res.locals.posts[0].board}/manage/thread/${res.locals.posts[0].thread || res.locals.posts[0].postId}.html`) + `#${res.locals.posts[0].postId}`,
});
}

@ -12,9 +12,9 @@ module.exports = async (req, res, next) => {
if (res.locals.numFiles === 0) {
errors.push('Must provide a file');
} else if (res.locals.numFiles > globalLimits.bannerFiles.max) {
errors.push(`Exceeded max banner uploads in one request of ${globalLimits.bannerFiles.max}`)
} else if (res.locals.board.banners.length+res.locals.numFiles > 100) {
errors.push('Number of uploads would exceed 100 banner limit');
errors.push(`Exceeded max banner uploads in one request of ${globalLimits.bannerFiles.max}`);
} else if (res.locals.board.banners.length+res.locals.numFiles > globalLimits.bannerFiles.total) {
errors.push(`Total number of banners would exceed global limit of ${globalLimits.bannerFiles.total}`);
}
if (errors.length > 0) {

@ -1,7 +1,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('accounts')
, db = Mongo.db.collection('accounts')
, bcrypt = require('bcrypt')
, cache = require(__dirname+'/../redis.js');

@ -2,7 +2,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('bans');
, db = Mongo.db.collection('bans');
module.exports = {

@ -3,7 +3,7 @@
const Mongo = require(__dirname+'/db.js')
, cache = require(__dirname+'/../redis.js')
, dynamicResponse = require(__dirname+'/../helpers/dynamic.js')
, db = Mongo.client.db('jschan').collection('boards');
, db = Mongo.db.collection('boards');
module.exports = {
@ -291,15 +291,11 @@ module.exports = {
}, {
'$project': {
'_id': 1,
'lockMode': {
'new': '$settings.lockMode',
'old': '$preTriggerMode.lockMode'
},
'captchaMode': {
'new': '$settings.captchaMode',
'old': '$preTriggerMode.captchaMode'
},
'threadLimit': '$settings.threadLimit'
'lockMode': '$settings.lockMode',
'lockReset': '$settings.lockReset',
'captchaMode': '$settings.captchaMode',
'captchaReset': '$settings.captchaReset',
'threadLimit': '$settings.threadLimit',
}
}
]).toArray();

@ -2,7 +2,7 @@
const Mongo = require(__dirname+'/db.js')
, { blockBypass } = require(__dirname+'/../configs/main.js')
, db = Mongo.client.db('jschan').collection('bypass');
, db = Mongo.db.collection('bypass');
module.exports = {

@ -1,7 +1,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('captcha');
, db = Mongo.db.collection('captcha');
module.exports = {

@ -14,11 +14,11 @@ module.exports = {
useNewUrlParser: true,
useUnifiedTopology: true
});
module.exports.db = module.exports.client.db(configs.dbName);
},
checkVersion: async() => {
const currentVersion = await module.exports.client
.db('jschan')
const currentVersion = await module.exports.db
.collection('version')
.findOne({ '_id': 'version' })
.then(res => res.version);

@ -3,7 +3,7 @@
const Mongo = require(__dirname+'/db.js')
, Boards = require(__dirname+'/boards.js')
, formatSize = require(__dirname+'/../helpers/files/formatsize.js')
, db = Mongo.client.db('jschan').collection('files')
, db = Mongo.db.collection('files')
module.exports = {

@ -1,7 +1,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('modlog');
, db = Mongo.db.collection('modlog');
module.exports = {

@ -2,7 +2,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('news');
, db = Mongo.db.collection('news');
module.exports = {

@ -3,7 +3,7 @@
const Mongo = require(__dirname+'/db.js')
, Boards = require(__dirname+'/boards.js')
, Stats = require(__dirname+'/stats.js')
, db = Mongo.client.db('jschan').collection('posts')
, db = Mongo.db.collection('posts')
, { quoteLimit, previewReplies, stickyPreviewReplies
, early404Replies, early404Fraction } = require(__dirname+'/../configs/main.js');

@ -1,7 +1,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('ratelimit');
, db = Mongo.db.collection('ratelimit');
module.exports = {

@ -2,7 +2,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('poststats');
, db = Mongo.db.collection('poststats');
module.exports = {

@ -1,7 +1,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('webring');
, db = Mongo.db.collection('webring');
module.exports = {

@ -483,7 +483,7 @@ th {
td, th {
padding: 5px;
/*word-break: break-all;*/
word-break: break-word;
overflow-wrap: anywhere;
}
.flex-center {
@ -955,6 +955,7 @@ input:invalid, textarea:invalid {
padding: 5px;
padding-left: 3px;
display:block;
overflow-wrap: break-word;
}
.post-container.op .post-info {
@ -1156,6 +1157,7 @@ table.boardtable td:nth-child(3), table.boardtable th:nth-child(3),
table.boardtable td:nth-child(4), table.boardtable th:nth-child(4),
table.boardtable td:nth-child(5), table.boardtable th:nth-child(5) {
word-break: keep-all;
overflow-wrap: break-word;
}
table.boardtable th:nth-child(6) {

@ -82,7 +82,7 @@ class CaptchaController {
addMissingCaptcha() {
const postSubmitButton = document.getElementById('submitpost');
const captchaFormSectionHtml = captchaformsection();
const captchaFormSectionHtml = captchaformsection({ captchaGridSize });
postSubmitButton.insertAdjacentHTML('beforebegin', captchaFormSectionHtml);
const captchaFormSection = postSubmitButton.previousSibling;
const captchaField = captchaFormSection.querySelector('.captchafield');
@ -100,10 +100,14 @@ class CaptchaController {
refreshDiv.classList.add('captcharefresh', 'noselect');
refreshDiv.addEventListener('click', (e) => this.refreshCaptchas(e), true);
refreshDiv.textContent = '↻';
field.placeholder = 'loading';
if (captchaType === 'text') {
field.placeholder = 'loading';
}
captchaImg.src = '/captcha';
captchaImg.onload = () => {
field.placeholder = 'Captcha text';
if (captchaType === 'text') {
field.placeholder = 'Captcha text';
}
captchaDiv.appendChild(captchaImg);
captchaDiv.appendChild(refreshDiv);
this.startRefreshTimer();

@ -165,8 +165,18 @@ window.addEventListener('DOMContentLoaded', (event) => {
expandedElement.controls = 'true';
source = document.createElement('source');
expandedElement.appendChild(source);
expandedElement.style.minWidth = fileAnchor.offsetWidth+'px';
expandedElement.style.minHeight = fileAnchor.offsetHeight+'px';
if (type === 'audio' && thumbElement.nodeName === 'IMG') {
expandedElement.style.backgroundImage =
`url("${encodeURI(thumbElement.src)}")`;
expandedElement.style.backgroundRepeat = 'no-repeat';
expandedElement.style.backgroundPosition = 'top';
expandedElement.style.backgroundSize = 'contain';
expandedElement.style.minWidth = thumbElement.width+'px';
expandedElement.style.paddingTop = thumbElement.height+'px';
} else {
expandedElement.style.minWidth = fileAnchor.offsetWidth+'px';
expandedElement.style.minHeight = fileAnchor.offsetHeight+'px';
}
pfs.appendChild(expandedElement);
fileAnchor.appendChild(closeSpan);
toggle(thumbElement, expandedElement, fileName, pfs);

@ -45,7 +45,7 @@ async function wipe() {
const Mongo = require(__dirname+'/db/db.js')
const Redis = require(__dirname+'/redis.js')
await Mongo.connect();
const db = Mongo.client.db('jschan');
const db = Mongo.db;
//make these because mongo is dumb and doesnt make them automatically
await db.createCollection('accounts');
@ -256,6 +256,7 @@ function scripts() {
const locals = `const themes = ['${themes.join("', '")}'];
const codeThemes = ['${codeThemes.join("', '")}'];
const captchaType = '${configs.captchaOptions.type}';
const captchaGridSize = ${configs.captchaOptions.grid.size};
const SERVER_TIMEZONE = '${Intl.DateTimeFormat().resolvedOptions().timeZone}';
const settings = ${JSON.stringify(configs.frontendScriptDefault)};
`;
@ -293,7 +294,7 @@ const settings = ${JSON.stringify(configs.frontendScriptDefault)};
`!${paths.scripts.src}/timezone.js`,
])
.pipe(concat('all.js'))
// .pipe(uglify({compress:false}))
.pipe(uglify({compress:false}))
.pipe(gulp.dest(paths.scripts.dest));
return gulp.src([
`${paths.scripts.src}/hidefileinput.js`,
@ -304,7 +305,7 @@ const settings = ${JSON.stringify(configs.frontendScriptDefault)};
`${paths.scripts.src}/time.js`,
])
.pipe(concat('render.js'))
// .pipe(uglify({compress:false}))
.pipe(uglify({compress:false}))
.pipe(gulp.dest(paths.scripts.dest));
}
@ -313,7 +314,7 @@ 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');
const db = Mongo.db;
//get current version from db if present (set in 'reset' task in recent versions)
let currentVersion = await db.collection('version').findOne({

@ -0,0 +1,22 @@
const ffmpeg = require('fluent-ffmpeg')
, { thumbSize } = require(__dirname+'/../../configs/main.js')
, uploadDirectory = require(__dirname+'/uploadDirectory.js');
module.exports = (file) => {
return new Promise((resolve, reject) => {
ffmpeg(`${uploadDirectory}/file/${file.filename}`)
.on('end', () => {
return resolve();
})
.on('error', function(err, stdout, stderr) {
return reject(err);
})
.complexFilter([{
filter: 'showwavespic',
options: { split_channels: 1, s: `${thumbSize}x${thumbSize}` }
}])
.save(`${uploadDirectory}/file/thumb-${file.hash}${file.thumbextension}`);
});
};

@ -25,6 +25,7 @@ const video = new Set([
]);
const audio = new Set([
'audio/flac',
'audio/mp3',
'audio/mpeg',
'audio/ogg',

@ -5,7 +5,8 @@ const { ObjectId } = require(__dirname+'/../db/db.js')
'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts', 'countries']) //only these should be arrays, since express bodyparser can output arrays
, trimFields = ['tags', 'uri', 'moderators', 'filters', 'announcement', 'description', 'message',
'name', 'subject', 'email', 'postpassword', 'password', 'default_name', 'report_reason', 'ban_reason', 'log_message', 'custom_css'] //trim if we dont want filed with whitespace
, numberFields = ['filter_mode', 'lock_mode', 'message_r9k_mode', 'file_r9k_mode', 'captcha_mode', 'tph_trigger', 'pph_trigger', 'trigger_action', 'bump_limit', 'reply_limit', 'move_to_thread',, 'postId',
, numberFields = ['lock_reset', 'captcha_reset', 'filter_mode', 'lock_mode', 'message_r9k_mode', 'file_r9k_mode', 'captcha_mode',
'tph_trigger', 'pph_trigger', 'pph_trigger_action', 'tph_trigger_action', 'bump_limit', 'reply_limit', 'move_to_thread', 'postId',
'max_files', 'thread_limit', 'thread', 'max_thread_message_length', 'max_reply_message_length', 'min_thread_message_length', 'min_reply_message_length', 'auth_level'] //convert these to numbers before they hit our routes
, banDurationRegex = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+mo)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?(?<MINUTE>[\d]+m)?(?<SECOND>[\d]+s)?$/
, timeUtils = require(__dirname+'/timeutils.js')

@ -25,13 +25,13 @@ const greentextRegex = /^&gt;((?!&gt;\d+|&gt;&gt;&#x2F;\w+(&#x2F;\d*)?).*)/gm
{ 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: 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: linkRegex, cb: require(__dirname+'/linkmatch.js') },
{ regex: detectedRegex, cb: (match, detected) => `<span class='detected'>${detected}</span>` },
{ regex: detectedRegex, cb: (match, detected) => `<span class='detected'>${detected}</span>` },
{ regex: diceroll.regexMarkdown, cb: diceroll.markdown },
];

@ -246,22 +246,24 @@ module.exports = {
},
'update': {
'$set': {
'settings.lockMode': p.lockMode.old,
'settings.captchaMode': p.captchaMode.old
/* reset=0 is "no change", the options go from 0-2, and get reset to 0 or 1,
so if >0, we subtract 1 otherwise no change */
'settings.lockMode': (p.lockReset > 0 ? Math.min(p.lockReset-1, p.lockMode) : p.lockMode),
'settings.captchaMode': (p.captchaReset > 0 ? Math.min(p.captchaReset-1, p.captchaMode) : p.captchaMode),
}
}
}
}
})
});
await Boards.db.bulkWrite(bulkWrites);
const promises = [];
triggerModes.forEach(async (p) => {
await cache.del(`board:${p._id}`);
if (p.captchaMode.old < p.captchaMode.new) {
if (p.captchaMode.old === 2) {
if (p.captchaReset > 0 && p.captchaReset-1 < p.captchaMode) {
if (p.captchaReset-1 <= 1) {
promises.push(remove(`${uploadDirectory}/html/${p._id}/thread/`));
}
if (p.captchaMode.old === 0) {
if (p.captchaReset-1 === 0) {
buildQueue.push({
'task': 'buildBoardMultiple',
'options': {
@ -282,7 +284,6 @@ module.exports = {
await Promise.all(promises);
const end = process.hrtime(start);
debugLogs && console.log(timeDiffString(label, end));
},
buildChangePassword: async () => {

@ -16,4 +16,5 @@ module.exports = {
'0.0.13': require(__dirname+'/migration-0.0.13.js'), //add r9k mode (files)
'0.0.14': require(__dirname+'/migration-0.0.14.js'), //add option for disable .onion file posts to board settings
'0.0.15': require(__dirname+'/migration-0.0.15.js'), //messages r9k option
'0.0.16': require(__dirname+'/migration-0.0.16.js'), //separate tph/pph triggers
}

@ -0,0 +1,25 @@
'use strict';
module.exports = async(db, redis) => {
console.log('Allow tph/pph separate triggers and resets');
await db.collection('boards').updateMany({}, {
'$rename': {
'settings.triggerAction' : 'settings.pphTriggerAction',
}
});
await db.collection('boards').updateMany({}, {
'$unset': {
'settings.resetTrigger' : '',
'preTriggerMode': '',
}
});
await db.collection('boards').updateMany({}, {
'$set': {
'settings.tphTriggerAction' : 0,
'settings.captchaReset' : 0,
'settings.lockReset' : 0,
}
});
console.log('Cleared boards cache');
await redis.deletePattern('board:*');
};

@ -93,11 +93,13 @@ module.exports = async (req, res, next) => {
'forceReplyFile': booleanSetting(req.body.force_reply_file),
'forceThreadSubject': booleanSetting(req.body.force_thread_subject),
'disableReplySubject': booleanSetting(req.body.disable_reply_subject),
'resetTrigger': booleanSetting(req.body.reset_trigger),
'captchaMode': numberSetting(req.body.captcha_mode, oldSettings.captchaMode),
'tphTrigger': numberSetting(req.body.tph_trigger, oldSettings.tphTrigger),
'tphTriggerAction': numberSetting(req.body.tph_trigger_action, oldSettings.tphTriggerAction),
'pphTrigger': numberSetting(req.body.pph_trigger, oldSettings.pphTrigger),
'triggerAction': numberSetting(req.body.trigger_action, oldSettings.triggerAction),
'pphTriggerAction': numberSetting(req.body.pph_trigger_action, oldSettings.pphTriggerAction),
'captchaReset': numberSetting(req.body.captcha_reset, oldSettings.captchaReset),
'lockReset': numberSetting(req.body.lock_reset, oldSettings.lockReset),
'threadLimit': numberSetting(req.body.thread_limit, oldSettings.threadLimit),
'replyLimit': numberSetting(req.body.reply_limit, oldSettings.replyLimit),
'bumpLimit': numberSetting(req.body.bump_limit, oldSettings.bumpLimit),
@ -134,10 +136,6 @@ module.exports = async (req, res, next) => {
await Boards.updateOne(req.params.board, {
'$set': {
'settings': newSettings,
'preTriggerMode': {
'lockMode': newSettings.lockMode,
'captchaMode': newSettings.captchaMode
}
}
});
@ -188,7 +186,9 @@ module.exports = async (req, res, next) => {
}
}
if (newSettings.theme !== oldSettings.theme
if (newSettings.name !== oldSettings.name
|| newSettings.description !== oldSettings.description
|| newSettings.theme !== oldSettings.theme
|| newSettings.codeTheme !== oldSettings.codeTheme
|| newSettings.announcement.raw !== oldSettings.announcement.raw
|| newSettings.customCss !== oldSettings.customCss) {

@ -178,7 +178,7 @@ todo: handle some more situations
dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Post edited successfully',
//redirect
'redirect': req.body.referer,
});
res.end();

@ -17,6 +17,7 @@ const path = require('path')
, imageThumbnail = require(__dirname+'/../../helpers/files/imagethumbnail.js')
, imageIdentify = require(__dirname+'/../../helpers/files/imageidentify.js')
, videoThumbnail = require(__dirname+'/../../helpers/files/videothumbnail.js')
, audioThumbnail = require(__dirname+'/../../helpers/files/audiothumbnail.js')
, ffprobe = require(__dirname+'/../../helpers/files/ffprobe.js')
, formatSize = require(__dirname+'/../../helpers/files/formatsize.js')
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
@ -25,7 +26,8 @@ const path = require('path')
, deletePosts = require(__dirname+'/deletepost.js')
, spamCheck = require(__dirname+'/../../helpers/checks/spamcheck.js')
, { checkRealMimeTypes, thumbSize, thumbExtension, videoThumbPercentage,
postPasswordSecret, strictFiltering, animatedGifThumbnails } = require(__dirname+'/../../configs/main.js')
postPasswordSecret, strictFiltering, animatedGifThumbnails,
audioThumbnails } = require(__dirname+'/../../configs/main.js')
, buildQueue = require(__dirname+'/../../queue.js')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
, { buildThread } = require(__dirname+'/../../helpers/tasks.js');
@ -47,9 +49,9 @@ module.exports = async (req, res, next) => {
let redirect = `/${req.params.board}/`
let salt = null;
let thread = null;
const { filterBanDuration, filterMode, filters, blockedCountries, resetTrigger,
const { filterBanDuration, filterMode, filters, blockedCountries, threadLimit, ids, userPostSpoiler,
lockReset, captchaReset, pphTrigger, tphTrigger, tphTriggerAction, pphTriggerAction,
maxFiles, sageOnlyEmail, forceAnon, replyLimit, disableReplySubject,
threadLimit, ids, userPostSpoiler, pphTrigger, tphTrigger, triggerAction,
captchaMode, lockMode, allowedFileTypes, flags, fileR9KMode, messageR9KMode } = res.locals.board.settings;
if (res.locals.permLevel >= 4
&& res.locals.country
@ -239,12 +241,11 @@ module.exports = async (req, res, next) => {
//type and subtype
const [type, subtype] = processedFile.mimetype.split('/');
if (type !== 'audio') { //audio doesnt need thumb
processedFile.thumbextension = thumbExtension;
}
let imageData;
let firstFrameOnly = true;
if (type === 'image') {
processedFile.thumbextension = thumbExtension;
///detect images with opacity for PNG thumbnails, set thumbextension before increment
try {
imageData = await imageIdentify(req.files.file[i].tempFilePath, null, true);
@ -278,6 +279,13 @@ module.exports = async (req, res, next) => {
firstFrameOnly = false;
processedFile.thumbextension = '.gif';
}
} else if (type === 'audio') {
if (audioThumbnails) {
// waveform has a transparent background, so force png
processedFile.thumbextension = '.png';
}
} else {
processedFile.thumbextension = thumbExtension;
}
//increment file count
@ -295,9 +303,9 @@ module.exports = async (req, res, next) => {
await moveUpload(file, processedFile.filename, 'file');
}
} else {
const existsThumb = await pathExists(`${uploadDirectory}/file/thumb-${processedFile.hash}${processedFile.thumbextension}`);
switch (type) {
case 'image': {
const existsThumb = await pathExists(`${uploadDirectory}/file/thumb-${processedFile.hash}${processedFile.thumbextension}`);
if (!existsFull) {
await moveUpload(file, processedFile.filename, 'file');
}
@ -308,7 +316,6 @@ module.exports = async (req, res, next) => {
break;
}
case 'video': {
const existsThumb = await pathExists(`${uploadDirectory}/file/thumb-${processedFile.hash}${processedFile.thumbextension}`);
//video metadata
const videoData = await ffprobe(req.files.file[i].tempFilePath, null, true);
videoData.streams = videoData.streams.filter(stream => stream.width != null); //filter to only video streams or something with a resolution
@ -351,10 +358,19 @@ module.exports = async (req, res, next) => {
const audioData = await ffprobe(req.files.file[i].tempFilePath, null, true);
processedFile.duration = audioData.format.duration;
processedFile.durationString = timeUtils.durationString(audioData.format.duration*1000);
processedFile.hasThumb = false;
processedFile.hasThumb = audioThumbnails;
if (!existsFull) {
await moveUpload(file, processedFile.filename, 'file');
}
if (audioThumbnails) {
// audio thumbnail is always thumbSize x thumbSize
processedFile.geometry = {
thumbwidth: thumbSize, thumbheight: thumbSize,
};
if (!existsThumb) {
await audioThumbnail(processedFile);
}
}
break;
}
default:
@ -362,7 +378,7 @@ module.exports = async (req, res, next) => {
}
}
if (processedFile.hasThumb === true) {
if (processedFile.hasThumb === true && type !== 'audio') {
if (processedFile.geometry.width < thumbSize && processedFile.geometry.height < thumbSize) {
//dont scale up thumbnail for smaller images
processedFile.geometry.thumbwidth = processedFile.geometry.width;
@ -463,41 +479,42 @@ module.exports = async (req, res, next) => {
const postId = await Posts.insertOne(res.locals.board, data, thread);
let enableCaptcha = false;
if (triggerAction > 0 //if trigger is enabled
&& (tphTrigger > 0 || pphTrigger > 0) //and has a threshold > 0
&& ((triggerAction < 3 && captchaMode < triggerAction) //and its triggering captcha and captcha isnt on
|| (triggerAction === 3 && lockMode < 1) //or triggering locking and board isnt locked
|| (triggerAction === 4 && lockMode < 2))) {
//read stats to check number threads in past hour
const hourPosts = await Stats.getHourPosts(res.locals.board._id);
if (hourPosts //if stats exist for this hour and its above either trigger
&& (tphTrigger > 0 && hourPosts.tph >= tphTrigger)
|| (pphTrigger > 0 && hourPosts.pph > pphTrigger)) {
//update in memory for other stuff done e.g. rebuilds
const update = {
'$set': {
'preTriggerMode': {
lockMode,
captchaMode
let enableCaptcha = false; //make this returned from some function, refactor and move the next section to another file
const pphTriggerActive = (pphTriggerAction > 0 && pphTrigger > 0);
const tphTriggerActive = (tphTriggerAction > 0 && tphTrigger > 0);
if (pphTriggerAction || tphTriggerActive) { //if a trigger is enabled
const triggerUpdate = {
'$set': {},
};
//and a setting needs to be updated
const pphTriggerUpdate = (pphTriggerAction < 3 && captchaMode < pphTriggerAction)
|| (pphTriggerAction === 3 && lockMode < 1)
|| (pphTriggerAction === 4 && lockMode < 2);
const tphTriggerUpdate = (tphTriggerAction < 3 && captchaMode < tphTriggerAction)
|| (tphTriggerAction === 3 && lockMode < 1)
|| (tphTriggerAction === 4 && lockMode < 2);
if (tphTriggerUpdate || pphTriggerUpdate) {
const hourPosts = await Stats.getHourPosts(res.locals.board._id);
const calcTriggerMode = (update, trigger, triggerAction, stat) => { //todo: move this somewhere else
if (trigger > 0 && stat >= trigger) {
//update in memory for other stuff done e.g. rebuilds
if (triggerAction < 3) {
res.locals.board.settings.captchaMode = triggerAction;
update['$set']['settings.captchaMode'] = triggerAction;
enableCaptcha = true; //todo make this also returned after moving/refactoring this
} else {
res.locals.board.settings.lockMode = triggerAction-2;
update['$set']['settings.lockMode'] = triggerAction-2;
}
return true;
}
};
if (triggerAction < 3) {
res.locals.board.settings.captchaMode = triggerAction;
update['$set']['settings.captchaMode'] = triggerAction;
enableCaptcha = true;
} else if (triggerAction === 3) {
res.locals.board.settings.lockMode = 1;
update['$set']['settings.lockMode'] = 1;
} else if (triggerAction === 4) {
res.locals.board.settings.lockMode = 2;
update['$set']['settings.lockMode'] = 2;
return false;
}
//set it in the db
await Boards.updateOne(res.locals.board._id, update);
if (resetTrigger) {
//mark the board as being triggered so we can return it to old mode after on schedule
const updatedPphTrigger = pphTriggerUpdate && calcTriggerMode(triggerUpdate, pphTrigger, pphTriggerAction, hourPosts.pph);
const updatedTphTrigger = tphTriggerUpdate && calcTriggerMode(triggerUpdate, tphTrigger, tphTriggerAction, hourPosts.tph);
if (updatedPphTrigger || updatedTphTrigger) {
//set it in the db
await Boards.updateOne(res.locals.board._id, triggerUpdate);
await cache.sadd('triggered', res.locals.board._id);
}
}

@ -6,10 +6,16 @@ module.exports = async (req, res, next) => {
let html, json;
try {
({ html, json } = await buildThread({
const buildThreadData = await buildThread({
threadId: res.locals.thread.postId,
board: res.locals.board
}));
});
/* unlikely, but postsExists middleware can be true, but this can be null if deleted. so just next() to 404
wont matter in the build-workers that call this because they dont destructure and never cause the bug */
if (!buildThreadData) {
return next();
}
({ html, json } = buildThreadData);
} catch (err) {
return next(err);
}

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

@ -60,7 +60,7 @@ module.exports = async () => {
if (webringBoards.length > 0) {
//$out from temp collection to replace webring boards
const tempCollection = Mongo.client.db('jschan').collection('tempwebring');
const tempCollection = Mongo.db.collection('tempwebring');
await tempCollection.insertMany(webringBoards);
await tempCollection.aggregate([
{ $out : 'webring' }

@ -105,16 +105,42 @@ const express = require('express')
// catch any unhandled errors
app.use((err, req, res, next) => {
let errStatus = 500;
let errMessage = 'Internal Server Error';
if (err.code === 'EBADCSRFTOKEN') {
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Invalid CSRF token'
});
errMessage = 'Invalid CSRF token';
errStatus= 403;
}
console.error(err);
return dynamicResponse(req, res, 500, 'message', {
'title': 'Internal Server Error',
'error': 'Internal Server Error', //what to put here?
if (err.type != null) {
//body-parser errors
errStatus = err.status;
switch (err.type) {
case 'charset.unsupported':
case 'entity.parse.failed':
case 'entity.verify.failed':
case 'encoding.unsupported':
case 'request.size.invalid':
case 'parameters.too.many':
//no need to give an error for every one, since these will never happen to a legit user anyway
errMessage = 'Invalid request body';
break;
case 'request.aborted':
errMessage = 'Client aborted request';
break;
case 'entity.too.large':
errMessage = 'Your upload was too large';
break;
default:
break;
}
}
if (errStatus === 500 && errMessage === 'Internal Server Error') {
//no specific/friendly error, probably something worth logging
console.error(err);
}
return dynamicResponse(req, res, errStatus, 'message', {
'title': errStatus === 500 ? 'Internal Server Error' : 'Bad Request',
'error': errMessage,
'redirect': req.headers.referer || '/'
});
})

@ -28,12 +28,12 @@ mixin catalogtile(board, post, index)
- const file = post.files[0]
if post.spoiler || file.spoiler
div.spoilerimg.catalog-thumb
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
else if file.mimetype.startsWith('audio')
div.audioimg.catalog-thumb
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
img.catalog-thumb(src=`/file/${file.filename}` width=file.geometry.width height=file.geometry.height loading='lazy')
if post.message

@ -83,12 +83,12 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
a(target='_blank' href=`/file/${file.filename}`)
if post.spoiler || file.spoiler
div.spoilerimg.file-thumb
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
else if type === 'audio'
div.audioimg.file-thumb
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
img.file-thumb(src=`/file/${file.filename}` height=file.geometry.height width=file.geometry.width loading='lazy')
- 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

@ -20,7 +20,7 @@ block content
.pages
include ../includes/boardpages.pug
+boardnav(null, false, false)
form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}actions` method='POST' enctype='application/x-www-form-urlencoded')
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)

@ -27,7 +27,7 @@ block content
option(value="bump") Bump order
option(value="date") Creation date
option(value="replies") Reply count
form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}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)

@ -8,6 +8,7 @@ block content
.form-wrapper.flex-center.mv-10
form.form-post(action='/forms/editpost' method='POST')
input(type='hidden' name='_csrf' value=csrf)
input(type='hidden' name='referer' value=referer)
input(type='hidden' name='board' value=post.board)
input(type='hidden' name='postId' value=post.postId)
.anchor(id=post.postId)

@ -34,12 +34,13 @@ block content
input(type='checkbox', name='confirm', value='true' required)
input(type='submit', value='submit')
hr(size=1)
h4.no-m-p Settings:
.form-wrapper.flexleft.mt-10
form.form-post(action=`/forms/board/${board._id}/settings` method='POST' enctype='application/x-www-form-urlencoded')
input(type='hidden' name='_csrf' value=csrf)
.row.wrap.sb
.col.mr-5
.row
h4.mv-5 Settings:
.row
.label Board name
input(type='text' name='name' value=board.settings.name)
@ -56,8 +57,41 @@ block content
.label Announcement
textarea(name='announcement' placeholder='Supports post styling') #{board.settings.announcement.raw}
.row
.label Anon Name
input(type='text' name='default_name' value=board.settings.defaultName)
.label Theme
select(name='theme')
each theme in themes
option(value=theme selected=board.settings.theme === theme) #{theme}
.row
.label Code Theme
select(name='code_theme')
each theme in codeThemes
option(value=theme selected=board.settings.codeTheme === theme) #{theme}
if globalLimits.customCss.enabled
.row
.label Custom CSS
textarea(name='custom_css' placeholder='Test first in top-right settings if you have javascript enabled' maxlength=globalLimits.customCss.max) #{board.settings.customCss}
.row
.label IDs
label.postform-style.ph-5
input(type='checkbox', name='ids', value='true' checked=board.settings.ids)
.row
.label Geo Flags
label.postform-style.ph-5
input(type='checkbox', name='flags', value='true' checked=board.settings.flags)
.row
.label SFW
label.postform-style.ph-5
input(type='checkbox', name='sfw', value='true' checked=board.settings.sfw)
.row
.label Unlist locally
label.postform-style.ph-5
input(type='checkbox', name='unlisted_local', value='true' checked=board.settings.unlistedLocal)
.row
.label Unlist from webring
label.postform-style.ph-5
input(type='checkbox', name='unlisted_webring', value='true' checked=board.settings.unlistedWebring)
.row
h4.mv-5 File Options:
.row
.label Max Files
input(type='number' name='max_files' value=board.settings.maxFiles max=globalLimits.postFiles.max)
@ -81,10 +115,6 @@ block content
.label Allow Other Files
label.postform-style.ph-5
input(type='checkbox', name='files_allow_other', value='true' checked=board.settings.allowedFileTypes.other)
.row
.label User Post Deletion
label.postform-style.ph-5
input(type='checkbox', name='user_post_delete', value='true' checked=board.settings.userPostDelete)
.row
.label User File Spoilering
label.postform-style.ph-5
@ -93,6 +123,16 @@ block content
.label User File Unlinking
label.postform-style.ph-5
input(type='checkbox', name='user_post_unlink', value='true' checked=board.settings.userPostUnlink)
.col.mr-5
.row
h4.mv-5 Post Options:
.row
.label Anon Name
input(type='text' name='default_name' value=board.settings.defaultName)
.row
.label User Post Deletion
label.postform-style.ph-5
input(type='checkbox', name='user_post_delete', value='true' checked=board.settings.userPostDelete)
.row
.label Force Anon
label.postform-style.ph-5
@ -101,7 +141,6 @@ block content
.label Sage Only Email
label.postform-style.ph-5
input(type='checkbox', name='sage_only_email', value='true' checked=board.settings.sageOnlyEmail)
.col.mr-5
.row
.label Force Thread Subject
label.postform-style.ph-5
@ -138,6 +177,8 @@ block content
.row
.label Max Reply Message Length
input(type='number' name='max_reply_message_length' value=board.settings.maxReplyMessageLength max=globalLimits.fieldLength.message)
.row
h4.mv-5 Limits:
.row
.label Thread Limit
input(type='number' name='thread_limit' value=board.settings.threadLimit min=globalLimits.threadLimit.min max=globalLimits.threadLimit.max)
@ -147,20 +188,6 @@ block content
.row
.label Bump Limit
input(type='number' name='bump_limit' value=board.settings.bumpLimit min=globalLimits.bumpLimit.min max=globalLimits.bumpLimit.max)
.row
.label IDs
label.postform-style.ph-5
input(type='checkbox', name='ids', value='true' checked=board.settings.ids)
.row
.label Geo Flags
label.postform-style.ph-5
input(type='checkbox', name='flags', value='true' checked=board.settings.flags)
.row
.label Lock Mode
select(name='lock_mode')
option(value='0', selected=board.settings.lockMode === 0) Unlocked
option(value='1', selected=board.settings.lockMode === 1) Lock thread creation
option(value='2', selected=board.settings.lockMode === 2) Lock board
.row
.label Enforce Unique Files
select(name='file_r9k_mode')
@ -173,68 +200,66 @@ block content
option(value='0', selected=board.settings.messageR9KMode === 0) Off
option(value='1', selected=board.settings.messageR9KMode === 1) Per Thread
option(value='2', selected=board.settings.messageR9KMode === 2) Board Wide
.row
.label Unlist locally
label.postform-style.ph-5
input(type='checkbox', name='unlisted_local', value='true' checked=board.settings.unlistedLocal)
.row
.label Unlist from webring
label.postform-style.ph-5
input(type='checkbox', name='unlisted_webring', value='true' checked=board.settings.unlistedWebring)
.row
.label SFW
label.postform-style.ph-5
input(type='checkbox', name='sfw', value='true' checked=board.settings.sfw)
.col.w900
.row
.label Theme
select(name='theme')
each theme in themes
option(value=theme selected=board.settings.theme === theme) #{theme}
h4.mv-5 Antispam:
.row
.label Code Theme
select(name='code_theme')
each theme in codeThemes
option(value=theme selected=board.settings.codeTheme === theme) #{theme}
if globalLimits.customCss.enabled
.row
.label Custom CSS
textarea(name='custom_css' placeholder='Test first in top-right settings if you have javascript enabled' maxlength=globalLimits.customCss.max) #{board.settings.customCss}
.label Lock Mode
select(name='lock_mode')
option(value='0', selected=board.settings.lockMode === 0) Unlocked
option(value='1', selected=board.settings.lockMode === 1) Lock thread creation
option(value='2', selected=board.settings.lockMode === 2) Lock board
.row
.label Captcha Mode
select(name='captcha_mode')
option(value='0', selected=board.settings.captchaMode === 0) No Captcha
option(value='1', selected=board.settings.captchaMode === 1) Captcha for new thread
option(value='2', selected=board.settings.captchaMode === 2) Captcha for all posts
.row
.label TPH Trigger Threshold
input(type='number', name='tph_trigger', value=board.settings.tphTrigger)
.row
.label PPH Trigger Threshold
input(type='number', name='pph_trigger', value=board.settings.pphTrigger)
.row
.label TPH/PPH Trigger Action
select(name='trigger_action')
option(value='0', selected=board.settings.triggerAction === 0) Do nothing
option(value='1', selected=board.settings.triggerAction === 1) Enable captcha for new thread
option(value='2', selected=board.settings.triggerAction === 2) Enable captcha for all posts
option(value='3', selected=board.settings.triggerAction === 3) Lock thread creation
option(value='4', selected=board.settings.triggerAction === 4) Lock board
.label PPH Trigger Action
select(name='pph_trigger_action')
option(value='0', selected=board.settings.pphTriggerAction === 0) Do nothing
option(value='1', selected=board.settings.pphTriggerAction === 1) Enable captcha for new thread
option(value='2', selected=board.settings.pphTriggerAction === 2) Enable captcha for all posts
option(value='3', selected=board.settings.pphTriggerAction === 3) Lock thread creation
option(value='4', selected=board.settings.pphTriggerAction === 4) Lock board
.row
.label Auto Reset Trigger
label.postform-style.ph-5
input(type='checkbox', name='reset_trigger', value='true' checked=board.settings.resetTrigger)
.label TPH Trigger Threshold
input(type='number', name='tph_trigger', value=board.settings.tphTrigger)
.row
.label TPH Trigger Action
select(name='tph_trigger_action')
option(value='0', selected=board.settings.tphTriggerAction === 0) Do nothing
option(value='1', selected=board.settings.tphTriggerAction === 1) Enable captcha for new thread
option(value='2', selected=board.settings.tphTriggerAction === 2) Enable captcha for all posts
option(value='3', selected=board.settings.tphTriggerAction === 3) Lock thread creation
option(value='4', selected=board.settings.tphTriggerAction === 4) Lock board
.row
.label Trigger Reset Lock Mode
select(name='lock_reset')
option(value='0', selected=board.settings.lockReset === 0) No change
option(value='1', selected=board.settings.lockReset === 1) Unlock board
option(value='2', selected=board.settings.lockReset === 2) Lock thread creation
.row
.label Trigger Reset Captcha Mode
select(name='captcha_reset')
option(value='0', selected=board.settings.captchaReset === 0) No change
option(value='1', selected=board.settings.captchaReset === 1) Captcha disabled
option(value='2', selected=board.settings.captchaReset === 2) Captcha for new thread
.row
.label Early 404
label.postform-style.ph-5
input(type='checkbox', name='early404', value='true' checked=board.settings.early404)
.row
.label Blocked Countries
include ../includes/2charisocountries.pug
.row
.label Disable .onion file posting
label.postform-style.ph-5
input(type='checkbox', name='disable_onion_file_posting', value='true' checked=board.settings.disableOnionFilePosting)
.row
.label Blocked Countries
include ../includes/2charisocountries.pug
.row
.label Filters
textarea(name='filters' placeholder='Newline separated, max 50') #{board.settings.filters.join('\n')}
@ -252,4 +277,4 @@ block content
.label Filter Auto Ban Duration
input(type='text' name='ban_duration' placeholder='e.g. 1w' value=board.settings.filterBanDuration)
input.row(type='submit', value='save settings')
input.row(type='submit', value='Save')

@ -41,7 +41,7 @@ block content
.pages
+boardnav(null, true, true)
- const uids = board.settings.ids ? new Set() : void 0;
form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}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)

Loading…
Cancel
Save