From c9b64d9c1a8744c9ee5e068366a500ae0dab36bc Mon Sep 17 00:00:00 2001 From: Paul Nothaft <53005142+the-luap@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:06:40 +0200 Subject: [PATCH] fix(uploads): register HEIC/HEIF with the file validator + fix admin format hint (codex review of #832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from the Codex review: - validateFileType requires an ALLOWED_MEDIA_TYPES entry, which had neither image/heic nor image/heif — so HEIC was rejected before sharp ever saw it, despite the EXTENSION_TO_MIME additions. Added both with a single 'ftyp' (offset 4) magic number (the check is .every, so alternatives can't be separate entries). - Changing the shared upload.fileRequirements string to interpolate {{formats}} left the admin PhotoUpload caller passing only { limit }, rendering the placeholder literally (it was also already dropping {{sizeLimit}} from #823). The admin caller now passes formats + sizeLimit + limit, from the admin settings it already loads. --- backend/src/utils/fileSecurityUtils.js | 16 ++++++++++++++++ frontend/src/components/admin/PhotoUpload.tsx | 13 +++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/backend/src/utils/fileSecurityUtils.js b/backend/src/utils/fileSecurityUtils.js index 39e59b70..28155b33 100644 --- a/backend/src/utils/fileSecurityUtils.js +++ b/backend/src/utils/fileSecurityUtils.js @@ -79,6 +79,22 @@ const ALLOWED_IMAGE_TYPES = { extensions: ['.svg'], // SVG files are XML-based text files, so we skip magic number validation magicNumbers: null + }, + // HEIC/HEIF (iPhone). ISO-BMFF container: bytes 4-7 are the "ftyp" box marker, + // present in every HEIF/HEIC file (single entry — the magic check is `.every`, + // so alternatives can't be listed as separate entries). Sharp's libvips + // decodes these; extension + MIME are already gated by validateFileType. + 'image/heic': { + extensions: ['.heic'], + magicNumbers: [ + { offset: 4, bytes: [0x66, 0x74, 0x79, 0x70] } // "ftyp" + ] + }, + 'image/heif': { + extensions: ['.heif'], + magicNumbers: [ + { offset: 4, bytes: [0x66, 0x74, 0x79, 0x70] } // "ftyp" + ] } }; diff --git a/frontend/src/components/admin/PhotoUpload.tsx b/frontend/src/components/admin/PhotoUpload.tsx index 7d3f0ea1..b3a7d0a1 100644 --- a/frontend/src/components/admin/PhotoUpload.tsx +++ b/frontend/src/components/admin/PhotoUpload.tsx @@ -8,7 +8,7 @@ import { useQuery } from '@tanstack/react-query'; import { categoriesService } from '../../services/categories.service'; import { settingsService } from '../../services/settings.service'; import { useTranslation } from 'react-i18next'; -import { extensionsToMimeTypes, extensionsToAcceptString } from '../../utils/fileTypes'; +import { extensionsToMimeTypes, extensionsToAcceptString, extensionsToLabel } from '../../utils/fileTypes'; import { useUploadProgress } from '../../hooks/useUploadProgress'; interface PhotoUploadProps { @@ -118,6 +118,15 @@ export const PhotoUpload: React.FC = ({ eventId, onUploadCompl [settings?.general_allowed_file_types] ); + const formatsLabel = useMemo( + () => extensionsToLabel(settings?.general_allowed_file_types), + [settings?.general_allowed_file_types] + ); + + const maxFileSizeMb = Number.isFinite(Number(settings?.general_max_file_size_mb)) + ? Number(settings?.general_max_file_size_mb) + : 50; + const remainingSlots = Math.max(maxFilesPerUpload - selectedFiles.length, 0); const [isDragOver, setIsDragOver] = useState(false); @@ -511,7 +520,7 @@ export const PhotoUpload: React.FC = ({ eventId, onUploadCompl {t('upload.clickToUpload')}

- {t('upload.fileRequirements', { limit: maxFilesPerUpload })} + {t('upload.fileRequirements', { formats: formatsLabel, limit: maxFilesPerUpload, sizeLimit: maxFileSizeMb })}