mirror of https://gitgud.io/fatchan/jschan.git
Resolve "custom board flags" See merge request fatchan/jschan!219indiachan-spamvector
commit
1c3e62f7bd
43 changed files with 529 additions and 179 deletions
@ -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 |
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,9 +1,8 @@ |
|||||||
const fileInput = document.getElementById('file'); |
document.querySelectorAll('input[type="file"]').forEach(fileInput => { |
||||||
if (fileInput) { |
|
||||||
//not using display: none because we still want to show the browser prompt for a "required" file
|
//not using display: none because we still want to show the browser prompt for a "required" file
|
||||||
fileInput.style.position = 'absolute'; |
fileInput.style.position = 'absolute'; |
||||||
fileInput.style.border = 'none'; |
fileInput.style.border = 'none'; |
||||||
fileInput.style.height = '1px'; |
fileInput.style.height = '1px'; |
||||||
fileInput.style.width = '1px'; |
fileInput.style.width = '1px'; |
||||||
// fileInput.style.opacity = '0';
|
// fileInput.style.opacity = '0'; // same effect as display:none in some browsers, ugh...
|
||||||
} |
}); |
||||||
|
@ -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 |
||||||
|
}); |
||||||
|
|
||||||
|
} |
@ -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 |
||||||
|
}); |
||||||
|
} |
@ -1,2 +0,0 @@ |
|||||||
label.jsonly.postform-style.filelabel(for='file') |
|
||||||
| Select/Drop/Paste file#{maxFiles > 1 ? 's' : ''} |
|
@ -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' : ''} |
@ -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') |
|
Loading…
Reference in new issue