diff --git a/CHANGELOG.md b/CHANGELOG.md index a70461a4..5d73e04f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 0.11.4 + - Bugfix for the message stating how many banners were deleted when deleting banners. + - Add an option to limit the total resolution of an image/video (width*height). + ### 0.11.3 - Fix max vs total upload count in controller for uploading board assets, flags and banners. - Move css theme assets to themes/assets instead of all lumped in one folder. diff --git a/configs/nginx/snippets/jschan_clearnet_routes.conf b/configs/nginx/snippets/jschan_clearnet_routes.conf index 1568cd69..66fd9c79 100644 --- a/configs/nginx/snippets/jschan_clearnet_routes.conf +++ b/configs/nginx/snippets/jschan_clearnet_routes.conf @@ -1,5 +1,6 @@ location / { proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; @@ -16,6 +17,7 @@ location / { location @backend { proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; proxy_set_header X-Forwarded-Proto https; @@ -29,6 +31,7 @@ location @backend { location @backend-private { include /etc/nginx/snippets/security_headers_nocache.conf; proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; proxy_set_header X-Forwarded-Proto https; diff --git a/configs/nginx/snippets/jschan_loki_routes.conf b/configs/nginx/snippets/jschan_loki_routes.conf index 60c94c7d..7d0b2cf7 100644 --- a/configs/nginx/snippets/jschan_loki_routes.conf +++ b/configs/nginx/snippets/jschan_loki_routes.conf @@ -1,5 +1,6 @@ location / { proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; @@ -16,6 +17,7 @@ location / { location @backend { proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; proxy_set_header X-Forwarded-Proto http; @@ -28,6 +30,8 @@ location @backend { location @backend-private { include /etc/nginx/snippets/security_headers_nocache.conf; + proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; proxy_set_header X-Forwarded-Proto http; diff --git a/configs/nginx/snippets/jschan_tor_routes.conf b/configs/nginx/snippets/jschan_tor_routes.conf index cf7c65cd..b0856294 100644 --- a/configs/nginx/snippets/jschan_tor_routes.conf +++ b/configs/nginx/snippets/jschan_tor_routes.conf @@ -1,13 +1,12 @@ location / { proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; - proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_cache_bypass $http_upgrade; - proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; @@ -16,6 +15,7 @@ location / { location @backend { proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; proxy_set_header X-Forwarded-Proto http; @@ -29,6 +29,7 @@ location @backend { location @backend-private { include /etc/nginx/snippets/security_headers_nocache.conf; proxy_buffering off; + proxy_request_buffering off; proxy_pass http://chan$request_uri; proxy_http_version 1.1; proxy_set_header X-Forwarded-Proto http; diff --git a/configs/template.js.example b/configs/template.js.example index 4ba7cba2..1fc14baf 100644 --- a/configs/template.js.example +++ b/configs/template.js.example @@ -138,9 +138,9 @@ module.exports = { //enable the webring (also copy configs/webring.json.example -> configs/webring.json and edit) enableWebring: false, - //extension for thumbnails that do not contain transparency (png will be used) - thumbExtension: '.jpg', - //.gif images > thumbnail size will have animated .gif thumbnails, overriding thumbExtension + //extension for thumbnails + thumbExtension: '.webp', + //whether to animate gif thumbnails animatedGifThumbnails: false, //generate waveform thumbnails for audio audioThumbnails: true, @@ -262,10 +262,14 @@ module.exports = { max: 1000 }, postFiles: { //number of files in a post - max: 5 + max: 5, }, - postFilesSize: { //in bytes, 10MB default - max: 10485760 + postFilesSize: { + max: 10485760, //in bytes, 10MB default + //width*height max number, 10000*10000 default, since square vs very wids vs very tall doesnt matter, + //just the raw number of pixels e.g. 4K 3840x2160=77414400, so we give PLENTY of headroom + imageResolution: 100000000, + videoResolution: 77414400, }, bannerFiles: { width: 300, //banner image max width in px diff --git a/controllers/forms/globalsettings.js b/controllers/forms/globalsettings.js index 5daacd5e..3d6214b1 100644 --- a/controllers/forms/globalsettings.js +++ b/controllers/forms/globalsettings.js @@ -33,7 +33,7 @@ module.exports = { 'board_defaults_pph_trigger', 'board_defaults_tph_trigger_action', 'board_defaults_pph_trigger_action', 'board_defaults_captcha_reset', 'board_defaults_lock_reset', 'board_defaults_thread_limit', 'board_defaults_reply_limit', 'board_defaults_bump_limit', 'board_defaults_max_files', 'board_defaults_min_thread_message_length', 'board_defaults_min_reply_message_length', 'board_defaults_max_thread_message_length', 'board_defaults_max_reply_message_length', 'board_defaults_filter_mode', - 'board_defaults_delete_protection_count', 'frontend_script_default_tegaki_height', 'frontend_script_default_tegaki_width'] + 'board_defaults_delete_protection_count', 'frontend_script_default_tegaki_height', 'frontend_script_default_tegaki_width', 'global_limits_post_files_size_image_resolution', 'global_limits_post_files_size_video_resolution'] }), controller: async (req, res, next) => { @@ -138,6 +138,8 @@ module.exports = { { 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_post_files_size_image_resolution), expected: true, error: 'Image resolution max must be a number' }, + { result: numberBody(req.body.global_limits_post_files_size_video_resolution), expected: true, error: 'Video resolution max 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' }, diff --git a/lib/file/image/imageidentify.js b/lib/file/image/getdimensions.js similarity index 92% rename from lib/file/image/imageidentify.js rename to lib/file/image/getdimensions.js index f2ab0ac2..737fb17b 100644 --- a/lib/file/image/imageidentify.js +++ b/lib/file/image/getdimensions.js @@ -6,7 +6,7 @@ module.exports = (filename, folder, temp) => { return new Promise((resolve, reject) => { const filePath = temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`; gm(`${filePath}[0]`) //0 for first frame of gifs, much faster - .identify(function (err, data) { + .size(function (err, data) { if (err) { return reject(err); } diff --git a/migrations/0.11.4.js b/migrations/0.11.4.js new file mode 100644 index 00000000..5f44318e --- /dev/null +++ b/migrations/0.11.4.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = async(db, redis) => { + console.log('adding image/video resolution maximum setting'); + await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { + '$set': { + 'globalLimits.postFilesSize.imageResolution': 100000000, + 'globalLimits.postFilesSize.videoResolution': 77414400, + }, + }); + console.log('Clearing globalsettings cache'); + await redis.deletePattern('globalsettings'); +}; diff --git a/models/forms/changeglobalsettings.js b/models/forms/changeglobalsettings.js index 14e1fb54..bc3ea9bd 100644 --- a/models/forms/changeglobalsettings.js +++ b/models/forms/changeglobalsettings.js @@ -218,6 +218,8 @@ module.exports = async (req, res) => { }, postFilesSize: { max: numberSetting(req.body.global_limits_post_files_size_max, oldSettings.globalLimits.postFilesSize.max), + imageResolution: numberSetting(req.body.global_limits_post_files_size_image_resolution, oldSettings.globalLimits.postFilesSize.imageResolution), + videoResolution: numberSetting(req.body.global_limits_post_files_size_video_resolution, oldSettings.globalLimits.postFilesSize.videoResolution), }, bannerFiles: { width: numberSetting(req.body.global_limits_banner_files_width, oldSettings.globalLimits.bannerFiles.width), diff --git a/models/forms/makepost.js b/models/forms/makepost.js index 4ffeec2c..a13f7787 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -16,7 +16,7 @@ const { createHash, randomBytes } = require('crypto') , moveUpload = require(__dirname+'/../../lib/file/moveupload.js') , mimeTypes = require(__dirname+'/../../lib/file/mimetypes.js') , imageThumbnail = require(__dirname+'/../../lib/file/image/imagethumbnail.js') - , imageIdentify = require(__dirname+'/../../lib/file/image/imageidentify.js') + , getDimensions = require(__dirname+'/../../lib/file/image/getdimensions.js') , videoThumbnail = require(__dirname+'/../../lib/file/video/videothumbnail.js') , audioThumbnail = require(__dirname+'/../../lib/file/audio/audiothumbnail.js') , ffprobe = require(__dirname+'/../../lib/file/ffprobe.js') @@ -36,7 +36,7 @@ const { createHash, randomBytes } = require('crypto') module.exports = async (req, res) => { const { filterBanAppealable, checkRealMimeTypes, thumbSize, thumbExtension, videoThumbPercentage, - strictFiltering, animatedGifThumbnails, audioThumbnails, dontStoreRawIps } = config.get; + strictFiltering, animatedGifThumbnails, audioThumbnails, dontStoreRawIps, globalLimits } = config.get; //spam/flood check const flood = await spamCheck(req, res); @@ -249,10 +249,9 @@ module.exports = async (req, res) => { switch (type) { case 'image': { processedFile.thumbextension = thumbExtension; - ///detect images with opacity for PNG thumbnails, set thumbextension before increment - let imageData; + let imageDimensions; try { - imageData = await imageIdentify(req.files.file[i].tempFilePath, null, true); + imageDimensions = await getDimensions(req.files.file[i].tempFilePath, null, true); } catch (e) { await deleteTempFiles(req).catch(console.error); return dynamicResponse(req, res, 400, 'message', { @@ -261,15 +260,20 @@ module.exports = async (req, res) => { 'redirect': redirect }); } - if (imageData['Channel Statistics'] && imageData['Channel Statistics']['Opacity']) { - //does this depend on GM version or anything? - const opacityMaximum = imageData['Channel Statistics']['Opacity']['Maximum']; - if (opacityMaximum !== '0.00 (0.0000)') { - processedFile.thumbextension = '.png'; - } + if (Math.floor(imageDimensions.width*imageDimensions.height) > globalLimits.postFilesSize.imageResolution) { + await deleteTempFiles(req).catch(console.error); + return dynamicResponse(req, res, 400, 'message', { + 'title': 'Bad request', + 'message': `File "${req.files.file[i].name}" image resolution is too high. Width*Height must not exceed ${globalLimits.postFilesSize.imageResolution}.`, + 'redirect': redirect + }); + } + if (thumbExtension === '.jpg' && subtype === 'png') { + //avoid transparency issues for jpg thumbnails on pngs (the most common case -- for anything else, use webp thumbExtension) + processedFile.thumbextension = '.png'; } - processedFile.geometry = imageData.size; - processedFile.geometryString = imageData.Geometry; + processedFile.geometry = imageDimensions; + processedFile.geometryString = `${imageDimensions.width}x${imageDimensions.height}`; const lteThumbSize = (processedFile.geometry.height <= thumbSize && processedFile.geometry.width <= thumbSize); processedFile.hasThumb = !(mimeTypes.allowed(file.mimetype, {image: true}) @@ -278,8 +282,6 @@ module.exports = async (req, res) => { let firstFrameOnly = true; if (processedFile.hasThumb //if it needs thumbnailing && (file.mimetype === 'image/gif' //and its a gif -// && !lteThumbSize //and its big enough -> why was this a thing originally? - && (imageData['Delay'] != null || imageData['Iterations'] != null) //and its not a static gif (naive check) && animatedGifThumbnails === true)) { //and animated thumbnails for gifs are enabled firstFrameOnly = false; processedFile.thumbextension = '.gif'; @@ -301,6 +303,14 @@ module.exports = async (req, res) => { if (videoStreams.length > 0) { processedFile.thumbextension = thumbExtension; processedFile.geometry = {width: videoStreams[0].coded_width, height: videoStreams[0].coded_height}; + if (Math.floor(processedFile.geometry.width*processedFile.geometry.height) > globalLimits.postFilesSize.videoResolution) { + await deleteTempFiles(req).catch(console.error); + return dynamicResponse(req, res, 400, 'message', { + 'title': 'Bad request', + 'message': `File "${req.files.file[i].name}" video resolution is too high. Width*Height must not exceed ${globalLimits.postFilesSize.videoResolution}.`, + 'redirect': redirect + }); + } processedFile.geometryString = `${processedFile.geometry.width}x${processedFile.geometry.height}`; processedFile.hasThumb = true; await saveFull(); diff --git a/models/forms/uploadbanners.js b/models/forms/uploadbanners.js index 75c64db5..da0c73c9 100644 --- a/models/forms/uploadbanners.js +++ b/models/forms/uploadbanners.js @@ -5,7 +5,7 @@ const { remove, pathExists } = require('fs-extra') , uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js') , moveUpload = require(__dirname+'/../../lib/file/moveupload.js') , mimeTypes = require(__dirname+'/../../lib/file/mimetypes.js') - , imageIdentify = require(__dirname+'/../../lib/file/image/imageidentify.js') + , getDimensions = require(__dirname+'/../../lib/file/image/getdimensions.js') , deleteTempFiles = require(__dirname+'/../../lib/file/deletetempfiles.js') , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') , { Boards } = require(__dirname+'/../../db/') @@ -46,8 +46,8 @@ module.exports = async (req, res) => { } //300x100 check - const imageData = await imageIdentify(req.files.file[i].tempFilePath, null, true); - let geometry = imageData.size; + const imageDimensions = await getDimensions(req.files.file[i].tempFilePath, null, true); + let geometry = imageDimensions; if (Array.isArray(geometry)) { geometry = geometry[0]; } diff --git a/package-lock.json b/package-lock.json index 602ec16d..7ac5b864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jschan", - "version": "0.11.3", + "version": "0.11.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jschan", - "version": "0.11.3", + "version": "0.11.4", "license": "AGPL-3.0-only", "dependencies": { "@fatchan/express-fileupload": "^1.4.2", diff --git a/package.json b/package.json index 983cc2f4..fd28ed96 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jschan", - "version": "0.11.3", - "migrateVersion": "0.11.0", + "version": "0.11.4", + "migrateVersion": "0.11.4", "description": "", "main": "server.js", "dependencies": { diff --git a/views/pages/globalmanagesettings.pug b/views/pages/globalmanagesettings.pug index df67aac5..e19dbc91 100644 --- a/views/pages/globalmanagesettings.pug +++ b/views/pages/globalmanagesettings.pug @@ -89,9 +89,6 @@ block content .label Prune Files Immediately label.postform-style.ph-5 input(type='checkbox', name='prune_immediately', value='true' checked=settings.pruneImmediately) - .row - .label Thumbnail File Extension - input(type='text' name='thumb_extension' value=settings.thumbExtension) .row .label Fuzzy Hash Images label.postform-style.ph-5 @@ -575,6 +572,15 @@ block content .row .label Space File Name Replacement input(type='text', name='space_file_name_replacement', value=settings.spaceFileNameReplacement) + .row + .label Thumbnail File Extension + input(type='text' name='thumb_extension' value=settings.thumbExtension) + .row + .label Image Max Resolution + input(type='text' name='global_limits_post_files_size_image_resolution' value=settings.globalLimits.postFilesSize.imageResolution) + .row + .label Video Max Resolution + input(type='text' name='global_limits_post_files_size_video_resolution' value=settings.globalLimits.postFilesSize.videoResolution) .tab.tab-9 .col