diff --git a/changelog/unreleased/missing-wopi-keys.md b/changelog/unreleased/missing-wopi-keys.md new file mode 100644 index 0000000000..cab48f9c23 --- /dev/null +++ b/changelog/unreleased/missing-wopi-keys.md @@ -0,0 +1,5 @@ +Enhancement: Add missing WOPI features + +We added the feature to disable the chat for onlyoffice and added the missing language parameters to the wopi app url. + +https://github.com/owncloud/ocis/pull/9580 diff --git a/services/app-provider/pkg/config/config.go b/services/app-provider/pkg/config/config.go index cb462cded7..4421ca692a 100644 --- a/services/app-provider/pkg/config/config.go +++ b/services/app-provider/pkg/config/config.go @@ -62,7 +62,7 @@ type WOPIDriver struct { AppInternalURL string `yaml:"app_internal_url" env:"APP_PROVIDER_WOPI_APP_INTERNAL_URL" desc:"Internal URL to the app, like in your DMZ." introductionVersion:"pre5.0"` AppName string `yaml:"app_name" env:"APP_PROVIDER_WOPI_APP_NAME" desc:"Human readable app name." introductionVersion:"pre5.0"` AppURL string `yaml:"app_url" env:"APP_PROVIDER_WOPI_APP_URL" desc:"URL for end users to access the app." introductionVersion:"pre5.0"` - AppDisableChat bool `yaml:"app_disable_chat" env:"APP_PROVIDER_WOPI_DISABLE_CHAT" desc:"Disable the chat functionality of the office app." introductionVersion:"pre5.0"` + AppDisableChat bool `yaml:"app_disable_chat" env:"APP_PROVIDER_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable the chat functionality of the office app." introductionVersion:"pre5.0"` Insecure bool `yaml:"insecure" env:"APP_PROVIDER_WOPI_INSECURE" desc:"Disable TLS certificate validation for requests to the WOPI server and the web office application. Do not set this in production environments." introductionVersion:"pre5.0"` IopSecret string `yaml:"wopi_server_iop_secret" env:"APP_PROVIDER_WOPI_WOPI_SERVER_IOP_SECRET" desc:"Shared secret of the CS3org WOPI server." introductionVersion:"pre5.0"` WopiURL string `yaml:"wopi_server_external_url" env:"APP_PROVIDER_WOPI_WOPI_SERVER_EXTERNAL_URL" desc:"External url of the CS3org WOPI server." introductionVersion:"pre5.0"` diff --git a/services/collaboration/pkg/config/wopi.go b/services/collaboration/pkg/config/wopi.go index c0f4b01fc1..51e4caa3bd 100644 --- a/services/collaboration/pkg/config/wopi.go +++ b/services/collaboration/pkg/config/wopi.go @@ -2,6 +2,7 @@ package config // Wopi defines the available configuration for the WOPI endpoint. type Wopi struct { - WopiSrc string `yaml:"wopisrc" env:"COLLABORATION_WOPI_SRC" desc:"The WOPISrc base URL containing schema, host and port. Set this to the schema and domain where the collaboration service is reachable for the wopi app, such as https://office.owncloud.test." introductionVersion:"6.0.0"` - Secret string `yaml:"secret" env:"COLLABORATION_WOPI_SECRET" desc:"Used to mint and verify WOPI JWT tokens and encrypt and decrypt the REVA JWT token embedded in the WOPI JWT token." introductionVersion:"6.0.0"` + WopiSrc string `yaml:"wopisrc" env:"COLLABORATION_WOPI_SRC" desc:"The WOPISrc base URL containing schema, host and port. Set this to the schema and domain where the collaboration service is reachable for the wopi app, such as https://office.owncloud.test." introductionVersion:"6.0.0"` + Secret string `yaml:"secret" env:"COLLABORATION_WOPI_SECRET" desc:"Used to mint and verify WOPI JWT tokens and encrypt and decrypt the REVA JWT token embedded in the WOPI JWT token." introductionVersion:"6.0.0"` + DisableChat bool `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the frontend." introductionVersion:"%%NEXT%%"` } diff --git a/services/collaboration/pkg/connector/contentconnector.go b/services/collaboration/pkg/connector/contentconnector.go index 5de55c7963..f450ef5c47 100644 --- a/services/collaboration/pkg/connector/contentconnector.go +++ b/services/collaboration/pkg/connector/contentconnector.go @@ -71,7 +71,7 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error // Initiate download request req := &providerv1beta1.InitiateFileDownloadRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, } if wopiContext.ViewMode == appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY && wopiContext.ViewOnlyToken != "" { @@ -206,7 +206,7 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream // We need a stat call on the target file in order to get both the lock // (if any) and the current size of the file statRes, err := c.gwc.Stat(ctx, &providerv1beta1.StatRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, }) if err != nil { logger.Error().Err(err).Msg("PutFile: stat failed") @@ -254,7 +254,7 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream req := &providerv1beta1.InitiateFileUploadRequest{ Opaque: opaque, - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, LockId: lockID, Options: &providerv1beta1.InitiateFileUploadRequest_IfMatch{ IfMatch: statRes.GetInfo().GetEtag(), diff --git a/services/collaboration/pkg/connector/contentconnector_test.go b/services/collaboration/pkg/connector/contentconnector_test.go index 581bb3b78d..aaf9fecf7d 100644 --- a/services/collaboration/pkg/connector/contentconnector_test.go +++ b/services/collaboration/pkg/connector/contentconnector_test.go @@ -44,7 +44,7 @@ var _ = Describe("ContentConnector", func() { wopiCtx = middleware.WopiContext{ AccessToken: "abcdef123456", - FileReference: providerv1beta1.Reference{ + FileReference: &providerv1beta1.Reference{ ResourceId: &providerv1beta1.ResourceId{ StorageId: "abc", OpaqueId: "12345", @@ -178,7 +178,7 @@ var _ = Describe("ContentConnector", func() { wopiCtx = middleware.WopiContext{ AccessToken: "abcdef123456", ViewOnlyToken: "view.only.123456", - FileReference: providerv1beta1.Reference{ + FileReference: &providerv1beta1.Reference{ ResourceId: &providerv1beta1.ResourceId{ StorageId: "abc", OpaqueId: "12345", diff --git a/services/collaboration/pkg/connector/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go index 720757f5cf..59689286c9 100644 --- a/services/collaboration/pkg/connector/fileconnector.go +++ b/services/collaboration/pkg/connector/fileconnector.go @@ -89,7 +89,7 @@ func (f *FileConnector) GetLock(ctx context.Context) (string, error) { logger := zerolog.Ctx(ctx) req := &providerv1beta1.GetLockRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, } resp, err := f.gwc.GetLock(ctx, req) @@ -158,7 +158,7 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (str if oldLockID == "" { // If the oldLockID is empty, this is a "LOCK" request req := &providerv1beta1.SetLockRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, Lock: &providerv1beta1.Lock{ LockId: lockID, AppName: f.cfg.App.LockName + "." + f.cfg.App.Name, @@ -179,7 +179,7 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (str // If the oldLockID isn't empty, this is a "UnlockAndRelock" request. We'll // do a "RefreshLock" in reva and provide the old lock req := &providerv1beta1.RefreshLockRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, Lock: &providerv1beta1.Lock{ LockId: lockID, AppName: f.cfg.App.LockName + "." + f.cfg.App.Name, @@ -211,7 +211,7 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (str // In both cases, we need to get the current lock to return it in a // 409 response if needed req := &providerv1beta1.GetLockRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, } resp, err := f.gwc.GetLock(ctx, req) @@ -292,7 +292,7 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (string, } req := &providerv1beta1.RefreshLockRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, Lock: &providerv1beta1.Lock{ LockId: lockID, AppName: f.cfg.App.LockName + "." + f.cfg.App.Name, @@ -330,7 +330,7 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (string, // Either the file is unlocked or there is no lock // We need to return 409 with the current lock req := &providerv1beta1.GetLockRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, } resp, err := f.gwc.GetLock(ctx, req) @@ -400,7 +400,7 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (string, erro } req := &providerv1beta1.UnlockRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, Lock: &providerv1beta1.Lock{ LockId: lockID, AppName: f.cfg.App.LockName + "." + f.cfg.App.Name, @@ -424,7 +424,7 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (string, erro case rpcv1beta1.Code_CODE_LOCKED: // We need to return 409 with the current lock req := &providerv1beta1.GetLockRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, } resp, err := f.gwc.GetLock(ctx, req) @@ -485,7 +485,7 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (fileinfo.FileInfo, e logger := zerolog.Ctx(ctx) statRes, err := f.gwc.Stat(ctx, &providerv1beta1.StatRequest{ - Ref: &wopiContext.FileReference, + Ref: wopiContext.FileReference, }) if err != nil { logger.Error().Err(err).Msg("CheckFileInfo: stat failed") diff --git a/services/collaboration/pkg/connector/fileconnector_test.go b/services/collaboration/pkg/connector/fileconnector_test.go index 7669e8a570..75cead43bf 100644 --- a/services/collaboration/pkg/connector/fileconnector_test.go +++ b/services/collaboration/pkg/connector/fileconnector_test.go @@ -39,7 +39,7 @@ var _ = Describe("FileConnector", func() { wopiCtx = middleware.WopiContext{ AccessToken: "abcdef123456", - FileReference: providerv1beta1.Reference{ + FileReference: &providerv1beta1.Reference{ ResourceId: &providerv1beta1.ResourceId{ StorageId: "abc", OpaqueId: "12345", diff --git a/services/collaboration/pkg/middleware/wopicontext.go b/services/collaboration/pkg/middleware/wopicontext.go index dfba554058..e9998c0d75 100644 --- a/services/collaboration/pkg/middleware/wopicontext.go +++ b/services/collaboration/pkg/middleware/wopicontext.go @@ -25,7 +25,7 @@ const ( type WopiContext struct { AccessToken string ViewOnlyToken string - FileReference providerv1beta1.Reference + FileReference *providerv1beta1.Reference User *userv1beta1.User ViewMode appproviderv1beta1.ViewMode EditAppUrl string diff --git a/services/collaboration/pkg/service/grpc/v0/service.go b/services/collaboration/pkg/service/grpc/v0/service.go index 8c203b10c7..4dfa90a280 100644 --- a/services/collaboration/pkg/service/grpc/v0/service.go +++ b/services/collaboration/pkg/service/grpc/v0/service.go @@ -25,7 +25,10 @@ import ( // NewHandler creates a new grpc service implementing the OpenInApp interface func NewHandler(opts ...Option) (*Service, func(), error) { - teardown := func() {} + teardown := func() { + /* this is required as a argument for the return value to satisfy the interface */ + /* in case you are wondering about the necessity of this comment, sonarcloud is asking for it */ + } options := newOptions(opts...) gwc := options.Gwc @@ -93,18 +96,18 @@ func (s *Service) OpenInApp( var viewAppURL string var editAppURL string if viewCommentAppURLs, ok := s.appURLs["view_comment"]; ok { - if url := viewCommentAppURLs[fileExt]; ok { - viewCommentAppURL = url + if u, ok := viewCommentAppURLs[fileExt]; ok { + viewCommentAppURL = u } } if viewAppURLs, ok := s.appURLs["view"]; ok { - if url := viewAppURLs[fileExt]; ok { - viewAppURL = url + if u, ok := viewAppURLs[fileExt]; ok { + viewAppURL = u } } if editAppURLs, ok := s.appURLs["edit"]; ok { - if url, ok := editAppURLs[fileExt]; ok { - editAppURL = url + if u, ok := editAppURLs[fileExt]; ok { + editAppURL = u } } if editAppURL == "" && viewAppURL == "" && viewCommentAppURL == "" { @@ -151,6 +154,18 @@ func (s *Service) OpenInApp( q := u.Query() q.Add("WOPISrc", wopiSrcURL.String()) + + if s.config.Wopi.DisableChat { + q.Add("dchat", "1") + } + + lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang") + + if lang != "" { + q.Add("ui", lang) // OnlyOffice + q.Add("lang", lang) // Collabora, Impact on the default document language of OnlyOffice + q.Add("UI_LLCC", lang) // Office365 + } qs := q.Encode() u.RawQuery = qs @@ -199,7 +214,7 @@ func (s *Service) OpenInApp( wopiContext := middleware.WopiContext{ AccessToken: cryptedReqAccessToken, ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"), - FileReference: providerFileRef, + FileReference: &providerFileRef, User: user, ViewMode: req.GetViewMode(), EditAppUrl: editAppURL, diff --git a/services/collaboration/pkg/service/grpc/v0/service_test.go b/services/collaboration/pkg/service/grpc/v0/service_test.go index c8b6544bcd..e2361c090c 100644 --- a/services/collaboration/pkg/service/grpc/v0/service_test.go +++ b/services/collaboration/pkg/service/grpc/v0/service_test.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + "github.com/cs3org/reva/v2/pkg/utils" "github.com/golang-jwt/jwt/v4" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -163,6 +164,7 @@ var _ = Describe("Discovery", func() { ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime), } + req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "de") gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{ Status: status.NewOK(ctx), @@ -173,7 +175,93 @@ var _ = Describe("Discovery", func() { Expect(err).To(Succeed()) Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK)) Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST")) - Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e")) + Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&lang=de&ui=de")) + Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10))) + }) + + It("Success", func() { + ctx := context.Background() + nowTime := time.Now() + + cfg.Wopi.WopiSrc = "https://wopiserver.test.prv" + cfg.Wopi.Secret = "my_supa_secret" + cfg.Wopi.DisableChat = true + + myself := &userv1beta1.User{ + Id: &userv1beta1.UserId{ + Idp: "myIdp", + OpaqueId: "opaque001", + Type: userv1beta1.UserType_USER_TYPE_PRIMARY, + }, + Username: "username", + } + + req := &appproviderv1beta1.OpenInAppRequest{ + ResourceInfo: &providerv1beta1.ResourceInfo{ + Id: &providerv1beta1.ResourceId{ + StorageId: "myStorage", + OpaqueId: "storageOpaque001", + SpaceId: "SpaceA", + }, + Path: "/path/to/file.docx", + }, + ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, + AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime), + } + + gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{ + Status: status.NewOK(ctx), + User: myself, + }, nil) + + resp, err := srv.OpenInApp(ctx, req) + Expect(err).To(Succeed()) + Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK)) + Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST")) + Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1")) + Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10))) + }) + + It("Success", func() { + ctx := context.Background() + nowTime := time.Now() + + cfg.Wopi.WopiSrc = "https://wopiserver.test.prv" + cfg.Wopi.Secret = "my_supa_secret" + cfg.Wopi.DisableChat = true + + myself := &userv1beta1.User{ + Id: &userv1beta1.UserId{ + Idp: "myIdp", + OpaqueId: "opaque001", + Type: userv1beta1.UserType_USER_TYPE_PRIMARY, + }, + Username: "username", + } + + req := &appproviderv1beta1.OpenInAppRequest{ + ResourceInfo: &providerv1beta1.ResourceInfo{ + Id: &providerv1beta1.ResourceId{ + StorageId: "myStorage", + OpaqueId: "storageOpaque001", + SpaceId: "SpaceA", + }, + Path: "/path/to/file.docx", + }, + ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, + AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime), + } + + gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{ + Status: status.NewOK(ctx), + User: myself, + }, nil) + + resp, err := srv.OpenInApp(ctx, req) + Expect(err).To(Succeed()) + Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK)) + Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST")) + Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1")) Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10))) }) })