diff --git a/changelog/unreleased/wopi-driver-improvements.md b/changelog/unreleased/wopi-driver-improvements.md new file mode 100644 index 00000000000..5988219034f --- /dev/null +++ b/changelog/unreleased/wopi-driver-improvements.md @@ -0,0 +1,6 @@ +Enhancement: The wopi app driver supports more options + +We now generate a folderurl that is used in the wopi protocol. It provides an endpoint to go back from the app to the containing folder in the file list. In addition to that, we now include the UI_LLCC parameter in the app-open URL. + +https://github.com/cs3org/reva/pull/3290 +https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/online/discovery#ui_llcc diff --git a/pkg/app/provider/wopi/wopi.go b/pkg/app/provider/wopi/wopi.go index ba64e614d3a..583fec374a7 100644 --- a/pkg/app/provider/wopi/wopi.go +++ b/pkg/app/provider/wopi/wopi.go @@ -46,6 +46,7 @@ import ( "github.com/cs3org/reva/v2/pkg/mime" "github.com/cs3org/reva/v2/pkg/rhttp" "github.com/cs3org/reva/v2/pkg/sharedconf" + "github.com/cs3org/reva/v2/pkg/storage/utils/templates" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/golang-jwt/jwt" "github.com/mitchellh/mapstructure" @@ -57,16 +58,18 @@ func init() { } type config struct { - IOPSecret string `mapstructure:"iop_secret" docs:";The IOP secret used to connect to the wopiserver."` - WopiURL string `mapstructure:"wopi_url" docs:";The wopiserver's URL."` - AppName string `mapstructure:"app_name" docs:";The App user-friendly name."` - AppIconURI string `mapstructure:"app_icon_uri" docs:";A URI to a static asset which represents the app icon."` - AppURL string `mapstructure:"app_url" docs:";The App URL."` - AppIntURL string `mapstructure:"app_int_url" docs:";The internal app URL in case of dockerized deployments. Defaults to AppURL"` - AppAPIKey string `mapstructure:"app_api_key" docs:";The API key used by the app, if applicable."` - JWTSecret string `mapstructure:"jwt_secret" docs:";The JWT secret to be used to retrieve the token TTL."` - AppDesktopOnly bool `mapstructure:"app_desktop_only" docs:"false;Specifies if the app can be opened only on desktop."` - InsecureConnections bool `mapstructure:"insecure_connections"` + IOPSecret string `mapstructure:"iop_secret" docs:";The IOP secret used to connect to the wopiserver."` + WopiURL string `mapstructure:"wopi_url" docs:";The wopiserver's URL."` + WopiFolderURLBaseURL string `mapstructure:"wopi_folder_url_base_url" docs:";The base URL to generate links to navigate back to the containing folder."` + WopiFolderURLPathTemplate string `mapstructure:"wopi_folder_url_path_template" docs:";The template to generate the folderurl path segments."` + AppName string `mapstructure:"app_name" docs:";The App user-friendly name."` + AppIconURI string `mapstructure:"app_icon_uri" docs:";A URI to a static asset which represents the app icon."` + AppURL string `mapstructure:"app_url" docs:";The App URL."` + AppIntURL string `mapstructure:"app_int_url" docs:";The internal app URL in case of dockerized deployments. Defaults to AppURL"` + AppAPIKey string `mapstructure:"app_api_key" docs:";The API key used by the app, if applicable."` + JWTSecret string `mapstructure:"jwt_secret" docs:";The JWT secret to be used to retrieve the token TTL."` + AppDesktopOnly bool `mapstructure:"app_desktop_only" docs:"false;Specifies if the app can be opened only on desktop."` + InsecureConnections bool `mapstructure:"insecure_connections"` } func parseConfig(m map[string]interface{}) (*config, error) { @@ -140,6 +143,16 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc q.Add("fileid", resource.GetId().OpaqueId) q.Add("viewmode", viewMode.String()) + folderURLPath := templates.WithResourceInfo(resource, p.conf.WopiFolderURLPathTemplate) + folderURLBaseURL, err := url.Parse(p.conf.WopiFolderURLBaseURL) + if err != nil { + return nil, err + } + if folderURLPath != "" { + folderURLBaseURL.Path = path.Join(folderURLBaseURL.Path, folderURLPath) + q.Add("folderurl", folderURLBaseURL.String()) + } + u, ok := ctxpkg.ContextGetUser(ctx) if ok { // else defaults to "Guest xyz" if u.Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT || u.Id.Type == userpb.UserType_USER_TYPE_FEDERATED { @@ -242,10 +255,9 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc return nil, err } urlQuery := url.Query() - // we could improve this by using the UI_LLCC value from the wopi discovery url - // https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/online/discovery#ui_llcc - urlQuery.Set("ui", language) // OnlyOffice - urlQuery.Set("lang", language) // Collabora + urlQuery.Set("ui", language) // OnlyOffice + urlQuery.Set("lang", language) // Collabora + urlQuery.Set("UI_LLCC", language) // Office365 url.RawQuery = urlQuery.Encode() appFullURL = url.String() } diff --git a/pkg/storage/utils/templates/templates.go b/pkg/storage/utils/templates/templates.go index 61c3e5d22e8..0ad48074e52 100644 --- a/pkg/storage/utils/templates/templates.go +++ b/pkg/storage/utils/templates/templates.go @@ -33,31 +33,42 @@ import ( "github.com/Masterminds/sprig" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/pkg/errors" ) // UserData contains the template placeholders for a user. // For example {{.Username}} or {{.Id.Idp}} -type UserData struct { - *userpb.User - Email EmailData -} +type ( + UserData struct { + *userpb.User + Email EmailData + } -// SpaceData contains the templace placeholders for a space. -// For example {{.SpaceName}} {{.SpaceType}} or {{.User.Id.OpaqueId}} -type SpaceData struct { - *UserData - SpaceType string - SpaceName string -} + // SpaceData contains the templace placeholders for a space. + // For example {{.SpaceName}} {{.SpaceType}} or {{.User.Id.OpaqueId}} + SpaceData struct { + *UserData + SpaceType string + SpaceName string + } -// EmailData contains mail data -// split into local and domain part. -// It is extracted from splitting the username by @. -type EmailData struct { - Local string - Domain string -} + // EmailData contains mail data + // split into local and domain part. + // It is extracted from splitting the username by @. + EmailData struct { + Local string + Domain string + } + + // ResourceData contains the ResourceInfo + // ResourceData.ResourceID is a stringified form of ResourceInfo.Id + ResourceData struct { + ResourceInfo *providerv1beta1.ResourceInfo + ResourceID string + } +) // WithUser generates a layout based on user data. func WithUser(u *userpb.User, tpl string) string { @@ -95,6 +106,24 @@ func WithSpacePropertiesAndUser(u *userpb.User, spaceType string, spaceName stri return b.String() } +// WithResourceInfo renders template stings with ResourceInfo variables +func WithResourceInfo(i *providerv1beta1.ResourceInfo, tpl string) string { + tpl = clean(tpl) + data := newResourceData(i) + // compile given template tpl + t, err := template.New("tpl").Funcs(sprig.TxtFuncMap()).Parse(tpl) + if err != nil { + err := errors.Wrap(err, fmt.Sprintf("error parsing template: fileinfoandresourceid_template:%+v tpl:%s", data, tpl)) + panic(err) + } + b := bytes.Buffer{} + if err := t.Execute(&b, data); err != nil { + err := errors.Wrap(err, fmt.Sprintf("error executing template: fileinfoandresourceid_template:%+v tpl:%s", data, tpl)) + panic(err) + } + return b.String() +} + func newUserData(u *userpb.User) *UserData { usernameSplit := strings.Split(u.Username, "@") if u.Mail != "" { @@ -128,6 +157,14 @@ func newSpaceData(u *userpb.User, st string, n string) *SpaceData { return sd } +func newResourceData(i *providerv1beta1.ResourceInfo) *ResourceData { + rd := &ResourceData{ + ResourceInfo: i, + ResourceID: storagespace.FormatResourceID(*i.Id), + } + return rd +} + func clean(a string) string { return path.Clean(a) }