Merge pull request #9580 from dragonchaser/disable_collabora_chat

Add more wopi features, language, disable chat
This commit is contained in:
Christian Richter
2024-07-12 11:32:56 +00:00
committed by GitHub
10 changed files with 137 additions and 28 deletions

View File

@@ -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

View File

@@ -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"`

View File

@@ -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%%"`
}

View File

@@ -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(),

View File

@@ -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",

View File

@@ -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")

View File

@@ -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",

View File

@@ -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

View File

@@ -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,

View File

@@ -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)))
})
})