Merge branch 'develop' into feature/396-localisation

indiachan-spamvector
Thomas Lynch 1 year ago
commit bf3e18750e
  1. 4
      CHANGELOG.md
  2. 3
      configs/nginx/snippets/jschan_clearnet_routes.conf
  3. 4
      configs/nginx/snippets/jschan_loki_routes.conf
  4. 5
      configs/nginx/snippets/jschan_tor_routes.conf
  5. 16
      configs/template.js.example
  6. 4
      controllers/forms/globalsettings.js
  7. 2
      lib/file/image/getdimensions.js
  8. 13
      migrations/0.11.4.js
  9. 2
      models/forms/changeglobalsettings.js
  10. 7
      models/forms/deletebanners.js
  11. 40
      models/forms/makepost.js
  12. 6
      models/forms/uploadbanners.js
  13. 1622
      package-lock.json
  14. 14
      package.json
  15. 12
      views/pages/globalmanagesettings.pug

@ -22,6 +22,10 @@ Now, back to the program. Here are the changes for 1.0.0, with one especially no
- Remove showing language and relevance data when auto detecting highlighted code block language - Remove showing language and relevance data when auto detecting highlighted code block language
- More minor bugfixes to permissions pages displays. - More minor bugfixes to permissions pages displays.
### 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 ### 0.11.3
- Fix max vs total upload count in controller for uploading board assets, flags and banners. - 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. - Move css theme assets to themes/assets instead of all lumped in one folder.

@ -1,5 +1,6 @@
location / { location / {
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
@ -16,6 +17,7 @@ location / {
location @backend { location @backend {
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Proto https;
@ -29,6 +31,7 @@ location @backend {
location @backend-private { location @backend-private {
include /etc/nginx/snippets/security_headers_nocache.conf; include /etc/nginx/snippets/security_headers_nocache.conf;
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Proto https;

@ -1,5 +1,6 @@
location / { location / {
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
@ -16,6 +17,7 @@ location / {
location @backend { location @backend {
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
@ -28,6 +30,8 @@ location @backend {
location @backend-private { location @backend-private {
include /etc/nginx/snippets/security_headers_nocache.conf; include /etc/nginx/snippets/security_headers_nocache.conf;
proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;

@ -1,13 +1,12 @@
location / { location / {
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@ -16,6 +15,7 @@ location / {
location @backend { location @backend {
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
@ -29,6 +29,7 @@ location @backend {
location @backend-private { location @backend-private {
include /etc/nginx/snippets/security_headers_nocache.conf; include /etc/nginx/snippets/security_headers_nocache.conf;
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://chan$request_uri; proxy_pass http://chan$request_uri;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;

@ -140,9 +140,9 @@ module.exports = {
//enable the webring (also copy configs/webring.json.example -> configs/webring.json and edit) //enable the webring (also copy configs/webring.json.example -> configs/webring.json and edit)
enableWebring: false, enableWebring: false,
//extension for thumbnails that do not contain transparency (png will be used) //extension for thumbnails
thumbExtension: '.jpg', thumbExtension: '.webp',
//.gif images > thumbnail size will have animated .gif thumbnails, overriding thumbExtension //whether to animate gif thumbnails
animatedGifThumbnails: false, animatedGifThumbnails: false,
//generate waveform thumbnails for audio //generate waveform thumbnails for audio
audioThumbnails: true, audioThumbnails: true,
@ -264,10 +264,14 @@ module.exports = {
max: 1000 max: 1000
}, },
postFiles: { //number of files in a post postFiles: { //number of files in a post
max: 5 max: 5,
}, },
postFilesSize: { //in bytes, 10MB default postFilesSize: {
max: 10485760 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: { bannerFiles: {
width: 300, //banner image max width in px width: 300, //banner image max width in px

@ -34,7 +34,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_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_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_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) => { controller: async (req, res, next) => {
@ -142,6 +142,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: 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_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_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_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_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_size_max), expected: true, error: __('Banner files size must be a number') },

@ -6,7 +6,7 @@ module.exports = (filename, folder, temp) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const filePath = temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`; const filePath = temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`;
gm(`${filePath}[0]`) //0 for first frame of gifs, much faster gm(`${filePath}[0]`) //0 for first frame of gifs, much faster
.identify(function (err, data) { .size(function (err, data) {
if (err) { if (err) {
return reject(err); return reject(err);
} }

@ -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');
};

@ -221,6 +221,8 @@ module.exports = async (req, res) => {
}, },
postFilesSize: { postFilesSize: {
max: numberSetting(req.body.global_limits_post_files_size_max, oldSettings.globalLimits.postFilesSize.max), 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: { bannerFiles: {
width: numberSetting(req.body.global_limits_banner_files_width, oldSettings.globalLimits.bannerFiles.width), width: numberSetting(req.body.global_limits_banner_files_width, oldSettings.globalLimits.bannerFiles.width),

@ -13,14 +13,15 @@ module.exports = async (req, res) => {
const redirect = `/${req.params.board}/manage/assets.html`; const redirect = `/${req.params.board}/manage/assets.html`;
//delete file of all selected banners //delete file of all selected banners
await Promise.all(req.body.checkedbanners.map(async filename => { await Promise.all(req.body.checkedbanners.map(filename => {
remove(`${uploadDirectory}/banner/${req.params.board}/${filename}`); remove(`${uploadDirectory}/banner/${req.params.board}/${filename}`);
})); }));
//remove from db //remove from db
const amount = await Boards.removeBanners(req.params.board, req.body.checkedbanners).then(result => result.modifiedCount); await Boards.removeBanners(req.params.board, req.body.checkedbanners);
//update res locals banners in memory //update res locals banners in memory
const beforeBanners = res.locals.board.banners.length;
res.locals.board.banners = res.locals.board.banners.filter(banner => { res.locals.board.banners = res.locals.board.banners.filter(banner => {
return !req.body.checkedbanners.includes(banner); return !req.body.checkedbanners.includes(banner);
}); });
@ -35,7 +36,7 @@ module.exports = async (req, res) => {
return dynamicResponse(req, res, 200, 'message', { return dynamicResponse(req, res, 200, 'message', {
'title': __('Success'), 'title': __('Success'),
'message': __('Deleted %s banners', amount), 'message': __('Deleted %s banners', beforeBanners - res.locals.board.banners.length),
'redirect': redirect 'redirect': redirect
}); });
}; };

@ -16,7 +16,7 @@ const { createHash, randomBytes } = require('crypto')
, moveUpload = require(__dirname+'/../../lib/file/moveupload.js') , moveUpload = require(__dirname+'/../../lib/file/moveupload.js')
, mimeTypes = require(__dirname+'/../../lib/file/mimetypes.js') , mimeTypes = require(__dirname+'/../../lib/file/mimetypes.js')
, imageThumbnail = require(__dirname+'/../../lib/file/image/imagethumbnail.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') , videoThumbnail = require(__dirname+'/../../lib/file/video/videothumbnail.js')
, audioThumbnail = require(__dirname+'/../../lib/file/audio/audiothumbnail.js') , audioThumbnail = require(__dirname+'/../../lib/file/audio/audiothumbnail.js')
, ffprobe = require(__dirname+'/../../lib/file/ffprobe.js') , ffprobe = require(__dirname+'/../../lib/file/ffprobe.js')
@ -38,7 +38,7 @@ module.exports = async (req, res) => {
const { __ } = res.locals; const { __ } = res.locals;
const { filterBanAppealable, checkRealMimeTypes, thumbSize, thumbExtension, videoThumbPercentage, const { filterBanAppealable, checkRealMimeTypes, thumbSize, thumbExtension, videoThumbPercentage,
strictFiltering, animatedGifThumbnails, audioThumbnails, dontStoreRawIps } = config.get; strictFiltering, animatedGifThumbnails, audioThumbnails, dontStoreRawIps, globalLimits } = config.get;
//spam/flood check //spam/flood check
const flood = await spamCheck(req, res); const flood = await spamCheck(req, res);
@ -258,10 +258,9 @@ module.exports = async (req, res) => {
switch (type) { switch (type) {
case 'image': { case 'image': {
processedFile.thumbextension = thumbExtension; processedFile.thumbextension = thumbExtension;
///detect images with opacity for PNG thumbnails, set thumbextension before increment let imageDimensions;
let imageData;
try { try {
imageData = await imageIdentify(req.files.file[i].tempFilePath, null, true); imageDimensions = await getDimensions(req.files.file[i].tempFilePath, null, true);
} catch (e) { } catch (e) {
await deleteTempFiles(req).catch(console.error); await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
@ -270,15 +269,20 @@ module.exports = async (req, res) => {
'redirect': redirect 'redirect': redirect
}); });
} }
if (imageData['Channel Statistics'] && imageData['Channel Statistics']['Opacity']) { if (Math.floor(imageDimensions.width*imageDimensions.height) > globalLimits.postFilesSize.imageResolution) {
//does this depend on GM version or anything? await deleteTempFiles(req).catch(console.error);
const opacityMaximum = imageData['Channel Statistics']['Opacity']['Maximum']; return dynamicResponse(req, res, 400, 'message', {
if (opacityMaximum !== '0.00 (0.0000)') { 'title': 'Bad request',
processedFile.thumbextension = '.png'; '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.geometry = imageDimensions;
processedFile.geometryString = imageData.Geometry; processedFile.geometryString = `${imageDimensions.width}x${imageDimensions.height}`;
const lteThumbSize = (processedFile.geometry.height <= thumbSize const lteThumbSize = (processedFile.geometry.height <= thumbSize
&& processedFile.geometry.width <= thumbSize); && processedFile.geometry.width <= thumbSize);
processedFile.hasThumb = !(mimeTypes.allowed(file.mimetype, {image: true}) processedFile.hasThumb = !(mimeTypes.allowed(file.mimetype, {image: true})
@ -287,8 +291,6 @@ module.exports = async (req, res) => {
let firstFrameOnly = true; let firstFrameOnly = true;
if (processedFile.hasThumb //if it needs thumbnailing if (processedFile.hasThumb //if it needs thumbnailing
&& (file.mimetype === 'image/gif' //and its a gif && (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 && animatedGifThumbnails === true)) { //and animated thumbnails for gifs are enabled
firstFrameOnly = false; firstFrameOnly = false;
processedFile.thumbextension = '.gif'; processedFile.thumbextension = '.gif';
@ -310,6 +312,14 @@ module.exports = async (req, res) => {
if (videoStreams.length > 0) { if (videoStreams.length > 0) {
processedFile.thumbextension = thumbExtension; processedFile.thumbextension = thumbExtension;
processedFile.geometry = {width: videoStreams[0].coded_width, height: videoStreams[0].coded_height}; 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.geometryString = `${processedFile.geometry.width}x${processedFile.geometry.height}`;
processedFile.hasThumb = true; processedFile.hasThumb = true;
await saveFull(); await saveFull();

@ -5,7 +5,7 @@ const { remove, pathExists } = require('fs-extra')
, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js') , uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
, moveUpload = require(__dirname+'/../../lib/file/moveupload.js') , moveUpload = require(__dirname+'/../../lib/file/moveupload.js')
, mimeTypes = require(__dirname+'/../../lib/file/mimetypes.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') , deleteTempFiles = require(__dirname+'/../../lib/file/deletetempfiles.js')
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, { Boards } = require(__dirname+'/../../db/') , { Boards } = require(__dirname+'/../../db/')
@ -47,8 +47,8 @@ module.exports = async (req, res) => {
} }
//300x100 check //300x100 check
const imageData = await imageIdentify(req.files.file[i].tempFilePath, null, true); const imageDimensions = await getDimensions(req.files.file[i].tempFilePath, null, true);
let geometry = imageData.size; let geometry = imageDimensions;
if (Array.isArray(geometry)) { if (Array.isArray(geometry)) {
geometry = geometry[0]; geometry = geometry[0];
} }

1622
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{ {
"name": "jschan", "name": "jschan",
"version": "0.11.3", "version": "0.11.4",
"migrateVersion": "0.11.0", "migrateVersion": "0.11.4",
"description": "", "description": "",
"main": "server.js", "main": "server.js",
"dependencies": { "dependencies": {
@ -39,8 +39,8 @@
"imghash": "^0.0.9", "imghash": "^0.0.9",
"ioredis": "^4.28.5", "ioredis": "^4.28.5",
"ip6addr": "^0.2.5", "ip6addr": "^0.2.5",
"mongodb": "^4.13.0", "mongodb": "^4.14.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.9",
"otpauth": "^9.0.2", "otpauth": "^9.0.2",
"path": "^0.12.7", "path": "^0.12.7",
"pm2": "^5.2.2", "pm2": "^5.2.2",
@ -48,16 +48,16 @@
"pug-runtime": "^3.0.1", "pug-runtime": "^3.0.1",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"redlock": "^4.2.0", "redlock": "^4.2.0",
"sanitize-html": "^2.8.1", "sanitize-html": "^2.10.0",
"saslprep": "^1.0.3", "saslprep": "^1.0.3",
"semver": "^7.3.8", "semver": "^7.3.8",
"socket.io": "^4.5.4", "socket.io": "^4.6.1",
"socks-proxy-agent": "^6.2.1", "socks-proxy-agent": "^6.2.1",
"uid-safe": "^2.1.5", "uid-safe": "^2.1.5",
"unix-crypt-td-js": "^1.1.4" "unix-crypt-td-js": "^1.1.4"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.30.0", "eslint": "^8.36.0",
"eslint-plugin-jest": "^26.9.0", "eslint-plugin-jest": "^26.9.0",
"jest": "^27.5.1", "jest": "^27.5.1",
"jest-cli": "^27.5.1", "jest-cli": "^27.5.1",

@ -94,9 +94,6 @@ block content
.label #{__('Prune Files Immediately')} .label #{__('Prune Files Immediately')}
label.postform-style.ph-5 label.postform-style.ph-5
input(type='checkbox', name='prune_immediately', value='true' checked=settings.pruneImmediately) 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 .row
.label #{__('Fuzzy Hash Images')} .label #{__('Fuzzy Hash Images')}
label.postform-style.ph-5 label.postform-style.ph-5
@ -579,6 +576,15 @@ block content
.row .row
.label #{__('Space File Name Replacement')} .label #{__('Space File Name Replacement')}
input(type='text', name='space_file_name_replacement', value=settings.spaceFileNameReplacement) 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 .tab.tab-9
.col .col

Loading…
Cancel
Save