@@ -34,6 +34,7 @@ import (
3434 "code.gitea.io/gitea/modules/setting"
3535 "code.gitea.io/gitea/modules/templates"
3636 "code.gitea.io/gitea/modules/translation"
37+ "code.gitea.io/gitea/modules/typesniffer"
3738 "code.gitea.io/gitea/modules/util"
3839 "code.gitea.io/gitea/modules/web/middleware"
3940 "code.gitea.io/gitea/services/auth"
@@ -322,9 +323,9 @@ func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
322323 if statusPrefix == 4 || statusPrefix == 5 {
323324 log .Log (skip , log .TRACE , "plainTextInternal (status=%d): %s" , status , string (bs ))
324325 }
325- ctx .Resp .WriteHeader (status )
326326 ctx .Resp .Header ().Set ("Content-Type" , "text/plain;charset=utf-8" )
327327 ctx .Resp .Header ().Set ("X-Content-Type-Options" , "nosniff" )
328+ ctx .Resp .WriteHeader (status )
328329 if _ , err := ctx .Resp .Write (bs ); err != nil {
329330 log .ErrorWithSkip (skip , "plainTextInternal (status=%d): write bytes failed: %v" , status , err )
330331 }
@@ -345,16 +346,45 @@ func (ctx *Context) RespHeader() http.Header {
345346 return ctx .Resp .Header ()
346347}
347348
349+ type ServeHeaderOptions struct {
350+ ContentType string // defaults to "application/octet-stream"
351+ ContentTypeCharset string
352+ Disposition string // defaults to "attachment"
353+ Filename string
354+ CacheDuration time.Duration // defaults to 5 minutes
355+ }
356+
348357// SetServeHeaders sets necessary content serve headers
349- func (ctx * Context ) SetServeHeaders (filename string ) {
350- ctx .Resp .Header ().Set ("Content-Description" , "File Transfer" )
351- ctx .Resp .Header ().Set ("Content-Type" , "application/octet-stream" )
352- ctx .Resp .Header ().Set ("Content-Disposition" , "attachment; filename=" + filename )
353- ctx .Resp .Header ().Set ("Content-Transfer-Encoding" , "binary" )
354- ctx .Resp .Header ().Set ("Expires" , "0" )
355- ctx .Resp .Header ().Set ("Cache-Control" , "must-revalidate" )
356- ctx .Resp .Header ().Set ("Pragma" , "public" )
357- ctx .Resp .Header ().Set ("Access-Control-Expose-Headers" , "Content-Disposition" )
358+ func (ctx * Context ) SetServeHeaders (opts * ServeHeaderOptions ) {
359+ header := ctx .Resp .Header ()
360+
361+ contentType := typesniffer .ApplicationOctetStream
362+ if opts .ContentType != "" {
363+ if opts .ContentTypeCharset != "" {
364+ contentType = opts .ContentType + "; charset=" + strings .ToLower (opts .ContentTypeCharset )
365+ } else {
366+ contentType = opts .ContentType
367+ }
368+ }
369+ header .Set ("Content-Type" , contentType )
370+ header .Set ("X-Content-Type-Options" , "nosniff" )
371+
372+ if opts .Filename != "" {
373+ disposition := opts .Disposition
374+ if disposition == "" {
375+ disposition = "attachment"
376+ }
377+
378+ backslashEscapedName := strings .ReplaceAll (strings .ReplaceAll (opts .Filename , `\` , `\\` ), `"` , `\"` ) // \ -> \\, " -> \"
379+ header .Set ("Content-Disposition" , fmt .Sprintf (`%s; filename="%s"; filename*=UTF-8''%s` , disposition , backslashEscapedName , url .PathEscape (opts .Filename )))
380+ header .Set ("Access-Control-Expose-Headers" , "Content-Disposition" )
381+ }
382+
383+ duration := opts .CacheDuration
384+ if duration == 0 {
385+ duration = 5 * time .Minute
386+ }
387+ httpcache .AddCacheControlToHeader (header , duration )
358388}
359389
360390// ServeContent serves content to http request
@@ -366,7 +396,9 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa
366396 modTime = v
367397 }
368398 }
369- ctx .SetServeHeaders (name )
399+ ctx .SetServeHeaders (& ServeHeaderOptions {
400+ Filename : name ,
401+ })
370402 http .ServeContent (ctx .Resp , ctx .Req , name , modTime , r )
371403}
372404
@@ -378,13 +410,17 @@ func (ctx *Context) ServeFile(file string, names ...string) {
378410 } else {
379411 name = path .Base (file )
380412 }
381- ctx .SetServeHeaders (name )
413+ ctx .SetServeHeaders (& ServeHeaderOptions {
414+ Filename : name ,
415+ })
382416 http .ServeFile (ctx .Resp , ctx .Req , file )
383417}
384418
385419// ServeStream serves file via io stream
386420func (ctx * Context ) ServeStream (rd io.Reader , name string ) {
387- ctx .SetServeHeaders (name )
421+ ctx .SetServeHeaders (& ServeHeaderOptions {
422+ Filename : name ,
423+ })
388424 _ , err := io .Copy (ctx .Resp , rd )
389425 if err != nil {
390426 ctx .ServerError ("Download file failed" , err )
0 commit comments