Repository: owncloud/ocis
Pull request #9580 (2024-07-10 11:35:52)

Add more wopi features, language, disable chat
# Description

Implementation of #9398 
Related to #6540 (which was used in the former reva app provider)

## Added Features

- [x] Disable Chat
- [x] Language params
  - [x]  `lang` (Collabora)
  - [x]  `ui` (OnlyOffice)
  - [x] `UI_LLCC` (Office365)

https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/online/discovery#ui_llcc
https://api.onlyoffice.com/editors/wopi/discovery#ui
https://api.onlyoffice.com/editors/wopi/discovery#lang
https://sdk.collaboraonline.com/docs/advanced_integration.html#url-query-parameters


## Tested

- ✅ Disable Chat in `ocis_full` example with onlyoffice
- ✅  Change Language in `ocis_full` example with onlyoffice
- ✅  Change Language in `ocis_full` example with collabora

update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [3 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9580&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9580&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9580&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9580&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9580&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [1 New Bugs](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) (required ≤ 0)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [1 New Bugs](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) (required ≤ 0)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [1 New Bugs](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) (required ≤ 0)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9580&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9580&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9580&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9580&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9580&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9580&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9580&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9580&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [86.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9580&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9580&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9580&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9580&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=9580&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [86.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9580&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9580&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9580)



Patch:
diff --git a/changelog/unreleased/missing-wopi-keys.md b/changelog/unreleased/missing-wopi-keys.md
new file mode 100644
index 00000000000..cab48f9c235
--- /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 cb462cded7c..4421ca692af 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 c0f4b01fc11..51e4caa3bd5 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 5de55c79634..f450ef5c479 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/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go
index 720757f5cfc..59689286c99 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/middleware/wopicontext.go b/services/collaboration/pkg/middleware/wopicontext.go
index dfba5540589..e9998c0d75c 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 8c203b10c77..4dfa90a2805 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,


Test patch:
diff --git a/services/collaboration/pkg/connector/contentconnector_test.go b/services/collaboration/pkg/connector/contentconnector_test.go
index 581bb3b78d6..aaf9fecf7de 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_test.go b/services/collaboration/pkg/connector/fileconnector_test.go
index 7669e8a570c..75cead43bf0 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/service/grpc/v0/service_test.go b/services/collaboration/pkg/service/grpc/v0/service_test.go
index c8b6544bcd7..e2361c090c5 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)))
 		})
 	})