From 8f81b77cf2a3da0a445f3700fbf4a0091ea46c07 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Sun, 29 Mar 2026 02:03:07 +0800 Subject: [PATCH] fix: include filename in Content-Disposition header for inline downloads (#5860) --- http/raw.go | 3 +- http/raw_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 http/raw_test.go diff --git a/http/raw.go b/http/raw.go index a6857ffb..b5c86643 100644 --- a/http/raw.go +++ b/http/raw.go @@ -72,7 +72,8 @@ func parseQueryAlgorithm(r *http.Request) (string, archives.Archival, error) { func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) { if r.URL.Query().Get("inline") == "true" { - w.Header().Set("Content-Disposition", "inline") + // As per RFC6266 section 4.3 + w.Header().Set("Content-Disposition", "inline; filename*=utf-8''"+url.PathEscape(file.Name)) } else { // As per RFC6266 section 4.3 w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name)) diff --git a/http/raw_test.go b/http/raw_test.go new file mode 100644 index 00000000..ec53a173 --- /dev/null +++ b/http/raw_test.go @@ -0,0 +1,75 @@ +package fbhttp + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/filebrowser/filebrowser/v2/files" +) + +func TestSetContentDisposition(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + filename string + inline bool + expected string + }{ + "inline simple filename": { + filename: "document.pdf", + inline: true, + expected: "inline; filename*=utf-8''" + url.PathEscape("document.pdf"), + }, + "attachment simple filename": { + filename: "document.pdf", + inline: false, + expected: "attachment; filename*=utf-8''" + url.PathEscape("document.pdf"), + }, + "inline non-ASCII filename": { + filename: "日本語.txt", + inline: true, + expected: "inline; filename*=utf-8''" + url.PathEscape("日本語.txt"), + }, + "attachment non-ASCII filename": { + filename: "日本語.txt", + inline: false, + expected: "attachment; filename*=utf-8''" + url.PathEscape("日本語.txt"), + }, + "inline filename with spaces": { + filename: "my file.txt", + inline: true, + expected: "inline; filename*=utf-8''" + url.PathEscape("my file.txt"), + }, + "attachment filename with spaces": { + filename: "my file.txt", + inline: false, + expected: "attachment; filename*=utf-8''" + url.PathEscape("my file.txt"), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + recorder := httptest.NewRecorder() + req, err := http.NewRequest(http.MethodGet, "/test", http.NoBody) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + if tc.inline { + req.URL.RawQuery = "inline=true" + } + + file := &files.FileInfo{Name: tc.filename} + + setContentDisposition(recorder, req, file) + + got := recorder.Header().Get("Content-Disposition") + if got != tc.expected { + t.Errorf("Content-Disposition = %q, want %q", got, tc.expected) + } + }) + } +}