diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index 655e9a41..ae59cf45 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -24,6 +24,7 @@ import { import { DriveFileDTO } from "../dto/drive-file-dto"; import { DriveFileDTOBuilder } from "../../services/drive-file-dto-builder"; import config from "config"; +import { formatAttachmentContentDispositionHeader } from "../../../files/utils"; export class DocumentsController { private driveFileDTOBuilder = new DriveFileDTOBuilder(); @@ -510,9 +511,9 @@ export class DocumentsController { return response; } else if (archiveOrFile.file) { const data = archiveOrFile.file; - const filename = encodeURIComponent(data.name.replace(/[^\p{L}0-9 _.-]/gu, "")); - response.header("Content-disposition", `attachment; filename="${filename}"`); + response.header("Content-Disposition", formatAttachmentContentDispositionHeader(data.name)); + if (data.size) response.header("Content-Length", data.size); response.type(data.mime); return response.send(data.file); @@ -565,7 +566,10 @@ export class DocumentsController { try { const archive = await globalResolver.services.documents.documents.createZip(ids, context); - reply.raw.setHeader("content-disposition", 'attachment; filename="twake_drive.zip"'); + reply.raw.setHeader( + "content-disposition", + formatAttachmentContentDispositionHeader("twake_drive.zip"), + ); archive.on("finish", () => { reply.status(200); diff --git a/tdrive/backend/node/src/services/files/utils.ts b/tdrive/backend/node/src/services/files/utils.ts index e0584396..9b55e6b6 100644 --- a/tdrive/backend/node/src/services/files/utils.ts +++ b/tdrive/backend/node/src/services/files/utils.ts @@ -22,3 +22,12 @@ export const fileIsMedia = (file: Partial): boolean => { file.metadata?.mime?.startsWith("image/") ); }; + +/** + * Generate RFC 5987 UTF-8 encoding compliant header value for `Content-Disposition` + * to make a browser download a reponse to a file with the provided name + */ +export const formatAttachmentContentDispositionHeader = (filename: string) => { + const encoded = encodeURIComponent(filename.replace(/[^\p{L}0-9 _.-]/gu, "")); + return `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`; +}; diff --git a/tdrive/backend/node/src/services/files/web/controllers/files.ts b/tdrive/backend/node/src/services/files/web/controllers/files.ts index fd88bc6c..587dd0c4 100644 --- a/tdrive/backend/node/src/services/files/web/controllers/files.ts +++ b/tdrive/backend/node/src/services/files/web/controllers/files.ts @@ -5,6 +5,7 @@ import { CompanyExecutionContext } from "../types"; import { UploadOptions } from "../../types"; import { PublicFile } from "../../entities/file"; import gr from "../../../global-resolver"; +import { formatAttachmentContentDispositionHeader } from "../../utils"; export class FileController { async save( @@ -47,9 +48,8 @@ export class FileController { const params = request.params; try { const data = await gr.services.files.download(params.id, context); - const filename = data.name.replace(/[^a-zA-Z0-9 -_.]/g, ""); + response.header("Content-Disposition", formatAttachmentContentDispositionHeader(data.name)); - response.header("Content-disposition", `attachment; filename="${filename}"`); if (data.size) response.header("Content-Length", data.size); response.type(data.mime); return response.send(data.file);