Merge branch '295-custom-flags' into 'new-dev'

Resolve "custom board flags"

See merge request fatchan/jschan!219
indiachan-spamvector
Thomas Lynch 3 years ago
commit 1c3e62f7bd
  1. 6
      CHANGELOG.md
  2. 10
      configs/template.js.example
  3. 26
      controllers/forms.js
  4. 37
      controllers/forms/addflags.js
  5. 4
      controllers/forms/deletebanners.js
  6. 39
      controllers/forms/deleteflags.js
  7. 5
      controllers/forms/globalsettings.js
  8. 2
      controllers/forms/uploadbanners.js
  9. 4
      controllers/pages.js
  10. 45
      db/boards.js
  11. 24
      gulp/res/css/style.css
  12. 16
      gulp/res/js/forms.js
  13. 7
      gulp/res/js/hidefileinput.js
  14. 5
      gulp/res/js/uploaditem.js
  15. 1
      helpers/countries.js
  16. 113
      helpers/filemiddlewares.js
  17. 7
      helpers/paramconverter.js
  18. 29
      migrations/0.1.1.js
  19. 91
      models/forms/addflags.js
  20. 3
      models/forms/changeboardsettings.js
  21. 12
      models/forms/changeglobalsettings.js
  22. 4
      models/forms/create.js
  23. 2
      models/forms/deletebanners.js
  24. 3
      models/forms/deleteboard.js
  25. 28
      models/forms/deleteflags.js
  26. 15
      models/forms/makepost.js
  27. 2
      models/forms/uploadbanners.js
  28. 2
      models/pages/manage/assets.js
  29. 2
      models/pages/manage/index.js
  30. 2
      package-lock.json
  31. 6
      package.json
  32. 2
      views/includes/filelabel.pug
  33. 15
      views/includes/postform.pug
  34. 34
      views/mixins/fileform.pug
  35. 3
      views/mixins/filelabel.pug
  36. 2
      views/mixins/managenav.pug
  37. 8
      views/mixins/post.pug
  38. 7
      views/mixins/uploaditem.pug
  39. 2
      views/pages/account.pug
  40. 15
      views/pages/globalmanagesettings.pug
  41. 20
      views/pages/manageassets.pug
  42. 42
      views/pages/managebanners.pug
  43. 6
      views/pages/managesettings.pug

@ -0,0 +1,6 @@
##### 0.1.1
- Added changelog
- Version now changes with some kind of meaning
- Animated gif thumbnails no longer generate static image for images < thumbnail dimensions
- Boards management "Banners" page is now "Assets"
- Boards can have custom flags

@ -264,9 +264,16 @@ module.exports = {
max: 10, //number of banners uploadable in one request
total: 100, //max number of banners for a board
},
flagFiles: {
max: 10, //number of banners uploadable in one request
total: 100, //max number of flags for a board
},
bannerFilesSize: { //in bytes, 10MB default
max: 10485760
},
flagFilesSize: { //in bytes, 1MB default
max: 1048576
},
/* NOTE: postFilesSize and bannerFilesSize counts in bytes the amount of total data in form submission including
other fields like message, name, etc. Therefore a very long message would reduce the space left for files very slightly.
To counteract this, consider increasing postFilesSize and bannerFilesSize beyond your desired max filesize by a small margin */
@ -357,7 +364,8 @@ module.exports = {
sageOnlyEmail: false, //only allow sage email
early404: true, //delete threads beyond the first 1/3 of pages with less than 5 replies
ids: false, //show per thread poster ID based on ip
flags: false, //show geo flags, requires nginx setup
geoFlags: false, //show geo flags, requires nginx setup
customFlags: false, //show custom flags
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

@ -35,6 +35,8 @@ const express = require('express')
, deleteNewsController = require(__dirname+'/forms/deletenews.js')
, uploadBannersController = require(__dirname+'/forms/uploadbanners.js')
, deleteBannersController = require(__dirname+'/forms/deletebanners.js')
, addFlagsController = require(__dirname+'/forms/addflags.js')
, deleteFlagsController = require(__dirname+'/forms/deleteflags.js')
, boardSettingsController = require(__dirname+'/forms/boardsettings.js')
, transferController = require(__dirname+'/forms/transfer.js')
, resignController = require(__dirname+'/forms/resign.js')
@ -52,9 +54,9 @@ const express = require('express')
, logout = require(__dirname+'/../models/forms/logout.js');
//make new post
router.post('/board/:board/post', geoAndTor, fileMiddlewares.handlePostFilesEarlyTor, torPreBypassCheck, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, fileMiddlewares.handlePostFiles,
router.post('/board/:board/post', geoAndTor, fileMiddlewares.postsEarly, torPreBypassCheck, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, fileMiddlewares.posts,
paramConverter, verifyCaptcha, numFiles, blockBypassCheck, dnsblCheck, imageHashes, makePostController);
router.post('/board/:board/modpost', geoAndTor, fileMiddlewares.handlePostFilesEarlyTor, torPreBypassCheck, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), fileMiddlewares.handlePostFiles,
router.post('/board/:board/modpost', geoAndTor, fileMiddlewares.postsEarly, torPreBypassCheck, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), fileMiddlewares.posts,
paramConverter, csrf, numFiles, blockBypassCheck, dnsblCheck, makePostController); //mod post has token instead of captcha
//post actions
@ -67,14 +69,16 @@ router.post('/appeal', geoAndTor, torPreBypassCheck, processIp, useSession, sess
router.post('/editpost', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, csrf, paramConverter, Boards.bodyExists, calcPerms, hasPerms(3), editPostController);
//board management forms
router.post('/board/:board/transfer', /*geoAndTor, torPreBypassCheck, processIp,*/ useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, transferController);
router.post('/board/:board/settings', /*geoAndTor, torPreBypassCheck, processIp,*/ useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, boardSettingsController);
router.post('/board/:board/addbanners', /*geoAndTor, torPreBypassCheck, processIp,*/ useSession, sessionRefresh, fileMiddlewares.handleBannerFiles, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, numFiles, uploadBannersController); //add banners
router.post('/board/:board/deletebanners', /*geoAndTor, torPreBypassCheck, processIp,*/ useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, deleteBannersController); //delete banners
router.post('/board/:board/addcustompages', /*geoAndTor, torPreBypassCheck, processIp,*/ useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, addCustomPageController); //add banners
router.post('/board/:board/deletecustompages', /*geoAndTor, torPreBypassCheck, processIp,*/ useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, deleteCustomPageController); //delete banners
router.post('/board/:board/editbans', /*geoAndTor, torPreBypassCheck, processIp,*/ useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(3), paramConverter, editBansController); //edit bans
router.post('/board/:board/deleteboard', /*geoAndTor, torPreBypassCheck, processIp,*/ useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(config.get.deleteBoardPermLevel), deleteBoardController); //delete board
router.post('/board/:board/transfer', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, transferController);
router.post('/board/:board/settings', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, boardSettingsController);
router.post('/board/:board/addbanners', useSession, sessionRefresh, fileMiddlewares.banner, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, numFiles, uploadBannersController); //add banners
router.post('/board/:board/deletebanners', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, deleteBannersController); //delete banners
router.post('/board/:board/addflags', useSession, sessionRefresh, fileMiddlewares.flag, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, numFiles, addFlagsController); //add flags
router.post('/board/:board/deleteflags', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, deleteFlagsController); //delete flags
router.post('/board/:board/addcustompages', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, addCustomPageController); //add banners
router.post('/board/:board/deletecustompages', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(2), paramConverter, deleteCustomPageController); //delete banners
router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(3), paramConverter, editBansController); //edit bans
router.post('/board/:board/deleteboard', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, hasPerms(config.get.deleteBoardPermLevel), deleteBoardController); //delete board
//global management forms
router.post('/global/editbans', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(1), paramConverter, editBansController); //remove bans
@ -98,7 +102,7 @@ router.post('/deleteaccount', useSession, sessionRefresh, csrf, calcPerms, isLog
//removes captcha cookie, for refreshing for noscript users
router.post('/newcaptcha', newCaptcha);
//solve captcha for block bypass
router.post('/blockbypass', geoAndTor, /*torPreBypassCheck,*/ processIp, verifyCaptcha, blockBypass);
router.post('/blockbypass', geoAndTor, processIp, verifyCaptcha, blockBypass);
module.exports = router;

@ -0,0 +1,37 @@
'use strict';
const addFlags = require(__dirname+'/../../models/forms/addflags.js')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
, config = require(__dirname+'/../../config.js');
module.exports = async (req, res, next) => {
const { globalLimits } = config.get;
const errors = [];
if (res.locals.numFiles === 0) {
errors.push('Must provide a file');
} else if (res.locals.numFiles > globalLimits.flagFiles.max) {
errors.push(`Exceeded max flag uploads in one request of ${globalLimits.flagFiles.max}`);
} else if (res.locals.board.flags.length+res.locals.numFiles > globalLimits.flagFiles.total) {
errors.push(`Total number of flags would exceed global limit of ${globalLimits.flagFiles.total}`);
}
if (errors.length > 0) {
await deleteTempFiles(req).catch(e => console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html`
})
}
try {
await addFlags(req, res, next);
} catch (err) {
await deleteTempFiles(req).catch(e => console.error);
return next(err);
}
}

@ -15,7 +15,7 @@ module.exports = async (req, res, next) => {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}/manage/banners.html`
'redirect': `/${req.params.board}/manage/assets.html`
})
}
@ -24,7 +24,7 @@ module.exports = async (req, res, next) => {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': 'Invalid banners selected',
'redirect': `/${req.params.board}/manage/banners.html`
'redirect': `/${req.params.board}/manage/assets.html`
})
}
}

@ -0,0 +1,39 @@
'use strict';
const deleteFlags = require(__dirname+'/../../models/forms/deleteflags.js')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js');
module.exports = async (req, res, next) => {
const errors = [];
if (!req.body.checkedflags || req.body.checkedflags.length === 0) {
errors.push('Must select at least one flag to delete');
}
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html`
})
}
for (let i = 0; i < req.body.checkedflags.length; i++) {
if (!res.locals.board.flags[req.body.checkedflags[i]]) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': 'Invalid flags selected',
'redirect': `/${req.params.board}/manage/assets.html`
})
}
}
try {
await deleteFlags(req, res, next);
} catch (err) {
console.error(err);
return next(err);
}
}

@ -99,11 +99,14 @@ module.exports = async (req, res, next) => {
{ result: minmaxBody(req.body.global_limits_bump_limit_min, req.body.global_limits_bump_limit_max), expected: true, error: 'Global bump limit min must be less than max' },
{ result: numberBody(req.body.global_limits_post_files_max), expected: true, error: 'Post files max must be a number' },
{ result: numberBody(req.body.global_limits_post_files_size_max), expected: true, error: 'Post files size must be a number' },
{ result: numberBody(req.body.global_limits_banner_files_size_max), expected: true, error: 'Banner files size must be a number' },
{ result: numberBody(req.body.global_limits_banner_files_width, 1), expected: true, error: 'Banner files height must be a number > 0' },
{ result: numberBody(req.body.global_limits_banner_files_height, 1), expected: true, error: 'Banner files width must be a number > 0' },
{ result: numberBody(req.body.global_limits_banner_files_size_max), expected: true, error: 'Banner files size must be a number' },
{ result: numberBody(req.body.global_limits_banner_files_max), expected: true, error: 'Banner files max must be a number' },
{ result: numberBody(req.body.global_limits_banner_files_total), expected: true, error: 'Banner files total must be a number' },
{ result: numberBody(req.body.global_limits_flag_files_size_max), expected: true, error: 'Flag files size must be a number' },
{ result: numberBody(req.body.global_limits_flag_files_max), expected: true, error: 'Flag files max must be a number' },
{ result: numberBody(req.body.global_limits_flag_files_total), expected: true, error: 'Flag files total must be a number' },
{ result: numberBody(req.body.global_limits_field_length_name), expected: true, error: 'Global limit name field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_email), expected: true, error: 'Global limit email field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_subject), expected: true, error: 'Global limit subject field length must be a number' },

@ -23,7 +23,7 @@ module.exports = async (req, res, next) => {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/${req.params.board}/manage/banners.html`
'redirect': `/${req.params.board}/manage/assets.html`
})
}

@ -16,7 +16,7 @@ const express = require('express')
, csrf = require(__dirname+'/../helpers/checks/csrfmiddleware.js')
, setMinimal = require(__dirname+'/../helpers/setminimal.js')
//page models
, { manageRecent, manageReports, manageBanners, manageSettings, manageBans,
, { manageRecent, manageReports, manageAssets, manageSettings, manageBans,
manageBoard, manageThread, manageLogs, manageCatalog, manageCustomPages } = require(__dirname+'/../models/pages/manage/')
, { globalManageSettings, globalManageReports, globalManageBans, globalManageBoards,
globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/')
@ -52,7 +52,7 @@ router.get('/:board/manage/recent.(html|json)', useSession, sessionRefresh, isLo
router.get('/:board/manage/bans.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageBans);
router.get('/:board/manage/logs.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageLogs);
router.get('/:board/manage/settings.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageSettings);
router.get('/:board/manage/banners.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageBanners);
router.get('/:board/manage/assets.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageAssets);
router.get('/:board/manage/custompages.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageCustomPages);
router.get('/:board/manage/catalog.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageCatalog);
router.get('/:board/manage/:page(1[0-9]{1,}|[2-9][0-9]{0,}|index).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(3), csrf, manageBoard);

@ -98,36 +98,57 @@ module.exports = {
);
},
removeBanners: (board, filenames) => {
cache.del(`board:${board}`);
cache.del(`banners:${board}`);
addToArray: (board, key, list) => {
return db.updateOne(
{
'_id': board,
}, {
'$pullAll': {
'banners': filenames
'$push': {
[key]: {
'$each': list
}
}
}
);
},
addBanners: (board, filenames) => {
cache.del(`board:${board}`);
cache.del(`banners:${board}`);
removeFromArray: (board, key, list) => {
return db.updateOne(
{
'_id': board,
}, {
'$push': {
'banners': {
'$each': filenames
}
'$pullAll': {
[key]: list
}
}
);
},
removeBanners: (board, filenames) => {
cache.del(`board:${board}`);
cache.del(`banners:${board}`);
return module.exports.removeFromArray(board, 'banners', filenames);
},
addBanners: (board, filenames) => {
cache.del(`board:${board}`);
cache.del(`banners:${board}`);
return module.exports.addToArray(board, 'banners', filenames)
},
setFlags: (board, flags) => {
cache.del(`board:${board}`);
//could use dot notation and set flags.x for only changes? seems a bit unsafe though and couldnt have . in name
return db.updateOne({
'_id': board,
}, {
'$set': {
'flags': flags,
}
});
},
getLocalListed: async () => {
let cachedListed = await cache.sgetall('boards:listed');
if (cachedListed && cachedListed.length > 0) {

@ -896,6 +896,20 @@ input:invalid, textarea:invalid {
min-height: 100px;
}
.board-flag {
margin: 5px;
max-width: 100%;
border: 1px solid var(--post-outline-color);
width: 32px;
height: 22px;
}
#selected-flag[src^='/'] {
border: 1px solid var(--post-outline-color);
height: 22px;
width: 32px;
}
.board-description {
text-align:center;
margin: 0;
@ -1515,7 +1529,7 @@ row.wrap.sb .col {
}
.flag {
.flag, .customflag, .customflag::before {
display: inline-block;
width: 16px;
height: 11px;
@ -1526,6 +1540,14 @@ row.wrap.sb .col {
-ms-interpolation-mode: nearest-neighbor;
}
.customflag {
background: none;
}
.customflag::before {
content: ' ';
background-position:-48px -165px;
}
.flag.flag-xx {
background-position:-48px -165px;
}

@ -93,6 +93,12 @@ class formHandler {
this.fileInput.addEventListener('change', e => this.fileInputChange(e));
this.fileLabel.addEventListener('auxclick', e => this.fileLabelAuxclick(e));
}
this.customFlagInput = this.form.elements.customflag;
this.selectedFlagImage = document.getElementById('selected-flag');
if (this.customFlagInput && this.selectedFlagImage) {
this.customFlagInput.addEventListener('change', () => this.updateFlagField(), false);
this.updateFlagField();
}
this.messageBox && this.messageBox.addEventListener('keydown', e => this.controlEnterSubmit(e));
form.addEventListener('paste', e => this.paste(e));
form.addEventListener('submit', e => this.formSubmit(e));
@ -102,11 +108,12 @@ class formHandler {
const savedName = this.form.elements.name && this.form.elements.name.value;
this.form.reset();
if (this.form.elements.name) {
this.form.elements.name.value = savedName
this.form.elements.name.value = savedName;
}
if (this.form.elements.postpassword) {
this.form.elements.postpassword.value = localStorage.getItem('postpassword');
}
this.updateFlagField();
this.updateMessageBox();
this.files = [];
this.updateFilesText();
@ -116,6 +123,12 @@ class formHandler {
}
}
updateFlagField() {
if (this.customFlagInput) {
this.selectedFlagImage.src = this.customFlagInput.options[this.customFlagInput.options.selectedIndex].dataset.src || '';
}
}
controlEnterSubmit(e) {
if (e.ctrlKey && e.key === 'Enter') {
this.formSubmit(e);
@ -324,6 +337,7 @@ class formHandler {
}
const item = {
spoilers: this.fileUploadList.dataset.spoilers === 'true',
stripFilenames: this.fileUploadList.dataset.stripFilenames === 'true',
name: file.name,
hash: fileHash,
}

@ -1,9 +1,8 @@
const fileInput = document.getElementById('file');
if (fileInput) {
document.querySelectorAll('input[type="file"]').forEach(fileInput => {
//not using display: none because we still want to show the browser prompt for a "required" file
fileInput.style.position = 'absolute';
fileInput.style.border = 'none';
fileInput.style.height = '1px';
fileInput.style.width = '1px';
// fileInput.style.opacity = '0';
}
// fileInput.style.opacity = '0'; // same effect as display:none in some browsers, ugh...
});

@ -12,7 +12,10 @@ pug_html = pug_html + "\u003Cdiv class=\"row sb\"\u003E";
if (item.spoilers) {
pug_html = pug_html + "\u003Clabel\u003E\u003Cinput" + (" type=\"checkbox\" name=\"spoiler\""+pug_attr("value", item.hash, true, false)) + "\u002F\u003ESpoiler\u003C\u002Flabel\u003E";
}
pug_html = pug_html + "\u003Clabel\u003E\u003Cinput" + (" type=\"checkbox\" name=\"strip_filename\""+pug_attr("value", item.hash, true, false)) + "\u002F\u003EStrip Filename\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E";
if (item.stripFilenames) {
pug_html = pug_html + "\u003Clabel\u003E\u003Cinput" + (" type=\"checkbox\" name=\"strip_filename\""+pug_attr("value", item.hash, true, false)) + "\u002F\u003EStrip Filename\u003C\u002Flabel\u003E";
}
pug_html = pug_html + "\u003C\u002Fdiv\u003E";
}
pug_html = pug_html + "\u003C\u002Fdiv\u003E";
};

@ -22,6 +22,7 @@ countryNamesMap['LOKI'] = 'Lokinet SNApp';
module.exports = {
countryNamesMap,
countryCodes,
countryCodesSet: new Set(countryCodes),
isAnonymizer: (code) => {
return anonymizerCountryCodesSet.has(code);
},

@ -3,92 +3,67 @@
const { debugLogs } = require(__dirname+'/../configs/secrets.js')
, dynamicResponse = require(__dirname+'/dynamic.js')
, { addCallback } = require(__dirname+'/../redis.js')
, upload = require('express-fileupload');
let postFiles,
uploadLimitFunction,
handleBannerFiles,
numFilesUploadLimitFunction,
numBannersUploadLimitFunction;
const updateHandlers = () => {
const { globalLimits, filterFileNames, spaceFileNameReplacement } = require(__dirname+'/../config.js').get
uploadLimitFunction = (req, res, next) => {
, upload = require('express-fileupload')
, fileHandlers = {}
, fileSizeLimitFunction = (req, res, next) => {
return dynamicResponse(req, res, 413, 'message', {
'title': 'Payload Too Large',
'message': 'Your upload was too large',
'redirect': req.headers.referer
});
};
numFilesUploadLimitFunction = (req, res, next) => {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Too many files',
'message': res.locals.board ? `Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}`
: `Max files per request is ${globalLimits.postFiles.max}`, //because of difference in TOR body parsing, we dont populate res.locals.board at this point. something to address later.
'redirect': req.headers.referer
});
};
numBannersUploadLimitFunction = (req, res, next) => {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Too many files',
'message': `Max banners per request is ${globalLimits.bannerFiles.max}`,
'redirect': req.headers.referer
}
, updateHandlers = () => {
const { globalLimits, filterFileNames, spaceFileNameReplacement } = require(__dirname+'/../config.js').get;
['flag', 'banner', 'post'].forEach(fileType => {
//one day this will be more easy to extend
const fileSizeLimit = globalLimits[`${fileType}FilesSize`];
const fileNumLimit = globalLimits[`${fileType}Files`];
const fileNumLimitFunction = (req, res, next) => {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Too many files',
'message': (req.path.endsWith('/post') && res.locals.board) ? `Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}`
: `Max files per request is ${fileNumLimit.max}`,
'redirect': req.headers.referer
});
};
fileHandlers[fileType] = upload({
debug: debugLogs,
createParentPath: true,
safeFileNames: filterFileNames,
spaceFileNameReplacement,
preserveExtension: 4,
limits: {
totalSize: fileSizeLimit.max,
fileSize: fileSizeLimit.max,
files: fileNumLimit.max,
},
numFilesLimitHandler: fileNumLimitFunction,
limitHandler: fileSizeLimitFunction,
useTempFiles: true,
tempFileDir: __dirname+'/../tmp/'
});
module.exports[fileType] = fileHandlers[fileType];
});
};
handleBannerFiles = upload({
debug: debugLogs,
createParentPath: true,
safeFileNames: filterFileNames,
spaceFileNameReplacement,
preserveExtension: 4,
limits: {
totalSize: globalLimits.bannerFilesSize.max,
fileSize: globalLimits.bannerFilesSize.max,
files: globalLimits.bannerFiles.max
},
numFilesLimitHandler: numBannersUploadLimitFunction,
limitHandler: uploadLimitFunction,
useTempFiles: true,
tempFileDir: __dirname+'/../tmp/'
});
module.exports.handleBannerFiles = handleBannerFiles;
postFiles = upload({
debug: debugLogs,
createParentPath: true,
safeFileNames: filterFileNames,
spaceFileNameReplacement,
preserveExtension: 4,
limits: {
totalSize: globalLimits.postFilesSize.max,
fileSize: globalLimits.postFilesSize.max,
files: globalLimits.postFiles.max
},
numFilesLimitHandler: numFilesUploadLimitFunction,
limitHandler: uploadLimitFunction,
useTempFiles: true,
tempFileDir: __dirname+'/../tmp/'
});
};
updateHandlers();
addCallback('config', updateHandlers);
module.exports = {
handleBannerFiles,
handlePostFilesEarlyTor: (req, res, next) => {
banner: fileHandlers.banner,
flag: fileHandlers.flag,
posts: (req, res, next) => {
if (res.locals.anonymizer) {
return postFiles(req, res, next);
return next();
}
return next();
return fileHandlers.post(req, res, next);
},
handlePostFiles: (req, res, next) => {
postsEarly: (req, res, next) => {
if (res.locals.anonymizer) {
return next();
return fileHandlers.post(req, res, next);
}
return postFiles(req, res, next);
return next();
},
}
};

@ -3,7 +3,7 @@
const { ObjectId } = require(__dirname+'/../db/db.js')
//todo: separate these into a schema/set for differ ent routes and inject it before the controller, to prevent checkign a bunch of other shit for every post
, allowedArrays = new Set(['captcha', 'checkedcustompages', 'checkednews', 'checkedposts', 'globalcheckedposts', 'spoiler', 'strip_filename',
'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts', 'countries'])
'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts', 'checkedflags', 'countries'])
, trimFields = ['allowed_hosts', 'dnsbl_blacklists', 'other_mime_types', 'highlight_options_language_subset', 'themes', 'code_themes',
'global_limits_custom_css_filters', 'board_defaults_filters', 'filters', 'tags', 'uri', 'moderators', '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
@ -17,8 +17,9 @@ const { ObjectId } = require(__dirname+'/../db/db.js')
'lock_wait', 'prune_modlogs', 'prune_ips', 'thumb_size', 'video_thumb_percentage', 'quote_limit', 'preview_replies', 'sticky_preview_replies',
'early_404_fraction', 'early_404_replies', 'max_recent_news', 'highlight_options_threshold', 'global_limits_thread_limit_min', 'global_limits_thread_limit_max',
'global_limits_reply_limit_min', 'global_limits_reply_limit_max', 'global_limits_bump_limit_min', 'global_limits_bump_limit_max', 'global_limits_post_files_max',
'global_limits_post_files_size_max', 'global_limits_banner_files_width', 'global_limits_banner_files_height', 'global_limits_banner_files_max', 'global_limits_banner_files_total',
'global_alimits_banner_files_total', 'global_limits_banner_files_size_max', 'global_limits_field_length_name', 'global_limits_field_length_email',
'global_limits_post_files_size_max', 'global_limits_banner_files_width', 'global_limits_banner_files_height',
'global_limits_banner_files_max', 'global_limits_banner_files_total', 'global_limits_banner_files_size_max', 'global_limits_flag_files_max',
'global_limits_flag_files_total', 'global_limits_flag_files_size_max', 'global_limits_field_length_name', 'global_limits_field_length_email',
'global_limits_field_length_subject', 'global_limits_field_length_postpassword', 'global_limits_field_length_message', 'global_limits_field_length_report_reason',
'global_limits_field_length_ban_reason', 'global_limits_field_length_log_message', 'global_limits_field_length_uri', 'global_limits_field_length_boardname',
'global_limits_field_length_description', 'global_limits_multi_input_posts_anon', 'global_limits_multi_input_posts_staff', 'global_limits_custom_css_max',

@ -0,0 +1,29 @@
'use strict';
const fs = require('fs-extra')
, uploadDirectory = require(__dirname+'/../helpers/files/uploadDirectory.js');
module.exports = async(db, redis) => {
console.log('adding flags customisation');
await fs.ensureDir(`${uploadDirectory}/flag/`);
const template = require(__dirname+'/../configs/template.js.example');
await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
'$set': {
'globalLimits.flagFiles': template.globalLimits.flagFiles,
'globalLimits.flagFilesSize': template.globalLimits.flagFilesSize,
'boardDefaults.customFlags': false,
},
'$rename': {
'boardDefaults.flags': 'boardDefaults.geoFlags',
}
});
await db.collection('boards').updateMany({}, {
'$set': {
'flags': [],
'settings.customFlags': false,
},
'$rename': {
'settings.flags': 'settings.geoFlags',
}
});
};

@ -0,0 +1,91 @@
'use strict';
const path = require('path')
, { remove, pathExists } = require('fs-extra')
, config = require(__dirname+'/../../config.js')
, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
, moveUpload = require(__dirname+'/../../helpers/files/moveupload.js')
, mimeTypes = require(__dirname+'/../../helpers/files/mimetypes.js')
, imageIdentify = require(__dirname+'/../../helpers/files/imageidentify.js')
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
, { countryCodesSet } = require(__dirname+'/../../helpers/countries.js')
, { Boards } = require(__dirname+'/../../db/')
, buildQueue = require(__dirname+'/../../queue.js');
module.exports = async (req, res, next) => {
const { globalLimits, checkRealMimeTypes } = config.get;
const redirect = `/${req.params.board}/manage/assets.html`;
// check all mime types before we try saving anything
for (let i = 0; i < res.locals.numFiles; i++) {
if (!mimeTypes.allowed(req.files.file[i].mimetype, {
image: true,
animatedImage: true, //gif flags? i guess lol
video: false,
audio: false,
other: false
})) {
await deleteTempFiles(req).catch(e => console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': `Invalid file type for ${req.files.file[i].name}. Mimetype ${req.files.file[i].mimetype} not allowed.`,
'redirect': redirect
});
}
}
// check for any mismatching supposed mimetypes from the actual file mimetype
if (checkRealMimeTypes) {
for (let i = 0; i < res.locals.numFiles; i++) {
if (!(await mimeTypes.realMimeCheck(req.files.file[i]))) {
deleteTempFiles(req).catch(e => console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': `Mime type mismatch for file "${req.files.file[i].name}"`,
'redirect': redirect
});
}
}
}
const newFlags = {};
for (let i = 0; i < res.locals.numFiles; i++) {
const file = req.files.file[i];
let noExt = path.parse(file.name).name;
//match case for real country flags
if (noExt.length === 2 && countryCodesSet.has(noExt.toUpperCase())) {
noExt = noExt.toUpperCase();
}
//add to list after checking it doesnt already exist
newFlags[noExt] = file.name;
//then upload it
await moveUpload(file, file.name, `flag/${req.params.board}`);
//and delete the temp file
await remove(file.tempFilePath);
}
deleteTempFiles(req).catch(e => console.error);
const updatedFlags = { ...res.locals.board.flags, ...newFlags };
// add flags in db
await Boards.setFlags(req.params.board, updatedFlags);
/*
should we rebuild here if (overwriting country flag){}?
*/
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': `Uploaded ${res.locals.numFiles} new flags.`,
'redirect': redirect
});
}

@ -72,7 +72,8 @@ module.exports = async (req, res, next) => {
'unlistedWebring': booleanSetting(req.body.unlisted_webring),
'early404': booleanSetting(req.body.early404),
'ids': booleanSetting(req.body.ids),
'flags': booleanSetting(req.body.flags),
'geoFlags': booleanSetting(req.body.geo_flags),
'customFlags': booleanSetting(req.body.custom_flags),
'forceAnon': booleanSetting(req.body.force_anon),
'sageOnlyEmail': booleanSetting(req.body.sage_only_email),
'userPostDelete': booleanSetting(req.body.user_post_delete),

@ -162,7 +162,7 @@ module.exports = async (req, res, next) => {
early404Replies: numberSetting(req.body.early_404_replies, oldSettings.early404Replies),
maxRecentNews: numberSetting(req.body.max_recent_news, oldSettings.maxRecentNews),
filterFileNames: booleanSetting(req.body.filter_file_names, oldSettings.filterFileNames),
spaceFileNameReplacement: trimSetting(req.body.space_file_name_replacement, oldSettings.spaceFileNameReplacement),
spaceFileNameReplacement: req.body.space_file_name_replacement,
globalLimits: {
customCss: {
enabled: booleanSetting(req.body.global_limits_custom_css_enabled, oldSettings.globalLimits.customCss.enabled),
@ -198,6 +198,13 @@ module.exports = async (req, res, next) => {
bannerFilesSize: {
max: numberSetting(req.body.global_limits_banner_files_size_max, oldSettings.globalLimits.bannerFilesSize.max),
},
flagFiles: {
max: numberSetting(req.body.global_limits_flag_files_max, oldSettings.globalLimits.flagFiles.max),
total: numberSetting(req.body.global_limits_flag_files_total, oldSettings.globalLimits.flagFiles.total),
},
flagFilesSize: {
max: numberSetting(req.body.global_limits_flag_files_size_max, oldSettings.globalLimits.flagFilesSize.max),
},
fieldLength: {
name: numberSetting(req.body.global_limits_field_length_name, oldSettings.globalLimits.fieldLength.name),
email: numberSetting(req.body.global_limits_field_length_email, oldSettings.globalLimits.fieldLength.email),
@ -243,7 +250,8 @@ module.exports = async (req, res, next) => {
sageOnlyEmail: booleanSetting(req.body.board_defaults_sage_only_email, oldSettings.boardDefaults.sageOnlyEmail),
early404: booleanSetting(req.body.board_defaults_early_404, oldSettings.boardDefaults.early404),
ids: booleanSetting(req.body.board_defaults_ids, oldSettings.boardDefaults.ids),
flags: booleanSetting(req.body.board_defaults_flags, oldSettings.boardDefaults.flags),
customFlags: booleanSetting(req.body.board_defaults_custom_flags, oldSettings.boardDefaults.customFlags),
geoFlags: booleanSetting(req.body.board_defaults_geo_flags, oldSettings.boardDefaults.geoFlags),
userPostDelete: booleanSetting(req.body.board_defaults_user_post_delete, oldSettings.boardDefaults.userPostDelete),
userPostSpoiler: booleanSetting(req.body.board_defaults_user_post_spoiler, oldSettings.boardDefaults.userPostSpoiler),
userPostUnlink: booleanSetting(req.body.board_defaults_user_post_unlink, oldSettings.boardDefaults.userPostUnlink),

@ -41,6 +41,7 @@ module.exports = async (req, res, next) => {
'_id': uri,
owner,
'banners': [],
'flags': [],
'sequence_value': 1,
'pph': 0,
'ppd': 0,
@ -60,7 +61,8 @@ module.exports = async (req, res, next) => {
Accounts.addOwnedBoard(owner, uri),
ensureDir(`${uploadDirectory}/html/${uri}`),
ensureDir(`${uploadDirectory}/json/${uri}`),
ensureDir(`${uploadDirectory}/banner/${uri}`)
ensureDir(`${uploadDirectory}/banner/${uri}`),
ensureDir(`${uploadDirectory}/flag/${uri}`),
]);
return res.redirect(`/${uri}/index.html`);

@ -9,7 +9,7 @@ const { remove } = require('fs-extra')
module.exports = async (req, res, next) => {
const redirect = `/${req.params.board}/manage/banners.html`;
const redirect = `/${req.params.board}/manage/assets.html`;
//delete file of all selected banners
await Promise.all(req.body.checkedbanners.map(async filename => {

@ -24,7 +24,8 @@ module.exports = async (uri, board) => {
CustomPages.deleteBoard(uri), //custom pages for the board
remove(`${uploadDirectory}/html/${uri}/`), //html
remove(`${uploadDirectory}/json/${uri}/`), //json
remove(`${uploadDirectory}/banner/${uri}/`) //banners
remove(`${uploadDirectory}/banner/${uri}/`), //banners
remove(`${uploadDirectory}/flag/${uri}/`), //flags
]);
}

@ -0,0 +1,28 @@
'use strict';
const { remove } = require('fs-extra')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
, { Boards } = require(__dirname+'/../../db/')
module.exports = async (req, res, next) => {
const redirect = `/${req.params.board}/manage/assets.html`;
const updatedFlags = res.locals.board.flags;
//delete file of all selected flags
await Promise.all(req.body.checkedflags.map(async flagName => {
remove(`${uploadDirectory}/flag/${req.params.board}/${res.locals.board.flags[flagName]}`);
delete res.locals.board.flags[flagName];
}));
//remove from db
await Boards.setFlags(req.params.board, updatedFlags);
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': `Deleted flags.`,
'redirect': redirect
});
}

@ -54,7 +54,7 @@ module.exports = async (req, res, next) => {
const { filterBanDuration, filterMode, filters, blockedCountries, threadLimit, ids, userPostSpoiler,
lockReset, captchaReset, pphTrigger, tphTrigger, tphTriggerAction, pphTriggerAction,
maxFiles, sageOnlyEmail, forceAnon, replyLimit, disableReplySubject,
captchaMode, lockMode, allowedFileTypes, flags, fileR9KMode, messageR9KMode } = res.locals.board.settings;
captchaMode, lockMode, allowedFileTypes, customFlags, geoFlags, fileR9KMode, messageR9KMode } = res.locals.board.settings;
if (res.locals.permLevel >= 4
&& res.locals.country
&& blockedCountries.includes(res.locals.country.code)) {
@ -393,9 +393,20 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')).
// }
}
let country = null;
if (flags === true) {
if (geoFlags === true) {
country = res.locals.country;
}
if (customFlags === true) {
if (req.body.customflag && res.locals.board.flags[req.body.customflag] != null) {
//if customflags allowed, and its a valid selection
country = {
name: req.body.customflag,
code: req.body.customflag,
src: res.locals.board.flags[req.body.customflag],
custom: true, //this will help
};
}
}
let password = null;
if (req.body.postpassword) {
password = createHash('sha256').update(postPasswordSecret + req.body.postpassword).digest('base64');

@ -15,7 +15,7 @@ const path = require('path')
module.exports = async (req, res, next) => {
const { globalLimits, checkRealMimeTypes } = config.get;
const redirect = `/${req.params.board}/manage/banners.html`;
const redirect = `/${req.params.board}/manage/assets.html`;
// check all mime types before we try saving anything
for (let i = 0; i < res.locals.numFiles; i++) {

@ -4,7 +4,7 @@ module.exports = async (req, res, next) => {
res
.set('Cache-Control', 'private, max-age=5')
.render('managebanners', {
.render('manageassets', {
csrf: req.csrfToken(),
});

@ -6,7 +6,7 @@ module.exports = {
manageSettings: require(__dirname+'/settings.js'),
manageBans: require(__dirname+'/bans.js'),
manageLogs: require(__dirname+'/logs.js'),
manageBanners: require(__dirname+'/banners.js'),
manageAssets: require(__dirname+'/assets.js'),
manageBoard: require(__dirname+'/board.js'),
manageCatalog: require(__dirname+'/catalog.js'),
manageThread: require(__dirname+'/thread.js'),

2
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "jschan",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,10 +1,11 @@
{
"name": "jschan",
"version": "0.1.0",
"migrateVersion": "0.1.0",
"version": "0.1.1",
"migrateVersion": "0.1.1",
"description": "",
"main": "server.js",
"dependencies": {
"@fatchan/gulp-pug": "^4.0.1",
"bcrypt": "^5.0.1",
"bull": "^3.20.1",
"cache-pug-templates": "^2.0.3",
@ -25,7 +26,6 @@
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-less": "^4.0.1",
"@fatchan/gulp-pug": "^4.0.1",
"gulp-replace": "^1.0.0",
"gulp-uglify-es": "^2.0.0",
"highlight.js": "^10.6.0",

@ -1,2 +0,0 @@
label.jsonly.postform-style.filelabel(for='file')
| Select/Drop/Paste file#{maxFiles > 1 ? 's' : ''}

@ -1,3 +1,4 @@
include ../mixins/filelabel.pug
- const isThread = thread != null;
- const subjectRequired = (!isThread && board.settings.forceThreadSubject);
- const messageRequired = (!isThread && board.settings.forceThreadMessage) || (isThread && board.settings.forceReplyMessage);
@ -52,9 +53,9 @@ section.form-wrapper.flex-center
small Max #{maxFiles} files
small #{postFilesSize} total
span.col
include ./filelabel.pug
+filelabel('file', maxFiles)
input#file(type='file', name='file' multiple required=fileRequired )
.upload-list(data-spoilers=(board.settings.userPostSpoiler ? 'true' : 'false'))
.upload-list(data-spoilers=(board.settings.userPostSpoiler ? 'true' : 'false') data-strip-filenames='false')
if board.settings.userPostSpoiler
noscript
label.postform-style.ph-5.ml-1.fh
@ -64,6 +65,16 @@ section.form-wrapper.flex-center
section.row
.label Password
input(type='password', name='postpassword', placeholder='Password to delete/spoiler/unlink later' maxlength='50')
if modview || board.settings.customFlags === true
- const boardFlags = Object.entries(board.flags)
if boardFlags.length > 0
section.row
.label Flag
select#customflag(name='customflag')
option(value='') #{board.settings.geoFlags === true ? 'Geographic Flag' : 'None'}
each flag in boardFlags
option(value=flag[0] data-src=`/flag/${board._id}/${flag[1]}`) #{flag[0]}
img.jsonly#selected-flag
if ((board.settings.captchaMode === 1 && !isThread) || board.settings.captchaMode === 2) && !modview
if captchaType === 'text'
include ./captchasidelabel.pug

@ -0,0 +1,34 @@
include ./filelabel.pug
mixin fileform(name, max, total, addPath, deletePath, checkName, fileList, nameList, filePath, imageClass)
- const capitalName = `${name.charAt(0).toUpperCase()}${name.substring(1)}`;
h4.no-m-p Add #{capitalName}s (Max #{total})
.form-wrapper.flexleft.mt-10
form.form-post(action=addPath, enctype='multipart/form-data', method='POST')
input(type='hidden' name='_csrf' value=csrf)
.row
.label
span #{capitalName}#{max > 1 ? 's' : ''}
span.required *
if max > 1
|
|
small (Max #{max})
span.col
+filelabel(name, max)
input(id=name type='file', name='file' multiple required)
.upload-list(data-spoilers='false' data-strip-filenames='false')
input(type='submit', value='submit')
if fileList.length > 0
hr(size=1)
h4.no-m-p Delete #{capitalName}s:
.form-wrapper.flexleft.mt-10
form.form-post(action=deletePath, enctype='application/x-www-form-urlencoded', method='POST')
input(type='hidden' name='_csrf' value=csrf)
.catalog
each file, index in fileList
label.banner-check
input(type='checkbox' name=checkName value=nameList[index])
img(class=imageClass src=`${filePath}/${file}` loading='lazy')
small #{file.substring(0, file.lastIndexOf('.'))}
input(type='submit', value='delete')

@ -0,0 +1,3 @@
mixin filelabel(id, max)
label.jsonly.postform-style.filelabel(for=id)
| Select/Drop/Paste file#{max > 1 ? 's' : ''}

@ -19,6 +19,6 @@ mixin managenav(selected, upLevel)
if permLevel < 3
a(href=`${upLevel ? '../' : ''}settings.html` class=(selected === 'settings' ? 'bold' : '')) [Settings]
|
a(href=`${upLevel ? '../' : ''}banners.html` class=(selected === 'banners' ? 'bold' : '')) [Banners]
a(href=`${upLevel ? '../' : ''}assets.html` class=(selected === 'assets' ? 'bold' : '')) [Assets]
|
a(href=`${upLevel ? '../' : ''}custompages.html` class=(selected === 'custompages' ? 'bold' : '')) [Custom Pages]

@ -32,8 +32,12 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false, overboar
span.post-name #{post.name}
|
if post.country && post.country.code
span(class=`flag flag-${post.country.code.toLowerCase()}` title=post.country.name alt=post.country.name)
|
if post.country.custom === true
img.customflag(src=`/flag/${post.board}/${post.country.src}` title=post.country.name alt=' ')
|
else
span(class=`flag flag-${post.country.code.toLowerCase()}` title=post.country.name alt=post.country.name)
|
if post.tripcode
span.post-tripcode #{post.tripcode}
|

@ -10,6 +10,7 @@ mixin uploaditem(item)
label
input(type='checkbox', name='spoiler', value=item.hash)
| Spoiler
label
input(type='checkbox', name='strip_filename', value=item.hash)
| Strip Filename
if item.stripFilenames
label
input(type='checkbox', name='strip_filename', value=item.hash)
| Strip Filename

@ -64,7 +64,7 @@ block content
| ,
a(href=`/${b}/manage/settings.html`) Settings
| ,
a(href=`/${b}/manage/banners.html`) Banners
a(href=`/${b}/manage/assets.html`) Assets
| ,
a(href=`/${b}/manage/custompages.html`) Custom Pages
else

@ -529,6 +529,15 @@ block content
.row
.label Total Banners Per Board
input(type='number' name='global_limits_banner_files_total' value=settings.globalLimits.bannerFiles.total)
.row
.label Flag File Size Max
input(type='number' name='global_limits_flag_files_size_max' value=settings.globalLimits.flagFilesSize.max)
.row
.label Flags Per Upload Max
input(type='number' name='global_limits_flag_files_max' value=settings.globalLimits.flagFiles.max)
.row
.label Total Flags Per Board
input(type='number' name='global_limits_flag_files_total' value=settings.globalLimits.flagFiles.total)
.row
h4.mv-5 Webring
@ -650,7 +659,11 @@ block content
.row
.label Geo Flags
label.postform-style.ph-5
input(type='checkbox', name='board_defaults_flags', value='true' checked=settings.boardDefaults.flags)
input(type='checkbox', name='board_defaults_geo_flags', value='true' checked=settings.boardDefaults.geoFlags)
.row
.label Custom Flags
label.postform-style.ph-5
input(type='checkbox', name='board_defaults_custom_flags', value='true' checked=settings.boardDefaults.customFlags)
.row
.label User Post Deletion
label.postform-style.ph-5

@ -0,0 +1,20 @@
extends ../layout.pug
include ../mixins/managenav.pug
include ../mixins/boardheader.pug
include ../mixins/fileform.pug
block head
title /#{board._id}/ - Manage Assets
block content
+boardheader('Assets')
br
+managenav('assets')
hr(size=1)
+fileform('banner', globalLimits.bannerFiles.max, globalLimits.bannerFiles.total,
`/forms/board/${board._id}/addbanners`, `/forms/board/${board._id}/deletebanners`,
'checkedbanners', board.banners, board.banners, `/banners/${board._id}`, 'board-banner')
hr(size=1)
+fileform('flag', globalLimits.flagFiles.max, globalLimits.flagFiles.total,
`/forms/board/${board._id}/addflags`, `/forms/board/${board._id}/deleteflags`,
'checkedflags', Object.values(board.flags), Object.keys(board.flags), `/flag/${board._id}`, 'board-flag')

@ -1,42 +0,0 @@
extends ../layout.pug
include ../mixins/managenav.pug
include ../mixins/boardheader.pug
block head
title /#{board._id}/ - Manage Banners
block content
+boardheader('Banners')
br
+managenav('banners')
hr(size=1)
h4.no-m-p Add Banners (Max #{globalLimits.bannerFiles.total})
.form-wrapper.flexleft.mt-10
form.form-post(action=`/forms/board/${board._id}/addbanners`, enctype='multipart/form-data', method='POST')
input(type='hidden' name='_csrf' value=csrf)
.row
- const maxFiles = globalLimits.bannerFiles.max;
.label
span Banner#{maxFiles > 1 ? 's' : ''}
span.required *
if maxFiles > 1
|
|
small (Max #{maxFiles})
span.col
include ../includes/filelabel.pug
input#file(type='file', name='file' multiple required)
.upload-list
input(type='submit', value='submit')
if board.banners.length > 0
hr(size=1)
h4.no-m-p Delete Banners:
.form-wrapper.flexleft.mt-10
form.form-post(action=`/forms/board/${board._id}/deletebanners`, enctype='application/x-www-form-urlencoded', method='POST')
input(type='hidden' name='_csrf' value=csrf)
.catalog
each banner in board.banners
label.banner-check
input(type='checkbox' name='checkedbanners' value=banner)
img.board-banner(src=`/banner/${board._id}/${banner}` loading='lazy')
input(type='submit', value='delete')

@ -77,7 +77,11 @@ block content
.row
.label Geo Flags
label.postform-style.ph-5
input(type='checkbox', name='flags', value='true' checked=board.settings.flags)
input(type='checkbox', name='geo_flags', value='true' checked=board.settings.geoFlags)
.row
.label Custom Flags
label.postform-style.ph-5
input(type='checkbox', name='custom_flags', value='true' checked=board.settings.customFlags)
.row
.label SFW
label.postform-style.ph-5

Loading…
Cancel
Save