Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collaboration openinapp refactor #9827

Merged
merged 3 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions services/collaboration/pkg/connector/contentconnector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions services/collaboration/pkg/connector/fileconnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
6 changes: 1 addition & 5 deletions services/collaboration/pkg/connector/fileconnector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
})

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions services/collaboration/pkg/middleware/wopicontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
229 changes: 112 additions & 117 deletions services/collaboration/pkg/service/grpc/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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},
Expand All @@ -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
}
Loading