From e1cfacb2dd95dd6dcdec2d1a09dfc8ffdbfa468b Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Fri, 16 Oct 2026 11:29:07 +0200 Subject: [PATCH] feat(collaboration): send DownloadUrl outside of secure view --- .../pkg/connector/fileconnector.go | 28 +++++++++++++++++++ .../pkg/connector/fileconnector_test.go | 12 ++++++++ .../pkg/connector/fileinfo/collabora.go | 4 +++ 3 files changed, 44 insertions(+) diff --git a/services/collaboration/pkg/connector/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go index d79dfcd47c..dcdd8a8e08 100644 --- a/services/collaboration/pkg/connector/fileconnector.go +++ b/services/collaboration/pkg/connector/fileconnector.go @@ -1375,6 +1375,15 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse, infoMap[fileinfo.KeyUserExtraInfo] = extraInfo } + // secure view exists to keep the original file from the user, so it gets no download link + if wopiContext.ViewOnlyToken == "" { + if du, err := f.createFileDownloadURL(wopiContext, collaborationURL); err == nil { + infoMap[fileinfo.KeyDownloadURL] = du + } else { + logger.Error().Err(err).Msg("CheckFileInfo: failed to create the download URL") + } + } + // if the file content is empty and a template reference is set, add the template source URL if wopiContext.TemplateReference != nil && statRes.GetInfo().GetSize() == 0 { if tu, err := f.createDownloadURL(wopiContext, collaborationURL); err == nil { @@ -1412,6 +1421,25 @@ func (f *FileConnector) createDownloadURL(wopiContext middleware.WopiContext, co return downloadURL.String(), nil } +// createFileDownloadURL builds a URL to the WOPI contents of the file, authorized by a WOPI token. +func (f *FileConnector) createFileDownloadURL(wopiContext middleware.WopiContext, collaborationURL *url.URL) (string, error) { + token, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg, f.store) + if err != nil { + return "", err + } + downloadURL := *collaborationURL + downloadURL.Path = path.Join( + collaborationURL.Path, + "wopi/files/", + helpers.HashResourceId(wopiContext.FileReference.GetResourceId()), + "contents", + ) + q := downloadURL.Query() + q.Add("access_token", token) + downloadURL.RawQuery = q.Encode() + return downloadURL.String(), nil +} + // createAvatarURL builds a WOPI-proxied avatar URL for the given user. // The collaboration service's /wopi/avatars/ endpoint will fetch the avatar // from the Graph API using the WOPI token for authentication. diff --git a/services/collaboration/pkg/connector/fileconnector_test.go b/services/collaboration/pkg/connector/fileconnector_test.go index b5569cbd87..f7f7952d5d 100644 --- a/services/collaboration/pkg/connector/fileconnector_test.go +++ b/services/collaboration/pkg/connector/fileconnector_test.go @@ -1768,6 +1768,10 @@ var _ = Describe("FileConnector", func() { response, err := fc.CheckFileInfo(ctx) Expect(err).ToNot(HaveOccurred()) Expect(response.Status).To(Equal(200)) + // DownloadURL carries a fresh WOPI token, so check its shape separately + Expect(response.Body.(*fileinfo.Microsoft).DownloadURL).To(HavePrefix("https://wopi.opencloud.test/wopi/files/")) + Expect(response.Body.(*fileinfo.Microsoft).DownloadURL).To(ContainSubstring("/contents?access_token=")) + response.Body.(*fileinfo.Microsoft).DownloadURL = "" Expect(response.Body.(*fileinfo.Microsoft)).To(Equal(expectedFileInfo)) }) @@ -1862,6 +1866,10 @@ var _ = Describe("FileConnector", func() { // overwrite UserID and UserFriendlyName here for easier matching response.Body.(*fileinfo.Collabora).UserID = "guest-zzz000" response.Body.(*fileinfo.Collabora).UserFriendlyName = "guest zzz000" + // DownloadURL carries a fresh WOPI token, so check its shape separately + Expect(response.Body.(*fileinfo.Collabora).DownloadURL).To(HavePrefix("https://wopi.opencloud.test/wopi/files/")) + Expect(response.Body.(*fileinfo.Collabora).DownloadURL).To(ContainSubstring("/contents?access_token=")) + response.Body.(*fileinfo.Collabora).DownloadURL = "" Expect(err).ToNot(HaveOccurred()) Expect(response.Status).To(Equal(200)) @@ -2182,6 +2190,10 @@ var _ = Describe("FileConnector", func() { Expect(body.UserExtraInfo).ToNot(BeNil()) Expect(body.UserExtraInfo.Avatar).To(ContainSubstring("/wopi/avatars/aabbcc?access_token=")) Expect(body.UserExtraInfo.Mail).To(Equal("shaft@example.com")) + // DownloadURL carries a fresh WOPI token, so check its shape separately + Expect(body.DownloadURL).To(HavePrefix("https://wopi.opencloud.test/wopi/files/")) + Expect(body.DownloadURL).To(ContainSubstring("/contents?access_token=")) + body.DownloadURL = "" // Clear dynamic fields for struct comparison body.UserExtraInfo.Avatar = "" expectedFileInfo.UserExtraInfo.Avatar = "" diff --git a/services/collaboration/pkg/connector/fileinfo/collabora.go b/services/collaboration/pkg/connector/fileinfo/collabora.go index 8c326da083..f4611fc515 100644 --- a/services/collaboration/pkg/connector/fileinfo/collabora.go +++ b/services/collaboration/pkg/connector/fileinfo/collabora.go @@ -52,6 +52,8 @@ type Collabora struct { // The page to navigate to when the editor closes or fails unrecoverably. CloseURL string `json:"CloseUrl,omitempty"` + // A link to download the original file. + DownloadURL string `json:"DownloadUrl,omitempty"` // The page that lets the user share the file. FileSharingURL string `json:"FileSharingUrl,omitempty"` // The page that shows the version history of the file. @@ -170,6 +172,8 @@ func (cinfo *Collabora) SetProperties(props map[string]any) { case KeyCloseURL: cinfo.CloseURL = value.(string) + case KeyDownloadURL: + cinfo.DownloadURL = value.(string) case KeyFileSharingURL: cinfo.FileSharingURL = value.(string) case KeyFileVersionURL: