From bc802846c5a19c664d5be999a888947dcac85e05 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:00:51 -0400 Subject: [PATCH] fix: serve svg+xml as svg (#13277) Based from https://github.com/payloadcms/payload/pull/13276 Fixes https://github.com/payloadcms/payload/issues/7624 If an uploaded image has `.svg` ext, and the mimeType is read as `application/xml` adjust the mimeType to `image/svg+xml`. --------- Co-authored-by: Philipp Schneider <47689073+philipp-tailor@users.noreply.github.com> --- .../payload/src/uploads/endpoints/getFile.ts | 7 +- test/uploads/e2e.spec.ts | 20 ++ test/uploads/payload-types.ts | 207 ++++++++++++++++++ 3 files changed, 233 insertions(+), 1 deletion(-) diff --git a/packages/payload/src/uploads/endpoints/getFile.ts b/packages/payload/src/uploads/endpoints/getFile.ts index e7b39c079..8a316840f 100644 --- a/packages/payload/src/uploads/endpoints/getFile.ts +++ b/packages/payload/src/uploads/endpoints/getFile.ts @@ -93,9 +93,14 @@ export const getFileHandler: PayloadHandler = async (req) => { const data = streamFile(filePath) const fileTypeResult = (await fileTypeFromFile(filePath)) || getFileTypeFallback(filePath) + let mimeType = fileTypeResult.mime + + if (filePath.endsWith('.svg') && fileTypeResult.mime === 'application/xml') { + mimeType = 'image/svg+xml' + } let headers = new Headers() - headers.set('Content-Type', fileTypeResult.mime) + headers.set('Content-Type', mimeType) headers.set('Content-Length', stats.size + '') headers = collection.config.upload?.modifyResponseHeaders ? collection.config.upload.modifyResponseHeaders({ headers }) || headers diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index 97af36acb..1b55dadd4 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -42,6 +42,7 @@ import { mediaWithoutCacheTagsSlug, relationPreviewSlug, relationSlug, + svgOnlySlug, threeDimensionalSlug, withMetadataSlug, withOnlyJPEGMetadataSlug, @@ -87,6 +88,7 @@ let collectErrorsFromPage: () => boolean let stopCollectingErrorsFromPage: () => boolean let bulkUploadsURL: AdminUrlUtil let fileMimeTypeURL: AdminUrlUtil +let svgOnlyURL: AdminUrlUtil describe('Uploads', () => { let page: Page @@ -126,6 +128,7 @@ describe('Uploads', () => { constructorOptionsURL = new AdminUrlUtil(serverURL, constructorOptionsSlug) bulkUploadsURL = new AdminUrlUtil(serverURL, bulkUploadsSlug) fileMimeTypeURL = new AdminUrlUtil(serverURL, fileMimeTypeSlug) + svgOnlyURL = new AdminUrlUtil(serverURL, svgOnlySlug) const context = await browser.newContext() page = await context.newPage() @@ -270,6 +273,23 @@ describe('Uploads', () => { await expect(fileMetaSizeType).toHaveText(/model\/gltf-binary/) }) + test('should show proper mimetype for svg+xml file', async () => { + await page.goto(svgOnlyURL.create) + + await page.setInputFiles('input[type="file"]', path.resolve(dirname, './svgWithXml.svg')) + const filename = page.locator('.file-field__filename') + await expect(filename).toHaveValue('svgWithXml.svg') + + await saveDocAndAssert(page) + + const fileMetaSizeType = page.locator('.file-meta__size-type') + await expect(fileMetaSizeType).toHaveText(/image\/svg\+xml/) + + // ensure the svg loads + const svgImage = page.locator('img[src*="svgWithXml"]') + await expect(svgImage).toBeVisible() + }) + test('should create animated file upload', async () => { await page.goto(animatedTypeMediaURL.create) diff --git a/test/uploads/payload-types.ts b/test/uploads/payload-types.ts index fcccd753d..6e807e723 100644 --- a/test/uploads/payload-types.ts +++ b/test/uploads/payload-types.ts @@ -84,6 +84,9 @@ export interface Config { 'allow-list-media': AllowListMedia; 'skip-safe-fetch-media': SkipSafeFetchMedia; 'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMedia; + 'restrict-file-types': RestrictFileType; + 'no-restrict-file-types': NoRestrictFileType; + 'no-restrict-file-mime-types': NoRestrictFileMimeType; 'animated-type-media': AnimatedTypeMedia; enlarge: Enlarge; 'without-enlarge': WithoutEnlarge; @@ -113,6 +116,8 @@ export interface Config { 'constructor-options': ConstructorOption; 'bulk-uploads': BulkUpload; 'simple-relationship': SimpleRelationship; + 'file-mime-type': FileMimeType; + 'svg-only': SvgOnly; users: User; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; @@ -137,6 +142,9 @@ export interface Config { 'allow-list-media': AllowListMediaSelect | AllowListMediaSelect; 'skip-safe-fetch-media': SkipSafeFetchMediaSelect | SkipSafeFetchMediaSelect; 'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMediaSelect | SkipAllowListSafeFetchMediaSelect; + 'restrict-file-types': RestrictFileTypesSelect | RestrictFileTypesSelect; + 'no-restrict-file-types': NoRestrictFileTypesSelect | NoRestrictFileTypesSelect; + 'no-restrict-file-mime-types': NoRestrictFileMimeTypesSelect | NoRestrictFileMimeTypesSelect; 'animated-type-media': AnimatedTypeMediaSelect | AnimatedTypeMediaSelect; enlarge: EnlargeSelect | EnlargeSelect; 'without-enlarge': WithoutEnlargeSelect | WithoutEnlargeSelect; @@ -166,6 +174,8 @@ export interface Config { 'constructor-options': ConstructorOptionsSelect | ConstructorOptionsSelect; 'bulk-uploads': BulkUploadsSelect | BulkUploadsSelect; 'simple-relationship': SimpleRelationshipSelect | SimpleRelationshipSelect; + 'file-mime-type': FileMimeTypeSelect | FileMimeTypeSelect; + 'svg-only': SvgOnlySelect | SvgOnlySelect; users: UsersSelect | UsersSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; @@ -838,6 +848,60 @@ export interface SkipAllowListSafeFetchMedia { focalX?: number | null; focalY?: number | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "restrict-file-types". + */ +export interface RestrictFileType { + id: string; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "no-restrict-file-types". + */ +export interface NoRestrictFileType { + id: string; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "no-restrict-file-mime-types". + */ +export interface NoRestrictFileMimeType { + id: string; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "animated-type-media". @@ -1502,6 +1566,43 @@ export interface SimpleRelationship { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "file-mime-type". + */ +export interface FileMimeType { + id: string; + title?: string | null; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "svg-only". + */ +export interface SvgOnly { + id: string; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users". @@ -1601,6 +1702,18 @@ export interface PayloadLockedDocument { relationTo: 'skip-allow-list-safe-fetch-media'; value: string | SkipAllowListSafeFetchMedia; } | null) + | ({ + relationTo: 'restrict-file-types'; + value: string | RestrictFileType; + } | null) + | ({ + relationTo: 'no-restrict-file-types'; + value: string | NoRestrictFileType; + } | null) + | ({ + relationTo: 'no-restrict-file-mime-types'; + value: string | NoRestrictFileMimeType; + } | null) | ({ relationTo: 'animated-type-media'; value: string | AnimatedTypeMedia; @@ -1717,6 +1830,14 @@ export interface PayloadLockedDocument { relationTo: 'simple-relationship'; value: string | SimpleRelationship; } | null) + | ({ + relationTo: 'file-mime-type'; + value: string | FileMimeType; + } | null) + | ({ + relationTo: 'svg-only'; + value: string | SvgOnly; + } | null) | ({ relationTo: 'users'; value: string | User; @@ -2429,6 +2550,57 @@ export interface SkipAllowListSafeFetchMediaSelect { focalX?: T; focalY?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "restrict-file-types_select". + */ +export interface RestrictFileTypesSelect { + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "no-restrict-file-types_select". + */ +export interface NoRestrictFileTypesSelect { + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "no-restrict-file-mime-types_select". + */ +export interface NoRestrictFileMimeTypesSelect { + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "animated-type-media_select". @@ -3138,6 +3310,41 @@ export interface SimpleRelationshipSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "file-mime-type_select". + */ +export interface FileMimeTypeSelect { + title?: T; + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "svg-only_select". + */ +export interface SvgOnlySelect { + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users_select".