From 8549632022cfd8d887d480c6edc6661d04291553 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Tue, 8 Oct 2024 15:25:25 +0200 Subject: [PATCH] feat: template feature Signed-off-by: Michael Barz --- .../template-conversions-for-apps.md | 48 +++++++ .../pkg/connector/fileconnector.go | 35 +++++ .../pkg/connector/fileconnector_test.go | 79 +++++++++++ .../pkg/connector/fileinfo/onlyoffice.go | 5 +- .../pkg/middleware/wopicontext.go | 29 ++-- .../pkg/middleware/wopicontext_test.go | 64 +++++++++ .../collaboration/pkg/server/http/server.go | 12 ++ .../pkg/service/grpc/v0/service.go | 27 +++- .../pkg/service/grpc/v0/service_test.go | 130 +++++++++++++++++- 9 files changed, 417 insertions(+), 12 deletions(-) create mode 100644 changelog/unreleased/template-conversions-for-apps.md diff --git a/changelog/unreleased/template-conversions-for-apps.md b/changelog/unreleased/template-conversions-for-apps.md new file mode 100644 index 00000000000..6e53911edaf --- /dev/null +++ b/changelog/unreleased/template-conversions-for-apps.md @@ -0,0 +1,48 @@ +Enhancement: WebOffice Templates + +We are now able to use templates for WebOffice and use them as a starting point for new documents. + +We are supporting the following mime types: + +## OnlyOffice + +- **MimeType:** `application/vnd.ms-word.template.macroenabled.12` + **TargetExtension:** `docx` + +- **MimeType:** `application/vnd.oasis.opendocument.text-template` + **TargetExtension:** `docx` + +- **MimeType:** `application/vnd.openxmlformats-officedocument.wordprocessingml.template` + **TargetExtension:** `docx` + +- **MimeType:** `application/vnd.oasis.opendocument.spreadsheet-template` + **TargetExtension:** `xlsx` + +- **MimeType:** `application/vnd.ms-excel.template.macroenabled.12` + **TargetExtension:** `xlsx` + +- **MimeType:** `application/vnd.openxmlformats-officedocument.spreadsheetml.template` + **TargetExtension:** `xlsx` + +- **MimeType:** `application/vnd.oasis.opendocument.presentation-template` + **TargetExtension:** `pptx` + +- **MimeType:** `application/vnd.ms-powerpoint.template.macroenabled.12` + **TargetExtension:** `pptx` + +- **MimeType:** `application/vnd.openxmlformats-officedocument.presentationml.template` + **TargetExtension:** `pptx` + +## Collabora + +- **MimeType:** `application/vnd.oasis.opendocument.spreadsheet-template` + **TargetExtension:** `ods` + +- **MimeType:** `application/vnd.oasis.opendocument.text-template` + **TargetExtension:** `odt` + +- **MimeType:** `application/vnd.oasis.opendocument.presentation-template` + **TargetExtension:** `odp` + +https://github.com/owncloud/ocis/pull/10276 +https://github.com/owncloud/ocis/issues/9785 diff --git a/services/collaboration/pkg/connector/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go index c771acdb834..d63a4592ed1 100644 --- a/services/collaboration/pkg/connector/fileconnector.go +++ b/services/collaboration/pkg/connector/fileconnector.go @@ -1188,6 +1188,10 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse, if err != nil { return nil, err } + collaborationURL, err := url.Parse(f.cfg.Wopi.WopiSrc) + if err != nil { + return nil, err + } privateLinkURL := &url.URL{} *privateLinkURL = *ocisURL privateLinkURL.Path = path.Join(ocisURL.Path, "f", storagespace.FormatResourceID(statRes.GetInfo().GetId())) @@ -1240,12 +1244,43 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse, } } + // if the file content is empty and a template reference is set, add the template source URL + if wopiContext.TemplateReference != nil && statRes.GetInfo().GetSize() == 0 { + if tu, err := f.createDownloadURL(wopiContext, collaborationURL); err == nil { + infoMap[fileinfo.KeyTemplateSource] = tu + } + } + info.SetProperties(infoMap) logger.Debug().Interface("FileInfo", info).Msg("CheckFileInfo: success") return NewResponseSuccessBody(info), nil } +// createDownloadURL will create a download URL for the template file. +// It uses a new wopi context with the template reference set as the file reference +// and a reva access token to download the file. +func (f *FileConnector) createDownloadURL(wopiContext middleware.WopiContext, collaborationURL *url.URL) (string, error) { + templateContext := wopiContext + templateContext.FileReference = wopiContext.TemplateReference + templateContext.TemplateReference = nil + + token, _, err := middleware.GenerateWopiToken(templateContext, f.cfg) + if err != nil { + return "", err + } + downloadURL := *collaborationURL + downloadURL.Path = path.Join( + collaborationURL.Path, + "wopi/templates/", + helpers.HashResourceId(templateContext.FileReference.GetResourceId()), + ) + q := downloadURL.Query() + q.Add("access_token", token) + downloadURL.RawQuery = q.Encode() + return downloadURL.String(), nil +} + func createHostUrl(mode string, ocisUrl *url.URL, appName string, info *providerv1beta1.ResourceInfo) string { webUrl := createAppExternalURL(ocisUrl, appName, info) addURLParams(webUrl, map[string]string{"view_mode": mode}) diff --git a/services/collaboration/pkg/connector/fileconnector_test.go b/services/collaboration/pkg/connector/fileconnector_test.go index 7a776ea8b94..923951d2948 100644 --- a/services/collaboration/pkg/connector/fileconnector_test.go +++ b/services/collaboration/pkg/connector/fileconnector_test.go @@ -51,6 +51,7 @@ var _ = Describe("FileConnector", func() { WopiSrc: "https://ocis.server.prv", Secret: "topsecret", }, + TokenManager: &config.TokenManager{JWTSecret: "secret"}, } ccs = &collabmocks.ContentConnectorService{} @@ -1843,5 +1844,83 @@ var _ = Describe("FileConnector", func() { Expect(response.Status).To(Equal(200)) Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo)) }) + It("Stat success with template", func() { + wopiCtx.TemplateReference = &providerv1beta1.Reference{ + ResourceId: &providerv1beta1.ResourceId{ + StorageId: "storageid", + OpaqueId: "opaqueid2", + SpaceId: "spaceid", + }, + } + ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) + u := &userv1beta1.User{ + Id: &userv1beta1.UserId{ + Idp: "customIdp", + OpaqueId: "admin", + }, + DisplayName: "Pet Shaft", + } + ctx = ctxpkg.ContextSetUser(ctx, u) + + gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{ + Status: status.NewOK(ctx), + Info: &providerv1beta1.ResourceInfo{ + Owner: &userv1beta1.UserId{ + Idp: "customIdp", + OpaqueId: "aabbcc", + Type: userv1beta1.UserType_USER_TYPE_PRIMARY, + }, + Size: uint64(0), + Mtime: &typesv1beta1.Timestamp{ + Seconds: uint64(16273849), + }, + Path: "/path/to/test.txt", + Id: &providerv1beta1.ResourceId{ + StorageId: "storageid", + OpaqueId: "opaqueid", + SpaceId: "spaceid", + }, + }, + }, nil) + + expectedFileInfo := &fileinfo.OnlyOffice{ + Version: "v162738490", + BaseFileName: "test.txt", + BreadcrumbDocName: "test.txt", + BreadcrumbFolderName: "/path/to", + BreadcrumbFolderURL: "https://ocis.example.prv/f/storageid$spaceid%21opaqueid", + UserCanNotWriteRelative: false, + SupportsLocks: true, + SupportsUpdate: true, + SupportsRename: true, + UserCanWrite: true, + UserCanRename: true, + UserID: "61646d696e40637573746f6d496470", // hex of admin@customIdp + UserFriendlyName: "Pet Shaft", + FileSharingURL: "https://ocis.example.prv/f/storageid$spaceid%21opaqueid?details=sharing", + FileVersionURL: "https://ocis.example.prv/f/storageid$spaceid%21opaqueid?details=versions", + HostEditURL: "https://ocis.example.prv/external-onlyoffice/path/to/test.txt?fileId=storageid%24spaceid%21opaqueid&view_mode=write", + PostMessageOrigin: "https://ocis.example.prv", + } + + // change wopi app provider + cfg.App.Name = "OnlyOffice" + + response, err := fc.CheckFileInfo(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(response.Status).To(Equal(200)) + + returnedFileInfo := response.Body.(*fileinfo.OnlyOffice) + templateSource := returnedFileInfo.TemplateSource + expectedTemplateSource := "https://ocis.server.prv/wopi/templates/a340d017568d0d579ee061a9ac02109e32fb07082d35c40aa175864303bd9107?access_token=" + + // take TemplateSource out of the response for easier comparison + returnedFileInfo.TemplateSource = "" + Expect(returnedFileInfo).To(Equal(expectedFileInfo)) + // check if the template source is correct + // the url is using a generated access token which always has a new ttl + // so we can't compare the whole url + Expect(templateSource).To(HavePrefix(expectedTemplateSource)) + }) }) }) diff --git a/services/collaboration/pkg/connector/fileinfo/onlyoffice.go b/services/collaboration/pkg/connector/fileinfo/onlyoffice.go index 772670ff8f9..4a6535a46dd 100644 --- a/services/collaboration/pkg/connector/fileinfo/onlyoffice.go +++ b/services/collaboration/pkg/connector/fileinfo/onlyoffice.go @@ -77,6 +77,8 @@ type OnlyOffice struct { FileNameMaxLength int `json:"FileNameMaxLength,omitempty"` // copied from MS WOPI LastModifiedTime string `json:"LastModifiedTime,omitempty"` + // The ID of file (like the wopi/files/ID) can be a non-existing file. In that case, the file will be created from a template when the template (eg. an OTT file) is specified as TemplateSource in the CheckFileInfo response. The TemplateSource is supposed to be an URL like https://somewhere/accessible/file.ott that is accessible by the Online. For the actual saving of the content, normal PutFile mechanism will be used. + TemplateSource string `json:"TemplateSource,omitempty"` // // User metadata properties @@ -179,7 +181,8 @@ func (oinfo *OnlyOffice) SetProperties(props map[string]interface{}) { oinfo.FileNameMaxLength = value.(int) case KeyLastModifiedTime: oinfo.LastModifiedTime = value.(string) - + case KeyTemplateSource: + oinfo.TemplateSource = value.(string) case KeyIsAnonymousUser: oinfo.IsAnonymousUser = value.(bool) case KeyUserFriendlyName: diff --git a/services/collaboration/pkg/middleware/wopicontext.go b/services/collaboration/pkg/middleware/wopicontext.go index 077169ab28d..081a9b55dbb 100644 --- a/services/collaboration/pkg/middleware/wopicontext.go +++ b/services/collaboration/pkg/middleware/wopicontext.go @@ -26,10 +26,11 @@ const ( // WopiContext wraps all the information we need for WOPI type WopiContext struct { - AccessToken string - ViewOnlyToken string - FileReference *providerv1beta1.Reference - ViewMode appproviderv1beta1.ViewMode + AccessToken string + ViewOnlyToken string + FileReference *providerv1beta1.Reference + TemplateReference *providerv1beta1.Reference + ViewMode appproviderv1beta1.ViewMode } // WopiContextAuthMiddleware will prepare an HTTP handler to be used as @@ -120,12 +121,22 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl hashedRef := helpers.HashResourceId(claims.WopiContext.FileReference.GetResourceId()) fileID := parseWopiFileID(cfg, r.URL.Path) - if fileID != hashedRef { - wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token") - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return + if claims.WopiContext.TemplateReference != nil { + hashedTemplateRef := helpers.HashResourceId(claims.WopiContext.TemplateReference.GetResourceId()) + // the fileID could be one of the references within the access token if both are set + // because we can use the access token to get the contents of the template file + if fileID != hashedTemplateRef && fileID != hashedRef { + wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + } else { + if fileID != hashedRef { + wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } } - next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/services/collaboration/pkg/middleware/wopicontext_test.go b/services/collaboration/pkg/middleware/wopicontext_test.go index 900cf5c5679..af8f6e49a6a 100644 --- a/services/collaboration/pkg/middleware/wopicontext_test.go +++ b/services/collaboration/pkg/middleware/wopicontext_test.go @@ -165,6 +165,70 @@ var _ = Describe("Wopi Context Middleware", func() { mw.ServeHTTP(resp, req) Expect(resp.Code).To(Equal(http.StatusOK)) }) + It("Should authorize successful with template reference", func() { + req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx) + token, err := tknMngr.MintToken(ctx, user, nil) + Expect(err).ToNot(HaveOccurred()) + + wopiContext := middleware.WopiContext{ + AccessToken: token, + ViewMode: appprovider.ViewMode_VIEW_MODE_READ_WRITE, + TemplateReference: &providerv1beta1.Reference{ + ResourceId: rid, + Path: ".", + }, + FileReference: &providerv1beta1.Reference{ + ResourceId: &providerv1beta1.ResourceId{ + StorageId: "storageID", + OpaqueId: "opaqueID2", + SpaceId: "spaceID", + }, + }, + } + wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg) + q := req.URL.Query() + q.Add("access_token", wopiToken) + q.Add("access_token_ttl", strconv.FormatInt(ttl, 10)) + req.URL.RawQuery = q.Encode() + resp := httptest.NewRecorder() + + mw.ServeHTTP(resp, req) + Expect(resp.Code).To(Equal(http.StatusOK)) + }) + It("Should not authorize when no reference matches", func() { + req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx) + token, err := tknMngr.MintToken(ctx, user, nil) + Expect(err).ToNot(HaveOccurred()) + + wopiContext := middleware.WopiContext{ + AccessToken: token, + ViewMode: appprovider.ViewMode_VIEW_MODE_READ_WRITE, + TemplateReference: &providerv1beta1.Reference{ + ResourceId: &providerv1beta1.ResourceId{ + StorageId: "storageID", + OpaqueId: "opaqueID3", + SpaceId: "spaceID", + }, + Path: ".", + }, + FileReference: &providerv1beta1.Reference{ + ResourceId: &providerv1beta1.ResourceId{ + StorageId: "storageID", + OpaqueId: "opaqueID2", + SpaceId: "spaceID", + }, + }, + } + wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg) + q := req.URL.Query() + q.Add("access_token", wopiToken) + q.Add("access_token_ttl", strconv.FormatInt(ttl, 10)) + req.URL.RawQuery = q.Encode() + resp := httptest.NewRecorder() + + mw.ServeHTTP(resp, req) + Expect(resp.Code).To(Equal(http.StatusUnauthorized)) + }) It("Should not authorize with proxy when fileID mismatches", func() { cfg.Wopi.ProxySecret = "proxySecret" cfg.Wopi.ProxyURL = "https://proxy" diff --git a/services/collaboration/pkg/server/http/server.go b/services/collaboration/pkg/server/http/server.go index e00cdb403c6..cf2c3b4ebda 100644 --- a/services/collaboration/pkg/server/http/server.go +++ b/services/collaboration/pkg/server/http/server.go @@ -182,5 +182,17 @@ func prepareRoutes(r *chi.Mux, options Options) { }) }) }) + r.Route("/templates/{templateID}", func(r chi.Router) { + r.Use( + func(h stdhttp.Handler) stdhttp.Handler { + // authentication and wopi context + return colabmiddleware.WopiContextAuthMiddleware(options.Config, h) + }, + colabmiddleware.CollaborationTracingMiddleware, + ) + r.Get("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) { + adapter.GetFile(w, r) + }) + }) }) } diff --git a/services/collaboration/pkg/service/grpc/v0/service.go b/services/collaboration/pkg/service/grpc/v0/service.go index c2cb6308958..162d7947c43 100644 --- a/services/collaboration/pkg/service/grpc/v0/service.go +++ b/services/collaboration/pkg/service/grpc/v0/service.go @@ -14,6 +14,7 @@ import ( rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc" @@ -102,7 +103,12 @@ func (s *Service) OpenInApp( appURL, err = s.addQueryToURL(appURL, req) if err != nil { logger.Error().Err(err).Msg("OpenInApp: error parsing appUrl") - return nil, err + return &appproviderv1beta1.OpenInAppResponse{ + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_INVALID_ARGUMENT, + Message: "OpenInApp: error parsing appUrl", + }, + }, nil } // create the wopiContext and generate the token @@ -113,6 +119,25 @@ func (s *Service) OpenInApp( ViewMode: req.GetViewMode(), } + if templateID := utils.ReadPlainFromOpaque(req.GetOpaque(), "template"); templateID != "" { + // we can ignore the error here, as we are sure that the templateID is not empty + templateRes, _ := storagespace.ParseID(templateID) + // we need to have at least both opaqueID and spaceID set + if templateRes.GetOpaqueId() == "" || templateRes.GetSpaceId() == "" { + logger.Error().Err(err).Msg("OpenInApp: error parsing templateID") + return &appproviderv1beta1.OpenInAppResponse{ + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_INVALID_ARGUMENT, + Message: "OpenInApp: error parsing templateID", + }, + }, nil + } + wopiContext.TemplateReference = &providerv1beta1.Reference{ + ResourceId: &templateRes, + Path: ".", + } + } + accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config) if err != nil { logger.Error().Err(err).Msg("OpenInApp: error generating the token") diff --git a/services/collaboration/pkg/service/grpc/v0/service_test.go b/services/collaboration/pkg/service/grpc/v0/service_test.go index 40655ac3164..636a9f4acfa 100644 --- a/services/collaboration/pkg/service/grpc/v0/service_test.go +++ b/services/collaboration/pkg/service/grpc/v0/service_test.go @@ -89,7 +89,8 @@ var _ = Describe("Discovery", func() { ".xlsb": "https://test.server.prv/hosting/wopi/cell/view", }, "edit": map[string]string{ - ".docx": "https://test.server.prv/hosting/wopi/word/edit", + ".docx": "https://test.server.prv/hosting/wopi/word/edit", + ".invalid": "://test.server.prv/hosting/wopi/cell/edit", }, }), service.GatewayAPIClient(gatewayClient), @@ -242,5 +243,132 @@ var _ = Describe("Discovery", func() { Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=en-US&WOPISrc=https%3A%2F%2Foffice.proxy.test.prv%2Fwopi%2Ffiles%2FeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiaHR0cHM6Ly93b3Bpc2VydmVyLnRlc3QucHJ2L3dvcGkvZmlsZXMvIiwiZiI6IjJmNmVjMTg2OTZkZDEwMDgxMDY3NDliZDk0MTA2ZTVjZmFkNWMwOWUxNWRlN2I3NzA4OGQwMzg0M2U3MWI0M2UifQ.yfyLHZ18Z1MFOa6u7AP0LqfIiQ9X5AMkYauEZGhbCNs")) Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10))) }) + It("Fail with invalid app url", func() { + ctx := context.Background() + nowTime := time.Now() + + cfg.Wopi.WopiSrc = "htttps://wopiserver.test.prv" + cfg.Wopi.Secret = "my_supa_secret" + cfg.App.Name = "Microsoft" + + 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.invalid", + }, + ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, + AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime), + } + req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "en") + + 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_INVALID_ARGUMENT)) + Expect(resp.GetStatus().GetMessage()).To(Equal("OpenInApp: error parsing appUrl")) + }) + It("Fail with invalid template id", func() { + ctx := context.Background() + nowTime := time.Now() + + cfg.Wopi.WopiSrc = "htttps://wopiserver.test.prv" + cfg.Wopi.Secret = "my_supa_secret" + cfg.App.Name = "Microsoft" + + 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", "en") + req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "template", "&file_id") + + 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_INVALID_ARGUMENT)) + Expect(resp.GetStatus().GetMessage()).To(Equal("OpenInApp: error parsing templateID")) + }) + It("Success with valid template id", func() { + ctx := context.Background() + nowTime := time.Now() + + cfg.Wopi.WopiSrc = "htttps://wopiserver.test.prv" + cfg.Wopi.Secret = "my_supa_secret" + cfg.App.Name = "OnlyOffice" + + 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", "en") + req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "template", "prodiderID$spaceID!opaqueID") + + 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=htttps%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=en-US")) + Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10))) + }) }) })