import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
+import { getFilenameFromContentDisposition } from 'src/app/utils/http'
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import * as UTIF from 'utif'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
.get(downloadUrl, { observe: 'response', responseType: 'blob' })
.subscribe({
next: (response: HttpResponse<Blob>) => {
- const filename = response.headers
- .get('Content-Disposition')
- ?.split(';')
- ?.find((part) => part.trim().startsWith('filename='))
- ?.split('=')[1]
- ?.replace(/['"]/g, '')
+ const contentDisposition = response.headers.get('Content-Disposition')
+ const filename =
+ getFilenameFromContentDisposition(contentDisposition) ||
+ this.document.title
const blob = new Blob([response.body], {
type: response.body.type,
})
--- /dev/null
+import { getFilenameFromContentDisposition } from './http'
+
+describe('getFilenameFromContentDisposition', () => {
+ it('should extract filename from Content-Disposition header with filename*', () => {
+ const header = "attachment; filename*=UTF-8''example%20file.txt"
+ expect(getFilenameFromContentDisposition(header)).toBe('example file.txt')
+ })
+
+ it('should extract filename from Content-Disposition header with filename=', () => {
+ const header = 'attachment; filename="example-file.txt"'
+ expect(getFilenameFromContentDisposition(header)).toBe('example-file.txt')
+ })
+
+ it('should prioritize filename* over filename if both are present', () => {
+ const header =
+ 'attachment; filename="fallback.txt"; filename*=UTF-8\'\'preferred%20file.txt'
+ const result = getFilenameFromContentDisposition(header)
+ expect(result).toBe('preferred file.txt')
+ })
+
+ it('should gracefully fall back to null', () => {
+ // invalid UTF-8 sequence
+ expect(
+ getFilenameFromContentDisposition("attachment; filename*=UTF-8''%E0%A4%A")
+ ).toBeNull()
+ // missing filename
+ expect(getFilenameFromContentDisposition('attachment;')).toBeNull()
+ // empty header
+ expect(getFilenameFromContentDisposition(null)).toBeNull()
+ })
+})
--- /dev/null
+export function getFilenameFromContentDisposition(header: string): string {
+ if (!header) {
+ return null
+ }
+
+ // Try filename* (RFC 5987)
+ const filenameStar = header.match(/filename\*=(?:UTF-\d['']*)?([^;]+)/i)
+ if (filenameStar?.[1]) {
+ try {
+ return decodeURIComponent(filenameStar[1])
+ } catch (e) {
+ // Ignore decoding errors and fall through
+ }
+ }
+
+ // Fallback to filename=
+ const filenameMatch = header.match(/filename="?([^"]+)"?/)
+ if (filenameMatch?.[1]) {
+ return filenameMatch[1]
+ }
+
+ return null
+}