repo_description: 🌩 Self-hosted file management and sharing system, supports multiple storage providers
repo_topics: cloud cloud-storage cloudreve file file-manager file-sharing golang
code: package wopi
  
  import (
  	"context"
  	"errors"
  	"fmt"
  	"github.com/cloudreve/Cloudreve/v4/inventory/types"
  	"net/url"
  	"strings"
  	"time"
  
  	"github.com/cloudreve/Cloudreve/v4/application/dependency"
  	"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
  	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
  	"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
  	"github.com/cloudreve/Cloudreve/v4/pkg/setting"
  )
  
  var (
  	ErrActionNotSupported = errors.New("action not supported by current wopi endpoint")
  
  	queryPlaceholders = map[string]string{
  		"BUSINESS_USER":           "",
  		"DC_LLCC":                 "lng",
  		"DISABLE_ASYNC":           "",
  		"DISABLE_CHAT":            "",
  		"EMBEDDED":                "true",
  		"FULLSCREEN":              "true",
  		"HOST_SESSION_ID":         "",
  		"SESSION_CONTEXT":         "",
  		"RECORDING":               "",
  		"THEME_ID":                "darkmode",
  		"UI_LLCC":                 "lng",
  		"VALIDATOR_TEST_CATEGORY": "",
  	}
  )
  
  const (
  	SessionCachePrefix    = "wopi_session_"
  	AccessTokenQuery      = "access_token"
  	OverwriteHeader       = WopiHeaderPrefix + "Override"
  	ServerErrorHeader     = WopiHeaderPrefix + "ServerError"
  	RenameRequestHeader   = WopiHeaderPrefix + "RequestedName"
  	LockTokenHeader       = WopiHeaderPrefix + "Lock"
  	ItemVersionHeader     = WopiHeaderPrefix + "ItemVersion"
  	SuggestedTargetHeader = WopiHeaderPrefix + "SuggestedTarget"
  
  	MethodLock           = "LOCK"
  	MethodUnlock         = "UNLOCK"
  	MethodRefreshLock    = "REFRESH_LOCK"
  	MethodPutRelative    = "PUT_RELATIVE"
  	wopiSrcPlaceholder   = "WOPI_SOURCE"
  	wopiSrcParamDefault  = "WOPISrc"
  	languageParamDefault = "lang"
  	WopiHeaderPrefix     = "X-WOPI-"
  
  	LockDuration = time.Duration(30) * time.Minute
  )
  
  func GenerateWopiSrc(ctx context.Context, action types.ViewerAction, viewer *types.Viewer, viewerSession *manager.ViewerSession) (*url.URL, error) {
  	dep := dependency.FromContext(ctx)
  	base := dep.SettingProvider().SiteURL(setting.UseFirstSiteUrl(ctx))
  	hasher := dep.HashIDEncoder()
  
  	availableActions, ok := viewer.WopiActions[viewerSession.File.Ext()]
  	if !ok {
  		return nil, ErrActionNotSupported
  	}
  
  	var (
  		src string
  	)
  	fallbackOrder := []types.ViewerAction{action, types.ViewerActionView, types.ViewerActionEdit}
  	for _, a := range fallbackOrder {
  		if src, ok = availableActions[a]; ok {
  			break
  		}
  	}
  
  	if src == "" {
  		return nil, ErrActionNotSupported
  	}
  
  	actionUrl, err := generateActionUrl(src,
  		routes.MasterWopiSrc(base, hashid.EncodeFileID(hasher, viewerSession.File.ID())).String())
  	if err != nil {
  		return nil, err
  	}
  
  	return actionUrl, nil
  }
  
  // Replace query parameters in action URL template. Some placeholders need to be replaced
  // at the frontend, e.g. `THEME_ID`.
  func generateActionUrl(src string, fileSrc string) (*url.URL, error) {
  	src = strings.ReplaceAll(src, "<", "")
  	src = strings.ReplaceAll(src, ">", "")
  	actionUrl, err := url.Parse(src)
  	if err != nil {
  		return nil, fmt.Errorf("failed to parse action url: %s", err)
  	}
  
  	queries := actionUrl.Query()
  	srcReplaced := false
  	queryReplaced := url.Values{}
  	for k := range queries {
  		if placeholder, ok := queryPlaceholders[queries.Get(k)]; ok {
  			if placeholder != "" {
  				queryReplaced.Set(k, placeholder)
  			}
  
  			continue
  		}
  
  		if queries.Get(k) == wopiSrcPlaceholder {
  			queryReplaced.Set(k, fileSrc)
  			srcReplaced = true
  			continue
  		}
  
  		queryReplaced.Set(k, queries.Get(k))
  	}
  
  	if !srcReplaced {
  		queryReplaced.Set(wopiSrcParamDefault, fileSrc)
  	}
  
  	// LibreOffice require this flag to show correct language
  	queryReplaced.Set(languageParamDefault, "lng")
  
  	actionUrl.RawQuery = queryReplaced.Encode()
  	return actionUrl, nil
  }
  