mirror of
https://github.com/owncloud/ocis
synced 2026-04-25 17:25:21 +02:00
300 lines
9.3 KiB
Go
300 lines
9.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"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"
|
|
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
|
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
|
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
|
"github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool"
|
|
"github.com/owncloud/reva/v2/pkg/storagespace"
|
|
"github.com/owncloud/reva/v2/pkg/utils"
|
|
microstore "go-micro.dev/v4/store"
|
|
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
|
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
|
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
|
|
"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
|
|
"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
|
|
)
|
|
|
|
// NewHandler creates a new grpc service implementing the OpenInApp interface
|
|
func NewHandler(opts ...Option) (*Service, func(), error) {
|
|
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...)
|
|
|
|
gatewaySelector := options.GatewaySelector
|
|
var err error
|
|
if gatewaySelector == nil {
|
|
gatewaySelector, err = pool.GatewaySelector(options.Config.CS3Api.Gateway.Name)
|
|
if err != nil {
|
|
return nil, teardown, err
|
|
}
|
|
}
|
|
|
|
return &Service{
|
|
id: options.Config.GRPC.Namespace + "." + options.Config.Service.Name + "." + options.Config.App.Name,
|
|
appURLs: options.AppURLs,
|
|
logger: options.Logger,
|
|
config: options.Config,
|
|
gatewaySelector: gatewaySelector,
|
|
store: options.Store,
|
|
}, teardown, nil
|
|
}
|
|
|
|
// Service implements the OpenInApp interface
|
|
type Service struct {
|
|
id string
|
|
appURLs map[string]map[string]string
|
|
logger log.Logger
|
|
config *config.Config
|
|
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]
|
|
store microstore.Store
|
|
}
|
|
|
|
// OpenInApp will implement the OpenInApp interface of the app provider
|
|
func (s *Service) OpenInApp(
|
|
ctx context.Context,
|
|
req *appproviderv1beta1.OpenInAppRequest,
|
|
) (*appproviderv1beta1.OpenInAppResponse, error) {
|
|
|
|
// get the current user
|
|
var user *userv1beta1.User = nil
|
|
meReq := &gatewayv1beta1.WhoAmIRequest{
|
|
Token: req.GetAccessToken(),
|
|
}
|
|
gwc, err := s.gatewaySelector.Next()
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("OpenInApp: could not select a gateway client")
|
|
return nil, err
|
|
}
|
|
|
|
meResp, err := gwc.WhoAmI(ctx, meReq)
|
|
if err == nil {
|
|
if meResp.GetStatus().GetCode() == rpcv1beta1.Code_CODE_OK {
|
|
user = meResp.GetUser()
|
|
}
|
|
}
|
|
|
|
// required for the response, it will be used also for logs
|
|
providerFileRef := providerv1beta1.Reference{
|
|
ResourceId: req.GetResourceInfo().GetId(),
|
|
Path: ".",
|
|
}
|
|
|
|
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())
|
|
|
|
// 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")
|
|
}
|
|
|
|
// append the parameters we need
|
|
appURL, err = s.addQueryToURL(appURL, req)
|
|
if err != nil {
|
|
logger.Error().Err(err).Msg("OpenInApp: error parsing appUrl")
|
|
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
|
|
wopiContext := middleware.WopiContext{
|
|
AccessToken: req.GetAccessToken(), // it will be encrypted
|
|
ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"),
|
|
FileReference: &providerFileRef,
|
|
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, s.store)
|
|
if err != nil {
|
|
logger.Error().Err(err).Msg("OpenInApp: error generating the token")
|
|
return &appproviderv1beta1.OpenInAppResponse{
|
|
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_INTERNAL},
|
|
}, err
|
|
}
|
|
|
|
logger.Debug().Msg("OpenInApp: success")
|
|
|
|
return &appproviderv1beta1.OpenInAppResponse{
|
|
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_OK},
|
|
AppUrl: &appproviderv1beta1.OpenInAppURL{
|
|
AppUrl: appURL,
|
|
Method: "POST",
|
|
FormParameters: map[string]string{
|
|
// these parameters will be passed to the web server by the app provider application
|
|
"access_token": accessToken,
|
|
// milliseconds since Jan 1, 1970 UTC as required in https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#access_token_ttl
|
|
//"access_token_ttl": strconv.FormatInt(claims.ExpiresAt.UnixMilli(), 10),
|
|
"access_token_ttl": strconv.FormatInt(accessExpiration, 10),
|
|
},
|
|
},
|
|
}, 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[strings.ToLower(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.Product) == "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 := wopisrc.GenerateWopiSrc(fileRef, s.config)
|
|
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")
|
|
|
|
// @TODO: this is a temporary solution until we figure out how to send these from oc web
|
|
switch lang {
|
|
case "bg":
|
|
lang = "bg-BG"
|
|
case "cs":
|
|
lang = "cs-CZ"
|
|
case "de":
|
|
lang = "de-DE"
|
|
case "en":
|
|
lang = "en-US"
|
|
case "es":
|
|
lang = "es-ES"
|
|
case "fr":
|
|
lang = "fr-FR"
|
|
case "gl":
|
|
lang = "gl-ES"
|
|
case "it":
|
|
lang = "it-IT"
|
|
case "nl":
|
|
lang = "nl-NL"
|
|
case "ko":
|
|
lang = "ko-KR"
|
|
case "sq":
|
|
lang = "sq-AL"
|
|
case "sv":
|
|
lang = "sv-SE"
|
|
case "tr":
|
|
lang = "tr-TR"
|
|
case "zh":
|
|
lang = "zh-CN"
|
|
}
|
|
|
|
if lang != "" {
|
|
switch strings.ToLower(s.config.App.Product) {
|
|
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
|
|
}
|