github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/open_file.go (about)

     1  package sharing
     2  
     3  import (
     4  	"net/http"
     5  	"net/url"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/cozy/cozy-stack/client/request"
    10  	"github.com/cozy/cozy-stack/model/instance"
    11  	"github.com/cozy/cozy-stack/model/permission"
    12  	"github.com/cozy/cozy-stack/model/vfs"
    13  	build "github.com/cozy/cozy-stack/pkg/config"
    14  	"github.com/cozy/cozy-stack/pkg/config/config"
    15  	"github.com/cozy/cozy-stack/pkg/consts"
    16  	"github.com/cozy/cozy-stack/pkg/couchdb"
    17  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    18  	"github.com/labstack/echo/v4"
    19  )
    20  
    21  // FileOpener can be used to find the parameters for opening a file (shared or
    22  // not), when collaborative edition is possible (like for a note or an office
    23  // document).
    24  type FileOpener struct {
    25  	Inst      *instance.Instance
    26  	File      *vfs.FileDoc
    27  	Sharing   *Sharing // can be nil
    28  	Code      string
    29  	ClientID  string
    30  	MemberKey string
    31  }
    32  
    33  // NewFileOpener returns a FileOpener for the given file on the current instance.
    34  func NewFileOpener(inst *instance.Instance, file *vfs.FileDoc) (*FileOpener, error) {
    35  	// Looks if the document is shared
    36  	opener := &FileOpener{Inst: inst, File: file}
    37  	sharing, err := opener.getSharing(inst, file.ID())
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	opener.Sharing = sharing
    42  	return opener, nil
    43  }
    44  
    45  func (o *FileOpener) getSharing(inst *instance.Instance, fileID string) (*Sharing, error) {
    46  	sid := consts.Files + "/" + fileID
    47  	var ref SharedRef
    48  	if err := couchdb.GetDoc(inst, consts.Shared, sid, &ref); err != nil {
    49  		if couchdb.IsNotFoundError(err) {
    50  			return nil, nil
    51  		}
    52  		return nil, err
    53  	}
    54  
    55  	for sharingID, info := range ref.Infos {
    56  		if info.Removed {
    57  			continue
    58  		}
    59  		var sharing Sharing
    60  		if err := couchdb.GetDoc(inst, consts.Sharings, sharingID, &sharing); err != nil {
    61  			return nil, err
    62  		}
    63  		if sharing.Active {
    64  			return &sharing, nil
    65  		}
    66  	}
    67  	return nil, nil
    68  }
    69  
    70  // AddShareByLinkCode can be used to give a sharecode that can be used to open
    71  // the file, when the file is in a directory shared by link.
    72  func (o *FileOpener) AddShareByLinkCode(code string) {
    73  	o.Code = code
    74  }
    75  
    76  // CheckPermission takes the permission doc, and checks that the user has the
    77  // right to open the file.
    78  func (o *FileOpener) CheckPermission(pdoc *permission.Permission, sharingID string) error {
    79  	// If a file is opened from a preview of a sharing, and nobody has accepted
    80  	// the sharing until now, the io.cozy.shared document for the file has not
    81  	// been created, and we need to fill the sharing by another way.
    82  	if o.Sharing == nil && pdoc.Type == permission.TypeSharePreview {
    83  		parts := strings.SplitN(pdoc.SourceID, "/", 2)
    84  		if len(parts) != 2 {
    85  			return ErrInvalidSharing
    86  		}
    87  		sharingID := parts[1]
    88  		var sharing Sharing
    89  		if err := couchdb.GetDoc(o.Inst, consts.Sharings, sharingID, &sharing); err != nil {
    90  			return err
    91  		}
    92  		o.Sharing = &sharing
    93  		preview, err := permission.GetForSharePreview(o.Inst, sharingID)
    94  		if err != nil {
    95  			return err
    96  		}
    97  		for k, v := range preview.Codes {
    98  			if v == o.Code {
    99  				o.MemberKey = k
   100  			}
   101  		}
   102  	}
   103  
   104  	// If a file is opened via a token for cozy-to-cozy sharing, then the file
   105  	// must be in this sharing, or the stack should refuse to open the file.
   106  	if sharingID != "" && o.Sharing != nil && o.Sharing.ID() == sharingID {
   107  		o.ClientID = pdoc.SourceID
   108  		return nil
   109  	}
   110  
   111  	fs := o.Inst.VFS()
   112  	return vfs.Allows(fs, pdoc.Permissions, permission.GET, o.File)
   113  }
   114  
   115  // ShouldOpenLocally returns true if the file can be opened in the current
   116  // instance, and false if it is a shared file created on another instance.
   117  func (o *FileOpener) ShouldOpenLocally() bool {
   118  	if o.File.CozyMetadata == nil {
   119  		return true
   120  	}
   121  	u, err := url.Parse(o.File.CozyMetadata.CreatedOn)
   122  	if err != nil {
   123  		return true
   124  	}
   125  	return o.Inst.HasDomain(u.Host) || o.Sharing == nil
   126  }
   127  
   128  // GetSharecode returns a sharecode that can be used to open the note with the
   129  // permissions of the member.
   130  func (o *FileOpener) GetSharecode(memberIndex int, readOnly bool) (string, error) {
   131  	s := o.Sharing
   132  	if s == nil || (o.ClientID == "" && o.MemberKey == "") {
   133  		return o.Code, nil
   134  	}
   135  
   136  	var member *Member
   137  	var err error
   138  	if o.MemberKey != "" {
   139  		// Preview of a cozy-to-cozy sharing
   140  		for i, m := range s.Members {
   141  			if m.Instance == o.MemberKey || m.Email == o.MemberKey {
   142  				member = &s.Members[i]
   143  			}
   144  		}
   145  		if member == nil {
   146  			return "", ErrMemberNotFound
   147  		}
   148  		if member.ReadOnly {
   149  			readOnly = true
   150  		} else {
   151  			readOnly = s.ReadOnlyRules()
   152  		}
   153  	} else if s.Owner {
   154  		member, err = s.FindMemberByInboundClientID(o.ClientID)
   155  		if err != nil {
   156  			return "", err
   157  		}
   158  		if member.ReadOnly {
   159  			readOnly = true
   160  		} else {
   161  			readOnly = s.ReadOnlyRules()
   162  		}
   163  	} else {
   164  		// Trust the owner
   165  		if memberIndex < 0 || memberIndex >= len(s.Members) {
   166  			return "", ErrMemberNotFound
   167  		}
   168  		member = &s.Members[memberIndex]
   169  	}
   170  
   171  	if readOnly {
   172  		return o.getPreviewCode(member, memberIndex)
   173  	}
   174  	return o.Sharing.GetInteractCode(o.Inst, member, memberIndex)
   175  }
   176  
   177  // getPreviewCode returns a sharecode that can be used for reading the file. It
   178  // uses a share-preview token.
   179  func (o *FileOpener) getPreviewCode(member *Member, memberIndex int) (string, error) {
   180  	preview, err := permission.GetForSharePreview(o.Inst, o.Sharing.ID())
   181  	if err != nil {
   182  		if couchdb.IsNotFoundError(err) {
   183  			preview, err = o.Sharing.CreatePreviewPermissions(o.Inst)
   184  		}
   185  		if err != nil {
   186  			return "", err
   187  		}
   188  	}
   189  
   190  	indexKey := keyFromMemberIndex(memberIndex)
   191  	for key, code := range preview.ShortCodes {
   192  		if key == "" {
   193  			continue
   194  		}
   195  		if key == member.Instance || key == member.Email || key == indexKey {
   196  			return code, nil
   197  		}
   198  	}
   199  	for key, code := range preview.Codes {
   200  		if key == "" {
   201  			continue
   202  		}
   203  		if key == member.Instance || key == member.Email || key == indexKey {
   204  			return code, nil
   205  		}
   206  	}
   207  
   208  	return "", ErrCannotOpenFile
   209  }
   210  
   211  // OpenFileParameters is the list of parameters for building the URL where the
   212  // file can be opened in the browser.
   213  type OpenFileParameters struct {
   214  	FileID    string // ID of the file on the instance where the file can be edited
   215  	Subdomain string
   216  	Protocol  string
   217  	Instance  string
   218  	Sharecode string
   219  }
   220  
   221  // OpenLocalFile returns the parameters for opening the file on the local instance.
   222  func (o *FileOpener) OpenLocalFile(code string) OpenFileParameters {
   223  	params := OpenFileParameters{
   224  		FileID:    o.File.ID(),
   225  		Instance:  o.Inst.ContextualDomain(),
   226  		Sharecode: code,
   227  	}
   228  	switch config.GetConfig().Subdomains {
   229  	case config.FlatSubdomains:
   230  		params.Subdomain = "flat"
   231  	case config.NestedSubdomains:
   232  		params.Subdomain = "nested"
   233  	}
   234  	params.Protocol = "https"
   235  	if build.IsDevRelease() {
   236  		params.Protocol = "http"
   237  	}
   238  	return params
   239  }
   240  
   241  // PreparedRequest contains the parameters to make a request to another
   242  // instance for opening a shared file. If it is not possible, Opts will be
   243  // empty and the MemberIndex and ReadOnly fields can be used for opening
   244  // locally the file.
   245  type PreparedRequest struct {
   246  	Opts    *request.Options // Can be nil
   247  	XoredID string
   248  	Creds   *Credentials
   249  	Creator *Member
   250  	// MemberIndex and ReadOnly can be used even if Opts is nil
   251  	MemberIndex int
   252  	ReadOnly    bool
   253  }
   254  
   255  // PrepareRequestForSharedFile returns the parameters for making a request to
   256  // open the shared file on another instance.
   257  func (o *FileOpener) PrepareRequestForSharedFile() (*PreparedRequest, error) {
   258  	s := o.Sharing
   259  	prepared := PreparedRequest{}
   260  
   261  	if s.Owner {
   262  		domain := o.File.CozyMetadata.CreatedOn
   263  		for i, m := range s.Members {
   264  			if i == 0 {
   265  				continue // Skip the owner
   266  			}
   267  			// XXX Skip the not-ready members, as an instance can be listed
   268  			// several times in members; with different email addresses and
   269  			// status.
   270  			if m.Status != MemberStatusReady {
   271  				continue
   272  			}
   273  			if m.Instance == domain || m.Instance+"/" == domain {
   274  				prepared.Creds = &s.Credentials[i-1]
   275  				prepared.Creator = &s.Members[i]
   276  			}
   277  		}
   278  		if o.ClientID != "" && !prepared.ReadOnly {
   279  			for i, c := range s.Credentials {
   280  				if c.InboundClientID == o.ClientID {
   281  					prepared.MemberIndex = i + 1
   282  					prepared.ReadOnly = s.Members[i+1].ReadOnly
   283  				}
   284  			}
   285  		}
   286  	} else {
   287  		prepared.Creds = &s.Credentials[0]
   288  		prepared.Creator = &s.Members[0]
   289  	}
   290  
   291  	if prepared.Creator == nil ||
   292  		(prepared.Creator.Status != MemberStatusReady && prepared.Creator.Status != MemberStatusOwner) {
   293  		// If the creator of the file is no longer in the sharing, the owner of
   294  		// the sharing takes the lead, and if the sharing is revoked, any
   295  		// member can edit the file on their instance.
   296  		if o.ClientID == "" {
   297  			o.Sharing = nil
   298  		}
   299  		return &prepared, nil
   300  	}
   301  
   302  	prepared.XoredID = XorID(o.File.ID(), prepared.Creds.XorKey)
   303  	u, err := url.Parse(prepared.Creator.Instance)
   304  	if err != nil {
   305  		return nil, ErrCannotOpenFile
   306  	}
   307  	prepared.Opts = &request.Options{
   308  		Method: http.MethodGet,
   309  		Scheme: u.Scheme,
   310  		Domain: u.Host,
   311  		Queries: url.Values{
   312  			"SharingID":   {s.ID()},
   313  			"MemberIndex": {strconv.FormatInt(int64(prepared.MemberIndex), 10)},
   314  			"ReadOnly":    {strconv.FormatBool(prepared.ReadOnly)},
   315  		},
   316  		Headers: request.Headers{
   317  			echo.HeaderAccept:        jsonapi.ContentType,
   318  			echo.HeaderAuthorization: "Bearer " + prepared.Creds.AccessToken.AccessToken,
   319  		},
   320  		ParseError: ParseRequestError,
   321  	}
   322  	return &prepared, nil
   323  }