From da0be7488ed567d1f5bd5649efc1e1b8eb17e908 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travpc@gmail.com> Date: Mon, 17 Aug 2020 18:09:19 -0600 Subject: [PATCH] Implement MSC2702 Fixes https://github.com/turt2live/matrix-media-repo/issues/267 --- CHANGELOG.md | 1 + api/custom/exports.go | 1 + api/r0/download.go | 27 ++++++++++++++++------- api/r0/thumbnail.go | 4 ++-- api/unstable/blurhash_render.go | 1 + api/unstable/ipfs_download.go | 18 +++++++++++---- api/webserver/route_handler.go | 34 +++++++++++++++++++++++++---- dev/conduit-dev-docker-compose.yaml | 4 ---- util/strings.go | 14 ++++++++++++ 9 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 util/strings.go diff --git a/CHANGELOG.md b/CHANGELOG.md index d52293b3..b32a530f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Added thumbnailing support for some audio waveforms (MP3, WAV, OGG, and FLAC). * Added audio metadata (duration, etc) to the unstable `/info` endpoint. Aligns with [MSC2380](https://github.com/matrix-org/matrix-doc/pull/2380). * Added simple thumbnailing for MP4 videos. +* Added an `asAttachment` query parameter to download requests per [MSC2702](https://github.com/matrix-org/matrix-doc/pull/2702). ### Fixed diff --git a/api/custom/exports.go b/api/custom/exports.go index cf954903..6251a49e 100644 --- a/api/custom/exports.go +++ b/api/custom/exports.go @@ -260,6 +260,7 @@ func DownloadExportPart(r *http.Request, rctx rcontext.RequestContext, user api. SizeBytes: part.SizeBytes, Data: s, Filename: part.FileName, + TargetDisposition: "attachment", } } diff --git a/api/r0/download.go b/api/r0/download.go index 0122238f..fd06a839 100644 --- a/api/r0/download.go +++ b/api/r0/download.go @@ -14,10 +14,11 @@ import ( ) type DownloadMediaResponse struct { - ContentType string - Filename string - SizeBytes int64 - Data io.ReadCloser + ContentType string + Filename string + SizeBytes int64 + Data io.ReadCloser + TargetDisposition string } func DownloadMedia(r *http.Request, rctx rcontext.RequestContext, user api.UserInfo) interface{} { @@ -28,6 +29,15 @@ func DownloadMedia(r *http.Request, rctx rcontext.RequestContext, user api.UserI filename := params["filename"] allowRemote := r.URL.Query().Get("allow_remote") + targetDisposition := r.URL.Query().Get("org.matrix.msc2702.asAttachment") + if targetDisposition == "true" { + targetDisposition = "attachment" + } else if targetDisposition == "false" { + targetDisposition = "inline" + } else { + targetDisposition = "infer" + } + downloadRemote := true if allowRemote != "" { parsedFlag, err := strconv.ParseBool(allowRemote) @@ -62,9 +72,10 @@ func DownloadMedia(r *http.Request, rctx rcontext.RequestContext, user api.UserI } return &DownloadMediaResponse{ - ContentType: streamedMedia.ContentType, - Filename: filename, - SizeBytes: streamedMedia.SizeBytes, - Data: streamedMedia.Stream, + ContentType: streamedMedia.ContentType, + Filename: filename, + SizeBytes: streamedMedia.SizeBytes, + Data: streamedMedia.Stream, + TargetDisposition: targetDisposition, } } diff --git a/api/r0/thumbnail.go b/api/r0/thumbnail.go index fa8ffcbc..779cf635 100644 --- a/api/r0/thumbnail.go +++ b/api/r0/thumbnail.go @@ -79,7 +79,7 @@ func ThumbnailMedia(r *http.Request, rctx rcontext.RequestContext, user api.User "requestedAnimated": animated, }) - if width <= 0 || height <=0 { + if width <= 0 || height <= 0 { return api.BadRequest("Width and height must be greater than zero") } @@ -98,6 +98,6 @@ func ThumbnailMedia(r *http.Request, rctx rcontext.RequestContext, user api.User ContentType: streamedThumbnail.Thumbnail.ContentType, SizeBytes: streamedThumbnail.Thumbnail.SizeBytes, Data: streamedThumbnail.Stream, - Filename: "thumbnail", + Filename: "thumbnail.png", } } diff --git a/api/unstable/blurhash_render.go b/api/unstable/blurhash_render.go index 9c46c78c..c1ddeca7 100644 --- a/api/unstable/blurhash_render.go +++ b/api/unstable/blurhash_render.go @@ -64,5 +64,6 @@ func RenderBlurhash(r *http.Request, rctx rcontext.RequestContext, user api.User Filename: "blurhash.png", SizeBytes: int64(buf.Len()), Data: util.BufferToStream(buf), // convert to stream to avoid console spam + TargetDisposition: "inline", } } diff --git a/api/unstable/ipfs_download.go b/api/unstable/ipfs_download.go index 5c33122d..62461a29 100644 --- a/api/unstable/ipfs_download.go +++ b/api/unstable/ipfs_download.go @@ -17,6 +17,15 @@ func IPFSDownload(r *http.Request, rctx rcontext.RequestContext, user api.UserIn server := params["server"] ipfsContentId := params["ipfsContentId"] + targetDisposition := r.URL.Query().Get("org.matrix.msc2702.asAttachment") + if targetDisposition == "true" { + targetDisposition = "attachment" + } else if targetDisposition == "false" { + targetDisposition = "inline" + } else { + targetDisposition = "infer" + } + rctx = rctx.LogWithFields(logrus.Fields{ "ipfsContentId": ipfsContentId, "server": server, @@ -29,9 +38,10 @@ func IPFSDownload(r *http.Request, rctx rcontext.RequestContext, user api.UserIn } return &r0.DownloadMediaResponse{ - ContentType: obj.ContentType, - Filename: obj.FileName, - SizeBytes: obj.SizeBytes, - Data: obj.Data, + ContentType: obj.ContentType, + Filename: obj.FileName, + SizeBytes: obj.SizeBytes, + Data: obj.Data, + TargetDisposition: targetDisposition, } } diff --git a/api/webserver/route_handler.go b/api/webserver/route_handler.go index eff47562..36b633b6 100644 --- a/api/webserver/route_handler.go +++ b/api/webserver/route_handler.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "mime" "net" "net/http" "net/url" @@ -171,13 +172,38 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if result.SizeBytes > 0 { w.Header().Set("Content-Length", fmt.Sprint(result.SizeBytes)) } - if result.Filename != "" { - if is.ASCII(result.Filename) { - w.Header().Set("Content-Disposition", "inline; filename="+url.QueryEscape(result.Filename)) + disposition := result.TargetDisposition + if disposition == "" { + disposition = "inline" + } else if disposition == "infer" { + if result.ContentType == "" { + disposition = "attachment" } else { - w.Header().Set("Content-Disposition", "inline; filename*=utf-8''"+url.QueryEscape(result.Filename)) + if util.HasAnyPrefix(result.ContentType, []string{"image/", "audio/", "video/"}) { + disposition = "inline" + } else { + disposition = "attachment" + } } } + fname := result.Filename + if fname == "" { + exts, err := mime.ExtensionsByType(result.ContentType) + if err != nil { + exts = nil + contextLog.Warn("Unexpected error inferring file extension: " + err.Error()) + } + ext := "" + if exts != nil && len(exts) > 0 { + ext = exts[0] + } + fname = "file" + ext + } + if is.ASCII(result.Filename) { + w.Header().Set("Content-Disposition", disposition+"; filename="+url.QueryEscape(fname)) + } else { + w.Header().Set("Content-Disposition", disposition+"; filename*=utf-8''"+url.QueryEscape(fname)) + } defer result.Data.Close() writeResponseData(w, result.Data, result.SizeBytes) return // Prevent sending conflicting responses diff --git a/dev/conduit-dev-docker-compose.yaml b/dev/conduit-dev-docker-compose.yaml index d707ae67..c083cf5c 100644 --- a/dev/conduit-dev-docker-compose.yaml +++ b/dev/conduit-dev-docker-compose.yaml @@ -13,8 +13,6 @@ services: ROCKET_PORT: 8004 ROCKET_REGISTRATION_DISABLED: "false" ROCKET_ENCRYPTION_DISABLED: "false" - ports: - - "8004:8004" networks: - proxy nginx: @@ -33,8 +31,6 @@ services: restart: unless-stopped volumes: - ./element-config.json:/app/config.json - ports: - - "8080:80" networks: - proxy networks: diff --git a/util/strings.go b/util/strings.go new file mode 100644 index 00000000..9ecc3ead --- /dev/null +++ b/util/strings.go @@ -0,0 +1,14 @@ +package util + +import ( + "strings" +) + +func HasAnyPrefix(val string, prefixes []string) bool { + for _, p := range prefixes { + if strings.HasPrefix(val, p) { + return true + } + } + return false +} -- GitLab