Merge pull request #614 from the-luap/fix/guest-upload-limits-613

fix(gallery): guest upload honours general_max_files_per_upload + i18n placeholder interpolates (#613)
This commit is contained in:
Paul Nothaft
2026-06-10 18:37:30 +02:00
committed by GitHub
4 changed files with 72 additions and 6 deletions
+19 -4
View File
@@ -1627,7 +1627,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => {
// Import multer and photo processing
const multer = require('multer');
const { getAllowedMimeTypes } = require('../services/uploadSettings');
const { getAllowedMimeTypes, getMaxFilesPerUpload } = require('../services/uploadSettings');
const { validateFileType } = require('../utils/fileSecurityUtils');
// Resolve allowed MIME types from settings
@@ -1638,11 +1638,26 @@ router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => {
allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
}
// #613 — per-batch file count was hardcoded to 10 here, so the admin's
// Settings → General → "Max Files per Upload" value silently didn't
// apply to guest uploads (only admin uploads honoured it via
// adminPhotos.js:131). Zszywany reported uploading 16 files succeeded
// even with the limit set to 10. Mirror the admin path: resolve from
// settings (cached for 60s in the service) and feed multer both
// `limits.files` and the `.array(...)` cap. Fall back to the service's
// default if the read fails.
let maxFilesPerUpload;
try {
maxFilesPerUpload = await getMaxFilesPerUpload();
} catch {
maxFilesPerUpload = 500;
}
const upload = multer({
dest: tempUploadDir,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB
files: 10 // Max 10 files at once
fileSize: 50 * 1024 * 1024, // 50MB per file (separate concern from #613)
files: maxFilesPerUpload
},
fileFilter: (req, file, cb) => {
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
@@ -1651,7 +1666,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => {
cb(new Error('Invalid file type'));
}
}
}).array('photos', 10);
}).array('photos', maxFilesPerUpload);
// Handle upload
upload(req, res, async (err) => {
+20 -1
View File
@@ -18,7 +18,15 @@ router.get('/', async (req, res) => {
'event_default_require_password',
'event_default_feedback_enabled',
'gallery_show_filter_bar',
'event_phone_field_enabled'
'event_phone_field_enabled',
// #613 — guest upload UI needs to know the per-batch file
// count limit so it can render a real number in the
// "fileRequirements" hint (`{{limit}}` was showing literal)
// and short-circuit before posting too many files. The
// backend route also enforces it via getMaxFilesPerUpload,
// but a client-side guard saves a 4MB+ round-trip when the
// limit is small.
'general_max_files_per_upload'
]);
})
.select('setting_key', 'setting_value');
@@ -145,6 +153,17 @@ router.get('/', async (req, res) => {
gallery_show_filter_bar: settingsObject.gallery_show_filter_bar !== false,
// Upload settings (safe to expose - needed for client-side validation)
allowed_file_types: settingsObject.general_allowed_file_types || 'jpg,jpeg,png,webp',
// #613 — per-batch file count limit. The guest UserPhotoUpload modal
// reads this both to render the "{{limit}} files per upload" hint
// (showed `{{limit}}` literal before this) and to refuse a batch
// before posting it. Backend route enforces the same value via
// getMaxFilesPerUpload(), so the client-side check is purely a UX
// optimisation. Default mirrors uploadSettings.js
// DEFAULT_MAX_FILES_PER_UPLOAD so the UI shows a sensible number
// even on installs that have never explicitly set the value.
general_max_files_per_upload: Number.isFinite(Number(settingsObject.general_max_files_per_upload))
? Number(settingsObject.general_max_files_per_upload)
: 500,
// SEO meta tag flags (safe to expose - these are intended for crawlers)
seo_meta_noindex: settingsObject.seo_meta_noindex === true,
seo_meta_nofollow: settingsObject.seo_meta_nofollow === true,
@@ -32,6 +32,17 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
const { data: publicSettings } = usePublicSettings();
// #613 — guest upload UI was hardcoded to behave as if the limit was
// unlimited (no client-side guard) and the fileRequirements hint
// rendered `{{limit}}` literally because t() was called with no
// interpolation argument. publicSettings now surfaces
// general_max_files_per_upload (default 500 from publicSettings.js)
// so we can render a real number and refuse oversized batches before
// they hit the backend. Backend route enforces the same value too.
const maxFilesPerUpload = Number.isFinite(Number(publicSettings?.general_max_files_per_upload))
? Number(publicSettings?.general_max_files_per_upload)
: 500;
const allowedMimeTypes = useMemo(
() => extensionsToMimeTypes(publicSettings?.allowed_file_types),
[publicSettings?.allowed_file_types]
@@ -57,6 +68,20 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
return true;
});
if (validFiles.length === 0) return;
// #613 — per-batch file count guard. Mirrors what the admin's
// PhotoUpload component does. Backend also enforces, so this is
// purely UX (saves a multi-MB POST when the user clearly went over).
const remaining = Math.max(0, maxFilesPerUpload - files.length);
if (remaining === 0) {
toast.error(t('upload.limitReached', { limit: maxFilesPerUpload }));
return;
}
if (validFiles.length > remaining) {
toast.warning(t('upload.someFilesSkipped', { allowed: remaining, limit: maxFilesPerUpload }));
setFiles((prev) => [...prev, ...validFiles.slice(0, remaining)]);
return;
}
setFiles((prev) => [...prev, ...validFiles]);
};
@@ -200,7 +225,10 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
{t('upload.clickToUpload')}
</p>
<p className="text-xs text-muted-theme">
{t('upload.fileRequirements')}
{/* #613 — pass { limit } so `{{limit}}` interpolates
with the real number from settings instead of
rendering literally. */}
{t('upload.fileRequirements', { limit: maxFilesPerUpload })}
</p>
<input
type="file"
@@ -70,6 +70,10 @@ export interface PublicSettings {
umami_share_url: string | null;
// Upload settings
allowed_file_types?: string;
// #613 — per-batch file count limit, surfaced so the guest UserPhotoUpload
// modal can render the real number in `upload.fileRequirements` and refuse
// oversized batches client-side. Backend enforces the same value too.
general_max_files_per_upload?: number;
// Event field requirements
event_require_customer_name?: boolean;
event_require_customer_email?: boolean;