From 32276edb5db3709c9f50bc4aa26d95783688bc81 Mon Sep 17 00:00:00 2001 From: Montassar Ghanmy Date: Thu, 3 Oct 2024 13:43:33 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20URL=20encoded=20filename?= =?UTF-8?q?=20on=20download=20(#675)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: #665 url encoded filename on download * ♻️ back: refactor content-disposition attachment filename encoding (#665) --------- Co-authored-by: Eric Doughty-Papassideris --- .../services/documents/web/controllers/documents.ts | 10 +++++++--- tdrive/backend/node/src/services/files/utils.ts | 9 +++++++++ .../node/src/services/files/web/controllers/files.ts | 4 ++-- 3 files changed, 18 insertions(+), 5 deletions(-) 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);