diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..9909a086 --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/configs/template.js.example b/configs/template.js.example index deebda02..cbf1c575 100644 --- a/configs/template.js.example +++ b/configs/template.js.example @@ -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 diff --git a/controllers/forms.js b/controllers/forms.js index 5363ca69..1abac162 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -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; diff --git a/controllers/forms/addflags.js b/controllers/forms/addflags.js new file mode 100644 index 00000000..b6c55159 --- /dev/null +++ b/controllers/forms/addflags.js @@ -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); + } + +} diff --git a/controllers/forms/deletebanners.js b/controllers/forms/deletebanners.js index 14494c3a..cdad66f6 100644 --- a/controllers/forms/deletebanners.js +++ b/controllers/forms/deletebanners.js @@ -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` }) } } diff --git a/controllers/forms/deleteflags.js b/controllers/forms/deleteflags.js new file mode 100644 index 00000000..21a08bf0 --- /dev/null +++ b/controllers/forms/deleteflags.js @@ -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); + } + +} diff --git a/controllers/forms/globalsettings.js b/controllers/forms/globalsettings.js index aa9cfbb4..0d847bf1 100644 --- a/controllers/forms/globalsettings.js +++ b/controllers/forms/globalsettings.js @@ -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' }, diff --git a/controllers/forms/uploadbanners.js b/controllers/forms/uploadbanners.js index 16ddb9ae..5429c189 100644 --- a/controllers/forms/uploadbanners.js +++ b/controllers/forms/uploadbanners.js @@ -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` }) } diff --git a/controllers/pages.js b/controllers/pages.js index 69620a5a..00a3b5d0 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -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); diff --git a/db/boards.js b/db/boards.js index 1f4137da..f942c92c 100644 --- a/db/boards.js +++ b/db/boards.js @@ -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) { diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index ac9bd0f9..1aa8af86 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -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; } diff --git a/gulp/res/js/forms.js b/gulp/res/js/forms.js index 4dd5b996..c574c123 100644 --- a/gulp/res/js/forms.js +++ b/gulp/res/js/forms.js @@ -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, } diff --git a/gulp/res/js/hidefileinput.js b/gulp/res/js/hidefileinput.js index 58822ffb..c37e75de 100644 --- a/gulp/res/js/hidefileinput.js +++ b/gulp/res/js/hidefileinput.js @@ -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... +}); diff --git a/gulp/res/js/uploaditem.js b/gulp/res/js/uploaditem.js index 927e9924..aacd407e 100644 --- a/gulp/res/js/uploaditem.js +++ b/gulp/res/js/uploaditem.js @@ -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"; }; diff --git a/helpers/countries.js b/helpers/countries.js index 645d8cd5..8b837636 100644 --- a/helpers/countries.js +++ b/helpers/countries.js @@ -22,6 +22,7 @@ countryNamesMap['LOKI'] = 'Lokinet SNApp'; module.exports = { countryNamesMap, countryCodes, + countryCodesSet: new Set(countryCodes), isAnonymizer: (code) => { return anonymizerCountryCodesSet.has(code); }, diff --git a/helpers/filemiddlewares.js b/helpers/filemiddlewares.js index faa377d2..67891163 100644 --- a/helpers/filemiddlewares.js +++ b/helpers/filemiddlewares.js @@ -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(); }, -} +}; diff --git a/helpers/paramconverter.js b/helpers/paramconverter.js index cbbe0309..ca1922ba 100644 --- a/helpers/paramconverter.js +++ b/helpers/paramconverter.js @@ -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', diff --git a/migrations/0.1.1.js b/migrations/0.1.1.js new file mode 100644 index 00000000..372fc003 --- /dev/null +++ b/migrations/0.1.1.js @@ -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', + } + }); +}; diff --git a/models/forms/addflags.js b/models/forms/addflags.js new file mode 100644 index 00000000..bc987aae --- /dev/null +++ b/models/forms/addflags.js @@ -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 + }); + +} diff --git a/models/forms/changeboardsettings.js b/models/forms/changeboardsettings.js index 455a276d..3c3f2ac9 100644 --- a/models/forms/changeboardsettings.js +++ b/models/forms/changeboardsettings.js @@ -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), diff --git a/models/forms/changeglobalsettings.js b/models/forms/changeglobalsettings.js index e194e54b..104f6ad5 100644 --- a/models/forms/changeglobalsettings.js +++ b/models/forms/changeglobalsettings.js @@ -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), diff --git a/models/forms/create.js b/models/forms/create.js index c088d26d..66095c52 100644 --- a/models/forms/create.js +++ b/models/forms/create.js @@ -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`); diff --git a/models/forms/deletebanners.js b/models/forms/deletebanners.js index ccb2a066..bb6852da 100644 --- a/models/forms/deletebanners.js +++ b/models/forms/deletebanners.js @@ -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 => { diff --git a/models/forms/deleteboard.js b/models/forms/deleteboard.js index a86cd688..ece87227 100644 --- a/models/forms/deleteboard.js +++ b/models/forms/deleteboard.js @@ -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 ]); } diff --git a/models/forms/deleteflags.js b/models/forms/deleteflags.js new file mode 100644 index 00000000..d8dcacaf --- /dev/null +++ b/models/forms/deleteflags.js @@ -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 + }); +} diff --git a/models/forms/makepost.js b/models/forms/makepost.js index 9629a334..1aa325ad 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -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'); diff --git a/models/forms/uploadbanners.js b/models/forms/uploadbanners.js index 70f88202..2b013fd3 100644 --- a/models/forms/uploadbanners.js +++ b/models/forms/uploadbanners.js @@ -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++) { diff --git a/models/pages/manage/banners.js b/models/pages/manage/assets.js similarity index 83% rename from models/pages/manage/banners.js rename to models/pages/manage/assets.js index a7d179f3..50ffad8c 100644 --- a/models/pages/manage/banners.js +++ b/models/pages/manage/assets.js @@ -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(), }); diff --git a/models/pages/manage/index.js b/models/pages/manage/index.js index e439617b..1d321cba 100644 --- a/models/pages/manage/index.js +++ b/models/pages/manage/index.js @@ -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'), diff --git a/package-lock.json b/package-lock.json index ea30823e..4bbdad28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jschan", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 83826cb3..fd90eb31 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/views/includes/filelabel.pug b/views/includes/filelabel.pug deleted file mode 100644 index 8a42aad9..00000000 --- a/views/includes/filelabel.pug +++ /dev/null @@ -1,2 +0,0 @@ -label.jsonly.postform-style.filelabel(for='file') - | Select/Drop/Paste file#{maxFiles > 1 ? 's' : ''} diff --git a/views/includes/postform.pug b/views/includes/postform.pug index 338a570d..91a5f6ed 100644 --- a/views/includes/postform.pug +++ b/views/includes/postform.pug @@ -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 diff --git a/views/mixins/fileform.pug b/views/mixins/fileform.pug new file mode 100644 index 00000000..7d5b29ba --- /dev/null +++ b/views/mixins/fileform.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') diff --git a/views/mixins/filelabel.pug b/views/mixins/filelabel.pug new file mode 100644 index 00000000..d9878840 --- /dev/null +++ b/views/mixins/filelabel.pug @@ -0,0 +1,3 @@ +mixin filelabel(id, max) + label.jsonly.postform-style.filelabel(for=id) + | Select/Drop/Paste file#{max > 1 ? 's' : ''} diff --git a/views/mixins/managenav.pug b/views/mixins/managenav.pug index a3c5bfa3..031726fe 100644 --- a/views/mixins/managenav.pug +++ b/views/mixins/managenav.pug @@ -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] diff --git a/views/mixins/post.pug b/views/mixins/post.pug index 496fa3ae..aa88834d 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -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} | diff --git a/views/mixins/uploaditem.pug b/views/mixins/uploaditem.pug index e20207f1..a6da14ec 100644 --- a/views/mixins/uploaditem.pug +++ b/views/mixins/uploaditem.pug @@ -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 diff --git a/views/pages/account.pug b/views/pages/account.pug index e4232c78..00067d59 100644 --- a/views/pages/account.pug +++ b/views/pages/account.pug @@ -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 diff --git a/views/pages/globalmanagesettings.pug b/views/pages/globalmanagesettings.pug index 0081a428..f93d7d72 100644 --- a/views/pages/globalmanagesettings.pug +++ b/views/pages/globalmanagesettings.pug @@ -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 diff --git a/views/pages/manageassets.pug b/views/pages/manageassets.pug new file mode 100644 index 00000000..19f41048 --- /dev/null +++ b/views/pages/manageassets.pug @@ -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') diff --git a/views/pages/managebanners.pug b/views/pages/managebanners.pug deleted file mode 100644 index 4aad33cd..00000000 --- a/views/pages/managebanners.pug +++ /dev/null @@ -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') diff --git a/views/pages/managesettings.pug b/views/pages/managesettings.pug index 36bb5f98..36e22255 100644 --- a/views/pages/managesettings.pug +++ b/views/pages/managesettings.pug @@ -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