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 }