diff --git a/context.go b/context.go index d69df70b24..48d7550697 100644 --- a/context.go +++ b/context.go @@ -6,7 +6,6 @@ package gin import ( "errors" - "fmt" "io" "io/ioutil" "log" @@ -1018,7 +1017,11 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { // FileAttachment writes the specified file into the body stream in an efficient way // On the client side, the file will typically be downloaded with the given filename func (c *Context) FileAttachment(filepath, filename string) { - c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + if isASCII(filename) { + c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) + } else { + c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) + } http.ServeFile(c.Writer, c.Request, filepath) } diff --git a/context_test.go b/context_test.go index 9e02aede13..4eed164ab1 100644 --- a/context_test.go +++ b/context_test.go @@ -15,6 +15,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/url" "os" "reflect" "strings" @@ -1033,6 +1034,19 @@ func TestContextRenderAttachment(t *testing.T) { assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition")) } +func TestContextRenderUTF8Attachment(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + newFilename := "new🧑_filename.go" + + c.Request, _ = http.NewRequest("GET", "/", nil) + c.FileAttachment("./gin.go", newFilename) + + assert.Equal(t, 200, w.Code) + assert.Contains(t, w.Body.String(), "func New() *Engine {") + assert.Equal(t, `attachment; filename*=UTF-8''`+url.QueryEscape(newFilename), w.Header().Get("Content-Disposition")) +} + // TestContextRenderYAML tests that the response is serialized as YAML // and Content-Type is set to application/x-yaml func TestContextRenderYAML(t *testing.T) { diff --git a/utils.go b/utils.go index c32f0eeb0e..e4599ea927 100644 --- a/utils.go +++ b/utils.go @@ -12,6 +12,7 @@ import ( "reflect" "runtime" "strings" + "unicode" ) // BindKey indicates a default bind key. @@ -151,3 +152,13 @@ func resolveAddress(addr []string) string { panic("too many parameters") } } + +// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters +func isASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] > unicode.MaxASCII { + return false + } + } + return true +} diff --git a/utils_test.go b/utils_test.go index b50914f25e..d2a740bfb1 100644 --- a/utils_test.go +++ b/utils_test.go @@ -143,3 +143,8 @@ func TestMarshalXMLforH(t *testing.T) { e := h.MarshalXML(enc, x) assert.Error(t, e) } + +func TestIsASCII(t *testing.T) { + assert.Equal(t, isASCII("test"), true) + assert.Equal(t, isASCII("πŸ§‘πŸ’›πŸ’šπŸ’™πŸ’œ"), false) +}