From 8edd60296a287e7fa163ab26794c5743c138471c Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Wed, 30 Sep 2026 16:55:34 +0200 Subject: [PATCH] feat(collaboration): mobile web view for EuroOffice --- services/collaboration/README.md | 6 ++ services/collaboration/pkg/config/app.go | 2 + services/collaboration/pkg/config/wopi.go | 2 + .../pkg/service/grpc/v0/service.go | 14 +++++ .../pkg/service/grpc/v0/service_test.go | 63 +++++++++++++++++++ 5 files changed, 87 insertions(+) diff --git a/services/collaboration/README.md b/services/collaboration/README.md index df0b893f39..3983ac45e1 100644 --- a/services/collaboration/README.md +++ b/services/collaboration/README.md @@ -32,6 +32,9 @@ There are a few variables that you need to set: The product name of the connected WebOffice app, which can be one of the following:\ `Collabora`, `OnlyOffice`, `Microsoft365` or `MicrosoftOfficeOnline`. This is used to internally control the behavior according to the different features of the used products. +* `COLLABORATION_APP_PRODUCT_EDITION`:\ + The edition of the connected WebOffice app, it decides which features the app offers. Only used for EuroOffice, which supports `ce` (community edition), `de` (developer edition) and `ee` (enterprise edition). An empty value is the same as `ce`. + * `COLLABORATION_APP_ADDR`:\ The URL of the collaborative editing app (onlyoffice, collabora, etc).\ For example: `https://office.example.com`. @@ -46,6 +49,9 @@ There are a few variables that you need to set: * `COLLABORATION_WOPI_SHORTTOKENS`:\ Needs to be set if the office application like `Microsoft Office Online` complains about the URL is too long (which contains the access token) and refuses to work. If enabled, a store must be configured. +* `COLLABORATION_WOPI_ENABLE_MOBILE`:\ + Enables the mobile web view of the office web frontend. Only applies to EuroOffice. `ce` offers the mobile view for reading only, `de` and `ee` also for editing, so set `COLLABORATION_APP_PRODUCT_EDITION` accordingly. + The application can be customized further by changing the `COLLABORATION_APP_*` options to better describe the application. ## Storing diff --git a/services/collaboration/pkg/config/app.go b/services/collaboration/pkg/config/app.go index 5e9646d18c..f7ced7c2b0 100644 --- a/services/collaboration/pkg/config/app.go +++ b/services/collaboration/pkg/config/app.go @@ -12,6 +12,8 @@ type App struct { ProofKeys ProofKeys `yaml:"proofkeys"` LicenseCheckEnable bool `yaml:"licensecheckenable" env:"COLLABORATION_APP_LICENSE_CHECK_ENABLE" desc:"Enable license checking to edit files. Needs to be enabled when using Microsoft365 with the business flow." introductionVersion:"1.0.0"` + + ProductEdition string `yaml:"product_edition" env:"COLLABORATION_APP_PRODUCT_EDITION" desc:"The edition of the WebOffice app, it decides which features the app offers. Only used for EuroOffice, where 'ce', 'de' and 'ee' are supported and an empty value is the same as 'ce'." introductionVersion:"%%NEXT%%"` } type ProofKeys struct { diff --git a/services/collaboration/pkg/config/wopi.go b/services/collaboration/pkg/config/wopi.go index 294fdbc292..b8ca0f09ee 100644 --- a/services/collaboration/pkg/config/wopi.go +++ b/services/collaboration/pkg/config/wopi.go @@ -8,4 +8,6 @@ type Wopi struct { ProxyURL string `yaml:"proxy_url" env:"COLLABORATION_WOPI_PROXY_URL" desc:"The URL to the OpenCloud WOPI proxy. Optional. To use this feature, you need an office365 proxy subscription. If you become part of the Microsoft CSP program (https://learn.microsoft.com/en-us/partner-center/enroll/csp-overview), you can use WebOffice without a proxy." introductionVersion:"1.0.0"` ProxySecret string `yaml:"proxy_secret" env:"COLLABORATION_WOPI_PROXY_SECRET" desc:"Optional, the secret to authenticate against the OpenCloud WOPI proxy. This secret can be obtained from OpenCloud via the office365 proxy subscription." introductionVersion:"1.0.0"` ShortTokens bool `yaml:"short_tokens" env:"COLLABORATION_WOPI_SHORTTOKENS" desc:"Use short access tokens for WOPI access. This is useful for office packages, like Microsoft Office Online, which have URL length restrictions. If enabled, a persistent store must be configured." introductionVersion:"1.0.0"` + + EnableMobile bool `yaml:"enable_mobile" env:"COLLABORATION_WOPI_ENABLE_MOBILE" desc:"Enable the mobile web view of the office web frontend. This feature applies to EuroOffice, where the product edition decides whether it covers editing as well." introductionVersion:"%%NEXT%%"` } diff --git a/services/collaboration/pkg/service/grpc/v0/service.go b/services/collaboration/pkg/service/grpc/v0/service.go index b68be18d9d..cfef7f9d3b 100644 --- a/services/collaboration/pkg/service/grpc/v0/service.go +++ b/services/collaboration/pkg/service/grpc/v0/service.go @@ -239,6 +239,20 @@ func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAp q.Add("dchat", "1") } + // EuroOffice ce offers the mobile view for reading only, ee and de also for editing. + if s.config.Wopi.EnableMobile && strings.ToLower(s.config.App.Product) == "onlyoffice" { + if mobile := utils.ReadPlainFromOpaque(req.GetOpaque(), "mobile"); mobile != "" { + switch strings.ToLower(s.config.App.ProductEdition) { + case "ee", "de": + q.Add("mobile", mobile) + default: + if req.GetViewMode() != appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE { + q.Add("mobile", mobile) + } + } + } + } + lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang") // @TODO: this is a temporary solution until we figure out how to send these from oc web diff --git a/services/collaboration/pkg/service/grpc/v0/service_test.go b/services/collaboration/pkg/service/grpc/v0/service_test.go index 22707752e5..055a0aec68 100644 --- a/services/collaboration/pkg/service/grpc/v0/service_test.go +++ b/services/collaboration/pkg/service/grpc/v0/service_test.go @@ -2,6 +2,7 @@ package service_test import ( "context" + "net/url" "strconv" "time" @@ -206,6 +207,68 @@ var _ = Describe("Discovery", func() { Entry("Collabora no chat lang", "Collabora", "de", true, "https://cloud.opencloud.test/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopi.opencloud.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&closebutton=false&dchat=1&lang=de-DE"), Entry("OnlyOffice no chat lang", "OnlyOffice", "de", true, "https://cloud.opencloud.test/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopi.opencloud.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&ui=de-DE"), ) + DescribeTable( + "Mobile view", + func(product, edition string, enableMobile bool, viewMode appproviderv1beta1.ViewMode, mobile, expectedMobile string) { + ctx := context.Background() + nowTime := time.Now() + + cfg.Wopi.WopiSrc = "https://wopi.opencloud.test" + cfg.Wopi.Secret = "my_supa_secret" + cfg.Wopi.EnableMobile = enableMobile + cfg.App.Name = product + cfg.App.Product = product + cfg.App.ProductEdition = edition + + 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: viewMode, + AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime), + } + if mobile != "" { + req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "mobile", mobile) + } + + 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)) + + appURL, err := url.Parse(resp.GetAppUrl().GetAppUrl()) + Expect(err).To(Succeed()) + Expect(appURL.Query().Get("mobile")).To(Equal(expectedMobile)) + }, + Entry("ee edits on mobile", "OnlyOffice", "ee", true, appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, "true", "true"), + Entry("de edits on mobile", "OnlyOffice", "de", true, appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, "true", "true"), + Entry("edition is matched case-insensitively", "OnlyOffice", "EE", true, appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, "true", "true"), + Entry("ce does not edit on mobile", "OnlyOffice", "ce", true, appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, "true", ""), + Entry("an empty edition is ce", "OnlyOffice", "", true, appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, "true", ""), + Entry("ce reads on mobile", "OnlyOffice", "ce", true, appproviderv1beta1.ViewMode_VIEW_MODE_READ_ONLY, "true", "true"), + Entry("disabled stays out of the url", "OnlyOffice", "ee", false, appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, "true", ""), + Entry("without a mobile request nothing is added", "OnlyOffice", "ee", true, appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, "", ""), + Entry("collabora does not get it", "Collabora", "ee", true, appproviderv1beta1.ViewMode_VIEW_MODE_READ_ONLY, "true", ""), + ) + It("Success with Wopi Proxy", func() { ctx := context.Background() nowTime := time.Now()