Repository: owncloud/ocis
Pull request #9827 (2024-08-16 09:15:03)

Collaboration openinapp refactor
<!--
Thanks for submitting a change to ownCloud!

This is the bug tracker for the oCIS component. Find other components at https://github.com/owncloud/core/blob/master/.github/CONTRIBUTING.md#guidelines

For fixing potential security issues please see https://owncloud.org/security/

To make it possible for us to get your change reviewed and merged please carefully fill out the requested information below.

Please note that any kind of change needs first be submitted to the master branch which holds the next version of oCIS.

Please set the following labels:

- Set label "Status:Needs-Review" for review or "Status:In-Progress" if the PR still has open tasks.
- Assignment: assign to self
- Reviewers: pick at least one
-->

## Description
Simplify `OpenInApp` method. This contains:
* Custom sublogger to compact log calls
* Simplify and move the code to get the appURL
* Move the code to generate the query params for the appURL

We've also temporarily removed the `HostViewURL` and `HostEditURL` for 2 reasons:
* Current values are wrong. The values are expected to point to an ocis URL, not to Collabora (or other WOPI app)
* It keeps the `OpenInApp` code simpler

Those values might can back with https://github.com/owncloud/ocis/pull/9686

## Related Issue
No opened issue

## Motivation and Context
Code is simpler and easier to follow

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
- test environment:
- test case 1:
- test case 2:
- ...

## Screenshots (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [ ] Code changes
- [ ] Unit tests added
- [ ] Acceptance tests added
- [ ] Documentation ticket raised: <link> 


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=9827) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9827&issueStatuses=OPEN,CONFIRMED&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=9827&issueStatuses=ACCEPTED)

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=9827&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [87.3% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9827&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=9827&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9827)


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=9827) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=9827&issueStatuses=OPEN,CONFIRMED&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=9827&issueStatuses=ACCEPTED)

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=9827&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [87.3% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=9827&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=9827&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=9827)



Patch:
diff --git a/services/collaboration/pkg/connector/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go
index 0f9d42f67d2..d23e93c7358 100644
--- a/services/collaboration/pkg/connector/fileconnector.go
+++ b/services/collaboration/pkg/connector/fileconnector.go
@@ -1050,8 +1050,9 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
 		// to get the folder we actually need to do a GetPath() request
 		//BreadcrumbFolderName: path.Dir(statRes.Info.Path),
 
-		fileinfo.KeyHostViewURL: wopiContext.ViewAppUrl,
-		fileinfo.KeyHostEditURL: wopiContext.EditAppUrl,
+		// TODO: these URLs must point to ocis, which is hosting the editor's iframe
+		//fileinfo.KeyHostViewURL: wopiContext.ViewAppUrl,
+		//fileinfo.KeyHostEditURL: wopiContext.EditAppUrl,
 
 		fileinfo.KeyEnableOwnerTermination:     true, // only for collabora
 		fileinfo.KeySupportsExtendedLockLength: true,
@@ -1061,7 +1062,6 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
 		fileinfo.KeySupportsDeleteFile:         true,
 		fileinfo.KeySupportsRename:             true,
 
-		//fileinfo.KeyUserCanNotWriteRelative: true,
 		fileinfo.KeyIsAnonymousUser:  isAnonymousUser,
 		fileinfo.KeyUserFriendlyName: userFriendlyName,
 		fileinfo.KeyUserID:           userId,
diff --git a/services/collaboration/pkg/middleware/wopicontext.go b/services/collaboration/pkg/middleware/wopicontext.go
index 1232da0317b..e6b9dff0afe 100644
--- a/services/collaboration/pkg/middleware/wopicontext.go
+++ b/services/collaboration/pkg/middleware/wopicontext.go
@@ -31,8 +31,6 @@ type WopiContext struct {
 	FileReference *providerv1beta1.Reference
 	User          *userv1beta1.User
 	ViewMode      appproviderv1beta1.ViewMode
-	EditAppUrl    string
-	ViewAppUrl    string
 }
 
 // WopiContextAuthMiddleware will prepare an HTTP handler to be used as
diff --git a/services/collaboration/pkg/service/grpc/v0/service.go b/services/collaboration/pkg/service/grpc/v0/service.go
index 1568efe0ad2..1458a4e27e7 100644
--- a/services/collaboration/pkg/service/grpc/v0/service.go
+++ b/services/collaboration/pkg/service/grpc/v0/service.go
@@ -2,10 +2,11 @@ package service
 
 import (
 	"context"
-	"fmt"
+	"errors"
 	"net/url"
 	"path"
 	"strconv"
+	"strings"
 
 	appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
 	gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
@@ -80,147 +81,47 @@ func (s *Service) OpenInApp(
 		Path:       ".",
 	}
 
-	// build a urlsafe and stable file reference that can be used for proxy routing,
-	// so that all sessions on one file end on the same office server
-	fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())
+	logger := s.logger.With().
+		Str("FileReference", providerFileRef.String()).
+		Str("ViewMode", req.GetViewMode().String()).
+		Str("Requester", user.GetId().String()).
+		Logger()
 
 	// get the file extension to use the right wopi app url
 	fileExt := path.Ext(req.GetResourceInfo().GetPath())
 
-	var viewCommentAppURL string
-	var viewAppURL string
-	var editAppURL string
-	if viewCommentAppURLs, ok := s.appURLs["view_comment"]; ok {
-		if u, ok := viewCommentAppURLs[fileExt]; ok {
-			viewCommentAppURL = u
-		}
-	}
-	if viewAppURLs, ok := s.appURLs["view"]; ok {
-		if u, ok := viewAppURLs[fileExt]; ok {
-			viewAppURL = u
-		}
-	}
-	if editAppURLs, ok := s.appURLs["edit"]; ok {
-		if u, ok := editAppURLs[fileExt]; ok {
-			editAppURL = u
-		}
-	}
-	if editAppURL == "" && viewAppURL == "" && viewCommentAppURL == "" {
-		err := fmt.Errorf("OpenInApp: neither edit nor view app url found")
-		s.logger.Error().
-			Err(err).
-			Str("FileReference", providerFileRef.String()).
-			Str("ViewMode", req.GetViewMode().String()).
-			Str("Requester", user.GetId().String()).Send()
-		return nil, err
+	// get the appURL we need to use
+	appURL := s.getAppUrl(fileExt, req.GetViewMode())
+	if appURL == "" {
+		logger.Error().Msg("OpenInApp: neither edit nor view app URL found")
+		return nil, errors.New("neither edit nor view app URL found")
 	}
 
-	if editAppURL == "" {
-		// assuming that an view action is always available in the /hosting/discovery manifest
-		// eg. Collabora does support viewing jpgs but no editing
-		// eg. OnlyOffice does support viewing pdfs but no editing
-		// there is no known case of supporting edit only without view
-		editAppURL = viewAppURL
-	}
-	if viewAppURL == "" {
-		// the URL of the end-user application in view mode when different (defaults to edit mod URL)
-		viewAppURL = editAppURL
-	}
-	// TODO: check if collabora will support an "edit" url in the future
-	if viewAppURL == "" && editAppURL == "" && viewCommentAppURL != "" {
-		// there are rare cases where neither view nor edit is supported but view_comment is
-		viewAppURL = viewCommentAppURL
-		// that can be the case for editable and viewable files
-		if req.GetViewMode() == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
-			editAppURL = viewCommentAppURL
-		}
-	}
-	wopiSrcURL, err := url.Parse(s.config.Wopi.WopiSrc)
-	if err != nil {
-		return nil, err
-	}
-	wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
-
-	addWopiSrcQueryParam := func(baseURL string) (string, error) {
-		u, err := url.Parse(baseURL)
-		if err != nil {
-			return "", err
-		}
-
-		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
-
-		return u.String(), nil
-	}
-
-	viewAppURL, err = addWopiSrcQueryParam(viewAppURL)
-	if err != nil {
-		s.logger.Error().
-			Err(err).
-			Str("FileReference", providerFileRef.String()).
-			Str("ViewMode", req.GetViewMode().String()).
-			Str("Requester", user.GetId().String()).
-			Msg("OpenInApp: error parsing viewAppUrl")
-		return nil, err
-	}
-	editAppURL, err = addWopiSrcQueryParam(editAppURL)
+	// append the parameters we need
+	appURL, err = s.addQueryToURL(appURL, req)
 	if err != nil {
-		s.logger.Error().
-			Err(err).
-			Str("FileReference", providerFileRef.String()).
-			Str("ViewMode", req.GetViewMode().String()).
-			Str("Requester", user.GetId().String()).
-			Msg("OpenInApp: error parsing editAppUrl")
+		logger.Error().Err(err).Msg("OpenInApp: error parsing appUrl")
 		return nil, err
 	}
 
-	appURL := viewAppURL
-	if req.GetViewMode() == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
-		appURL = editAppURL
-	}
-
+	// create the wopiContext and generate the token
 	wopiContext := middleware.WopiContext{
 		AccessToken:   req.GetAccessToken(), // it will be encrypted
 		ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"),
 		FileReference: &providerFileRef,
 		User:          user,
 		ViewMode:      req.GetViewMode(),
-		EditAppUrl:    editAppURL,
-		ViewAppUrl:    viewAppURL,
 	}
 
 	accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config)
 	if err != nil {
-		s.logger.Error().
-			Err(err).
-			Str("FileReference", providerFileRef.String()).
-			Str("ViewMode", req.GetViewMode().String()).
-			Str("Requester", user.GetId().String()).
-			Msg("OpenInApp: error generating the token")
+		logger.Error().Err(err).Msg("OpenInApp: error generating the token")
 		return &appproviderv1beta1.OpenInAppResponse{
 			Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_INTERNAL},
 		}, err
 	}
 
-	s.logger.Debug().
-		Str("FileReference", providerFileRef.String()).
-		Str("ViewMode", req.GetViewMode().String()).
-		Str("Requester", user.GetId().String()).
-		Msg("OpenInApp: success")
+	logger.Debug().Msg("OpenInApp: success")
 
 	return &appproviderv1beta1.OpenInAppResponse{
 		Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_OK},
@@ -237,3 +138,97 @@ func (s *Service) OpenInApp(
 		},
 	}, nil
 }
+
+// getAppUrlFor gets the appURL from the list of appURLs based on the
+// action and file extension provided. If there is no match, an empty
+// string will be returned.
+func (s *Service) getAppUrlFor(action, fileExt string) string {
+	if actionURL, ok := s.appURLs[action]; ok {
+		if actionExtensionURL, ok := actionURL[fileExt]; ok {
+			return actionExtensionURL
+		}
+	}
+	return ""
+}
+
+// getAppUrl will get the appURL that should be used based on the extension
+// and the provided view mode.
+// "view" urls will be chosen first, then if the view mode is "read/write",
+// "edit" urls will be prioritized. Note that "view" url might be returned for
+// "read/write" view mode if no "edit" url is found.
+func (s *Service) getAppUrl(fileExt string, viewMode appproviderv1beta1.ViewMode) string {
+	// prioritize view action if possible
+	appURL := s.getAppUrlFor("view", fileExt)
+
+	if strings.ToLower(s.config.App.Name) == "collabora" {
+		// collabora provides only one action per extension. usual options
+		// are "view" (checked above), "edit" or "view_comment" (this last one
+		// is exclusive of collabora)
+		if appURL == "" {
+			if editURL := s.getAppUrlFor("edit", fileExt); editURL != "" {
+				return editURL
+			}
+			if commentURL := s.getAppUrlFor("view_comment", fileExt); commentURL != "" {
+				return commentURL
+			}
+		}
+	} else {
+		// If not collabora, there might be an edit action for the extension.
+		// If read/write mode has been requested, prioritize edit action.
+		if viewMode == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
+			if editAppURL := s.getAppUrlFor("edit", fileExt); editAppURL != "" {
+				appURL = editAppURL
+			}
+		}
+	}
+
+	return appURL
+}
+
+// addQueryToURL will add specific query parameters to the baseURL. These
+// parameters are:
+// * "WOPISrc" pointing to the requested resource in the OpenInAppRequest
+// * "dchat" to disable the chat, based on configuration
+// * "lang" (WOPI app dependent) with the language in the request. "lang"
+// for collabora, "ui" for onlyoffice and "UI_LLCC" for the rest
+func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAppRequest) (string, error) {
+	u, err := url.Parse(baseURL)
+	if err != nil {
+		return "", err
+	}
+
+	// build a urlsafe and stable file reference that can be used for proxy routing,
+	// so that all sessions on one file end on the same office server
+	fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())
+
+	wopiSrcURL, err := url.Parse(s.config.Wopi.WopiSrc)
+	if err != nil {
+		return "", err
+	}
+	wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
+
+	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 != "" {
+		switch strings.ToLower(s.config.App.Name) {
+		case "collabora":
+			q.Add("lang", lang)
+		case "onlyoffice":
+			q.Add("ui", lang)
+		default:
+			q.Add("UI_LLCC", lang)
+		}
+	}
+
+	qs := q.Encode()
+	u.RawQuery = qs
+
+	return u.String(), nil
+}


Test patch:
diff --git a/services/collaboration/pkg/connector/contentconnector_test.go b/services/collaboration/pkg/connector/contentconnector_test.go
index 38f308afcf3..82ba9e9c42f 100644
--- a/services/collaboration/pkg/connector/contentconnector_test.go
+++ b/services/collaboration/pkg/connector/contentconnector_test.go
@@ -52,10 +52,8 @@ var _ = Describe("ContentConnector", func() {
 				},
 				Path: ".",
 			},
-			User:       &userv1beta1.User{}, // Not used for now
-			ViewMode:   appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
-			EditAppUrl: "http://test.ex.prv/edit",
-			ViewAppUrl: "http://test.ex.prv/view",
+			User:     &userv1beta1.User{}, // Not used for now
+			ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
 		}
 
 		randomContent = "This is the content of the test.txt file"
@@ -186,10 +184,8 @@ var _ = Describe("ContentConnector", func() {
 					},
 					Path: ".",
 				},
-				User:       &userv1beta1.User{}, // Not used for now
-				ViewMode:   appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY,
-				EditAppUrl: "http://test.ex.prv/edit",
-				ViewAppUrl: "http://test.ex.prv/view",
+				User:     &userv1beta1.User{}, // Not used for now
+				ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY,
 			}
 
 			ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
diff --git a/services/collaboration/pkg/connector/fileconnector_test.go b/services/collaboration/pkg/connector/fileconnector_test.go
index 2f74ebb1419..eb1ec5462c2 100644
--- a/services/collaboration/pkg/connector/fileconnector_test.go
+++ b/services/collaboration/pkg/connector/fileconnector_test.go
@@ -82,9 +82,7 @@ var _ = Describe("FileConnector", func() {
 				//	},
 				//},
 			},
-			ViewMode:   appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
-			EditAppUrl: "http://test.ex.prv/edit",
-			ViewAppUrl: "http://test.ex.prv/view",
+			ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
 		}
 	})
 
@@ -1546,8 +1544,6 @@ var _ = Describe("FileConnector", func() {
 				BaseFileName:               "test.txt",
 				BreadcrumbDocName:          "test.txt",
 				UserCanNotWriteRelative:    false,
-				HostViewURL:                "http://test.ex.prv/view",
-				HostEditURL:                "http://test.ex.prv/edit",
 				SupportsExtendedLockLength: true,
 				SupportsGetLock:            true,
 				SupportsLocks:              true,
diff --git a/services/collaboration/pkg/service/grpc/v0/service_test.go b/services/collaboration/pkg/service/grpc/v0/service_test.go
index e2361c090c5..68f5aa1416b 100644
--- a/services/collaboration/pkg/service/grpc/v0/service_test.go
+++ b/services/collaboration/pkg/service/grpc/v0/service_test.go
@@ -136,133 +136,66 @@ var _ = Describe("Discovery", func() {
 			Expect(resp).To(BeNil())
 		})
 
-		It("Success", func() {
-			ctx := context.Background()
-			nowTime := time.Now()
-
-			cfg.Wopi.WopiSrc = "https://wopiserver.test.prv"
-			cfg.Wopi.Secret = "my_supa_secret"
-
-			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),
-			}
-			req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "de")
-
-			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?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",
+		DescribeTable(
+			"Success",
+			func(appName, lang string, disableChat bool, expectedAppUrl string) {
+				ctx := context.Background()
+				nowTime := time.Now()
+
+				cfg.Wopi.WopiSrc = "https://wopiserver.test.prv"
+				cfg.Wopi.Secret = "my_supa_secret"
+				cfg.Wopi.DisableChat = disableChat
+				cfg.App.Name = appName
+
+				myself := &userv1beta1.User{
+					Id: &userv1beta1.UserId{
+						Idp:      "myIdp",
+						OpaqueId: "opaque001",
+						Type:     userv1beta1.UserType_USER_TYPE_PRIMARY,
 					},
-					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",
+					Username: "username",
+				}
+
+				req := &appproviderv1beta1.OpenInAppRequest{
+					ResourceInfo: &providerv1beta1.ResourceInfo{
+						Id: &providerv1beta1.ResourceId{
+							StorageId: "myStorage",
+							OpaqueId:  "storageOpaque001",
+							SpaceId:   "SpaceA",
+						},
+						Path: "/path/to/file.docx",
 					},
-					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)))
-		})
+					ViewMode:    appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
+					AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
+				}
+				if lang != "" {
+					req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", lang)
+				}
+
+				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(expectedAppUrl))
+				Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
+			},
+			Entry("Microsoft chat no lang", "Microsoft", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
+			Entry("Collabora chat no lang", "Collabora", "", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
+			Entry("OnlyOffice chat no lang", "OnlyOffice", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
+			Entry("Microsoft chat lang", "Microsoft", "de", false, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
+			Entry("Collabora chat lang", "Collabora", "de", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&lang=de"),
+			Entry("OnlyOffice chat lang", "OnlyOffice", "de", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=de"),
+			Entry("Microsoft no chat no lang", "Microsoft", "", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
+			Entry("Collabora no chat no lang", "Collabora", "", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
+			Entry("OnlyOffice no chat no lang", "OnlyOffice", "", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
+			Entry("Microsoft no chat lang", "Microsoft", "de", true, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
+			Entry("Collabora no chat lang", "Collabora", "de", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&lang=de"),
+			Entry("OnlyOffice no chat lang", "OnlyOffice", "de", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&ui=de"),
+		)
 	})
 })