github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/propfind/propfind.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package propfind 20 21 import ( 22 "context" 23 "encoding/json" 24 "encoding/xml" 25 "fmt" 26 "io" 27 "net/http" 28 "net/url" 29 "path" 30 "path/filepath" 31 "strconv" 32 "strings" 33 "time" 34 35 gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" 36 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 37 link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" 38 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 39 typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 40 "github.com/cs3org/reva/v2/internal/grpc/services/storageprovider" 41 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config" 42 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors" 43 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 44 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/prop" 45 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/spacelookup" 46 "github.com/cs3org/reva/v2/pkg/appctx" 47 "github.com/cs3org/reva/v2/pkg/conversions" 48 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 49 "github.com/cs3org/reva/v2/pkg/publicshare" 50 rstatus "github.com/cs3org/reva/v2/pkg/rgrpc/status" 51 "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" 52 "github.com/cs3org/reva/v2/pkg/rhttp/router" 53 "github.com/cs3org/reva/v2/pkg/storagespace" 54 "github.com/cs3org/reva/v2/pkg/utils" 55 "github.com/iancoleman/strcase" 56 "github.com/rs/zerolog" 57 "go.opentelemetry.io/otel/attribute" 58 "go.opentelemetry.io/otel/codes" 59 semconv "go.opentelemetry.io/otel/semconv/v1.20.0" 60 "golang.org/x/sync/errgroup" 61 "google.golang.org/protobuf/types/known/fieldmaskpb" 62 ) 63 64 const ( 65 tracerName = "ocdav" 66 ) 67 68 // these keys are used to lookup in ArbitraryMetadata, generated prop names are lowercased 69 var ( 70 audioKeys = []string{ 71 "album", 72 "albumArtist", 73 "artist", 74 "bitrate", 75 "composers", 76 "copyright", 77 "disc", 78 "discCount", 79 "duration", 80 "genre", 81 "hasDrm", 82 "isVariableBitrate", 83 "title", 84 "track", 85 "trackCount", 86 "year", 87 } 88 locationKeys = []string{ 89 "altitude", 90 "latitude", 91 "longitude", 92 } 93 imageKeys = []string{ 94 "width", 95 "height", 96 } 97 photoKeys = []string{ 98 "cameraMake", 99 "cameraModel", 100 "exposureDenominator", 101 "exposureNumerator", 102 "fNumber", 103 "focalLength", 104 "iso", 105 "orientation", 106 "takenDateTime", 107 } 108 ) 109 110 type countingReader struct { 111 n int 112 r io.Reader 113 } 114 115 // Props represents properties related to a resource 116 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind) 117 type Props []xml.Name 118 119 // XML holds the xml representation of a propfind 120 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind 121 type XML struct { 122 XMLName xml.Name `xml:"DAV: propfind"` 123 Allprop *struct{} `xml:"DAV: allprop"` 124 Propname *struct{} `xml:"DAV: propname"` 125 Prop Props `xml:"DAV: prop"` 126 Include Props `xml:"DAV: include"` 127 } 128 129 // PropstatXML holds the xml representation of a propfind response 130 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat 131 type PropstatXML struct { 132 // Prop requires DAV: to be the default namespace in the enclosing 133 // XML. This is due to the standard encoding/xml package currently 134 // not honoring namespace declarations inside a xmltag with a 135 // parent element for anonymous slice elements. 136 // Use of multistatusWriter takes care of this. 137 Prop []prop.PropertyXML `xml:"d:prop>_ignored_"` 138 Status string `xml:"d:status"` 139 Error *errors.ErrorXML `xml:"d:error"` 140 ResponseDescription string `xml:"d:responsedescription,omitempty"` 141 } 142 143 // ResponseXML holds the xml representation of a propfind response 144 type ResponseXML struct { 145 XMLName xml.Name `xml:"d:response"` 146 Href string `xml:"d:href"` 147 Propstat []PropstatXML `xml:"d:propstat"` 148 Status string `xml:"d:status,omitempty"` 149 Error *errors.ErrorXML `xml:"d:error"` 150 ResponseDescription string `xml:"d:responsedescription,omitempty"` 151 } 152 153 // MultiStatusResponseXML holds the xml representation of a multistatus propfind response 154 type MultiStatusResponseXML struct { 155 XMLName xml.Name `xml:"d:multistatus"` 156 XmlnsS string `xml:"xmlns:s,attr,omitempty"` 157 XmlnsD string `xml:"xmlns:d,attr,omitempty"` 158 XmlnsOC string `xml:"xmlns:oc,attr,omitempty"` 159 160 Responses []*ResponseXML `xml:"d:response"` 161 } 162 163 // ResponseUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 164 type ResponseUnmarshalXML struct { 165 XMLName xml.Name `xml:"response"` 166 Href string `xml:"href"` 167 Propstat []PropstatUnmarshalXML `xml:"propstat"` 168 Status string `xml:"status,omitempty"` 169 Error *errors.ErrorXML `xml:"d:error"` 170 ResponseDescription string `xml:"responsedescription,omitempty"` 171 } 172 173 // MultiStatusResponseUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 174 type MultiStatusResponseUnmarshalXML struct { 175 XMLName xml.Name `xml:"multistatus"` 176 XmlnsS string `xml:"xmlns:s,attr,omitempty"` 177 XmlnsD string `xml:"xmlns:d,attr,omitempty"` 178 XmlnsOC string `xml:"xmlns:oc,attr,omitempty"` 179 180 Responses []*ResponseUnmarshalXML `xml:"response"` 181 } 182 183 // PropstatUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 184 type PropstatUnmarshalXML struct { 185 // Prop requires DAV: to be the default namespace in the enclosing 186 // XML. This is due to the standard encoding/xml package currently 187 // not honoring namespace declarations inside a xmltag with a 188 // parent element for anonymous slice elements. 189 // Use of multistatusWriter takes care of this. 190 Prop []*prop.PropertyXML `xml:"prop"` 191 Status string `xml:"status"` 192 Error *errors.ErrorXML `xml:"d:error"` 193 ResponseDescription string `xml:"responsedescription,omitempty"` 194 } 195 196 // spaceData is used to remember the space for a resource info 197 type spaceData struct { 198 Ref *provider.Reference 199 SpaceType string 200 } 201 202 // NewMultiStatusResponseXML returns a preconfigured instance of MultiStatusResponseXML 203 func NewMultiStatusResponseXML() *MultiStatusResponseXML { 204 return &MultiStatusResponseXML{ 205 XmlnsD: "DAV:", 206 XmlnsS: "http://sabredav.org/ns", 207 XmlnsOC: "http://owncloud.org/ns", 208 } 209 } 210 211 // Handler handles propfind requests 212 type Handler struct { 213 PublicURL string 214 selector pool.Selectable[gateway.GatewayAPIClient] 215 c *config.Config 216 } 217 218 // NewHandler returns a new PropfindHandler instance 219 func NewHandler(publicURL string, selector pool.Selectable[gateway.GatewayAPIClient], c *config.Config) *Handler { 220 return &Handler{ 221 PublicURL: publicURL, 222 selector: selector, 223 c: c, 224 } 225 } 226 227 // HandlePathPropfind handles a path based propfind request 228 // ns is the namespace that is prefixed to the path in the cs3 namespace 229 func (p *Handler) HandlePathPropfind(w http.ResponseWriter, r *http.Request, ns string) { 230 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path)) 231 defer span.End() 232 233 fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces? 234 235 sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() 236 dh := r.Header.Get(net.HeaderDepth) 237 238 depth, err := net.ParseDepth(dh) 239 if err != nil { 240 span.RecordError(err) 241 span.SetStatus(codes.Error, "Invalid Depth header value") 242 span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest)) 243 sublog.Debug().Str("depth", dh).Msg(err.Error()) 244 w.WriteHeader(http.StatusBadRequest) 245 m := fmt.Sprintf("Invalid Depth header value: %v", dh) 246 b, err := errors.Marshal(http.StatusBadRequest, m, "", "") 247 errors.HandleWebdavError(&sublog, w, b, err) 248 return 249 } 250 251 if depth == net.DepthInfinity && !p.c.AllowPropfindDepthInfinitiy { 252 span.RecordError(errors.ErrInvalidDepth) 253 span.SetStatus(codes.Error, "DEPTH: infinity is not supported") 254 span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest)) 255 sublog.Debug().Str("depth", dh).Msg(errors.ErrInvalidDepth.Error()) 256 w.WriteHeader(http.StatusBadRequest) 257 m := fmt.Sprintf("Invalid Depth header value: %v", dh) 258 b, err := errors.Marshal(http.StatusBadRequest, m, "", "") 259 errors.HandleWebdavError(&sublog, w, b, err) 260 return 261 } 262 263 pf, status, err := ReadPropfind(r.Body) 264 if err != nil { 265 sublog.Debug().Err(err).Msg("error reading propfind request") 266 w.WriteHeader(status) 267 return 268 } 269 270 // retrieve a specific storage space 271 client, err := p.selector.Next() 272 if err != nil { 273 sublog.Error().Err(err).Msg("error retrieving a gateway service client") 274 w.WriteHeader(http.StatusInternalServerError) 275 return 276 } 277 278 // TODO look up all spaces and request the root_info in the field mask 279 spaces, rpcStatus, err := spacelookup.LookUpStorageSpacesForPathWithChildren(ctx, client, fn) 280 if err != nil { 281 sublog.Error().Err(err).Msg("error sending a grpc request") 282 w.WriteHeader(http.StatusInternalServerError) 283 return 284 } 285 286 if rpcStatus.Code != rpc.Code_CODE_OK { 287 errors.HandleErrorStatus(&sublog, w, rpcStatus) 288 return 289 } 290 291 resourceInfos, sendTusHeaders, ok := p.getResourceInfos(ctx, w, r, pf, spaces, fn, depth, sublog) 292 if !ok { 293 // getResourceInfos handles responses in case of an error so we can just return here. 294 return 295 } 296 p.propfindResponse(ctx, w, r, ns, pf, sendTusHeaders, resourceInfos, sublog) 297 } 298 299 // HandleSpacesPropfind handles a spaces based propfind request 300 func (p *Handler) HandleSpacesPropfind(w http.ResponseWriter, r *http.Request, spaceID string) { 301 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_propfind") 302 defer span.End() 303 304 sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Logger() 305 dh := r.Header.Get(net.HeaderDepth) 306 307 depth, err := net.ParseDepth(dh) 308 if err != nil { 309 span.RecordError(err) 310 span.SetStatus(codes.Error, "Invalid Depth header value") 311 span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest)) 312 sublog.Debug().Str("depth", dh).Msg(err.Error()) 313 w.WriteHeader(http.StatusBadRequest) 314 m := fmt.Sprintf("Invalid Depth header value: %v", dh) 315 b, err := errors.Marshal(http.StatusBadRequest, m, "", "") 316 errors.HandleWebdavError(&sublog, w, b, err) 317 return 318 } 319 320 if depth == net.DepthInfinity && !p.c.AllowPropfindDepthInfinitiy { 321 span.RecordError(errors.ErrInvalidDepth) 322 span.SetStatus(codes.Error, "DEPTH: infinity is not supported") 323 span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest)) 324 sublog.Debug().Str("depth", dh).Msg(errors.ErrInvalidDepth.Error()) 325 w.WriteHeader(http.StatusBadRequest) 326 m := fmt.Sprintf("Invalid Depth header value: %v", dh) 327 b, err := errors.Marshal(http.StatusBadRequest, m, "", "") 328 errors.HandleWebdavError(&sublog, w, b, err) 329 return 330 } 331 332 pf, status, err := ReadPropfind(r.Body) 333 if err != nil { 334 sublog.Debug().Err(err).Msg("error reading propfind request") 335 w.WriteHeader(status) 336 return 337 } 338 339 ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path) 340 if err != nil { 341 sublog.Debug().Msg("invalid space id") 342 w.WriteHeader(http.StatusBadRequest) 343 m := fmt.Sprintf("Invalid space id: %v", spaceID) 344 b, err := errors.Marshal(http.StatusBadRequest, m, "", "") 345 errors.HandleWebdavError(&sublog, w, b, err) 346 return 347 } 348 349 client, err := p.selector.Next() 350 if err != nil { 351 sublog.Error().Err(err).Msg("error getting grpc client") 352 w.WriteHeader(http.StatusInternalServerError) 353 return 354 } 355 356 metadataKeys, _ := metadataKeys(pf) 357 358 // stat the reference and request the space in the field mask 359 res, err := client.Stat(ctx, &provider.StatRequest{ 360 Ref: &ref, 361 ArbitraryMetadataKeys: metadataKeys, 362 FieldMask: &fieldmaskpb.FieldMask{Paths: []string{"*"}}, // TODO use more sophisticated filter? we don't need all space properties, afaict only the spacetype 363 }) 364 if err != nil { 365 sublog.Error().Err(err).Msg("error getting grpc client") 366 w.WriteHeader(http.StatusInternalServerError) 367 return 368 } 369 if res.Status.Code != rpc.Code_CODE_OK { 370 status := rstatus.HTTPStatusFromCode(res.Status.Code) 371 if res.Status.Code == rpc.Code_CODE_ABORTED { 372 // aborted is used for etag an lock mismatches, which translates to 412 373 // in case a real Conflict response is needed, the calling code needs to send the header 374 status = http.StatusPreconditionFailed 375 } 376 m := res.Status.Message 377 if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { 378 // check if user has access to resource 379 sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: ref.GetResourceId()}}) 380 if err != nil { 381 sublog.Error().Err(err).Msg("error performing stat grpc request") 382 w.WriteHeader(http.StatusInternalServerError) 383 return 384 } 385 if sRes.Status.Code != rpc.Code_CODE_OK { 386 // return not found error so we do not leak existence of a space 387 status = http.StatusNotFound 388 } 389 } 390 if status == http.StatusNotFound { 391 m = "Resource not found" // mimic the oc10 error message 392 } 393 w.WriteHeader(status) 394 b, err := errors.Marshal(status, m, "", "") 395 errors.HandleWebdavError(&sublog, w, b, err) 396 return 397 } 398 var space *provider.StorageSpace 399 if res.Info.Space == nil { 400 sublog.Debug().Msg("stat did not include a space, executing an additional lookup request") 401 // fake a space root 402 space = &provider.StorageSpace{ 403 Id: &provider.StorageSpaceId{OpaqueId: spaceID}, 404 Opaque: &typesv1beta1.Opaque{ 405 Map: map[string]*typesv1beta1.OpaqueEntry{ 406 "path": { 407 Decoder: "plain", 408 Value: []byte("/"), 409 }, 410 }, 411 }, 412 Root: ref.ResourceId, 413 RootInfo: res.Info, 414 } 415 } 416 417 res.Info.Path = r.URL.Path 418 419 resourceInfos := []*provider.ResourceInfo{ 420 res.Info, 421 } 422 if res.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER && depth != net.DepthZero { 423 childInfos, ok := p.getSpaceResourceInfos(ctx, w, r, pf, &ref, r.URL.Path, depth, sublog) 424 if !ok { 425 // getResourceInfos handles responses in case of an error so we can just return here. 426 return 427 } 428 resourceInfos = append(resourceInfos, childInfos...) 429 } 430 431 // prefix space id to paths 432 for i := range resourceInfos { 433 resourceInfos[i].Path = path.Join("/", spaceID, resourceInfos[i].Path) 434 // add space to info so propfindResponse can access space type 435 if resourceInfos[i].Space == nil { 436 resourceInfos[i].Space = space 437 } 438 } 439 440 sendTusHeaders := true 441 // let clients know this collection supports tus.io POST requests to start uploads 442 if res.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 443 if res.Info.Opaque != nil { 444 _, ok := res.Info.Opaque.Map["disable_tus"] 445 sendTusHeaders = !ok 446 } 447 } 448 449 p.propfindResponse(ctx, w, r, "", pf, sendTusHeaders, resourceInfos, sublog) 450 } 451 452 func (p *Handler) propfindResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, namespace string, pf XML, sendTusHeaders bool, resourceInfos []*provider.ResourceInfo, log zerolog.Logger) { 453 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(ctx, "propfind_response") 454 defer span.End() 455 456 var linkshares map[string]struct{} 457 // public link access does not show share-types 458 // oc:share-type is not part of an allprops response 459 if namespace != "/public" { 460 // only fetch this if property was queried 461 for _, prop := range pf.Prop { 462 if prop.Space == net.NsOwncloud && (prop.Local == "share-types" || prop.Local == "permissions") { 463 filters := make([]*link.ListPublicSharesRequest_Filter, 0, len(resourceInfos)) 464 for i := range resourceInfos { 465 // FIXME this is expensive 466 // the filters array grow by one for every file in a folder 467 // TODO store public links as grants on the storage, reassembling them here is too costly 468 // we can then add the filter if the file has share-types=3 in the opaque, 469 // same as user / group shares for share indicators 470 filters = append(filters, publicshare.ResourceIDFilter(resourceInfos[i].Id)) 471 } 472 client, err := p.selector.Next() 473 if err != nil { 474 log.Error().Err(err).Msg("error getting grpc client") 475 w.WriteHeader(http.StatusInternalServerError) 476 return 477 } 478 listResp, err := client.ListPublicShares(ctx, &link.ListPublicSharesRequest{Filters: filters}) 479 if err == nil { 480 linkshares = make(map[string]struct{}, len(listResp.Share)) 481 for i := range listResp.Share { 482 linkshares[listResp.Share[i].ResourceId.OpaqueId] = struct{}{} 483 } 484 } else { 485 log.Error().Err(err).Msg("propfindResponse: couldn't list public shares") 486 span.SetStatus(codes.Error, err.Error()) 487 } 488 break 489 } 490 } 491 } 492 493 prefer := net.ParsePrefer(r.Header.Get(net.HeaderPrefer)) 494 returnMinimal := prefer[net.HeaderPreferReturn] == "minimal" 495 496 propRes, err := MultistatusResponse(ctx, &pf, resourceInfos, p.PublicURL, namespace, linkshares, returnMinimal) 497 if err != nil { 498 log.Error().Err(err).Msg("error formatting propfind") 499 w.WriteHeader(http.StatusInternalServerError) 500 return 501 } 502 w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol") 503 w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8") 504 if sendTusHeaders { 505 w.Header().Add(net.HeaderAccessControlExposeHeaders, net.HeaderTusResumable) 506 w.Header().Add(net.HeaderAccessControlExposeHeaders, net.HeaderTusVersion) 507 w.Header().Add(net.HeaderAccessControlExposeHeaders, net.HeaderTusExtension) 508 w.Header().Set(net.HeaderAccessControlExposeHeaders, strings.Join(w.Header().Values(net.HeaderAccessControlExposeHeaders), ", ")) 509 w.Header().Set(net.HeaderTusResumable, "1.0.0") 510 w.Header().Set(net.HeaderTusVersion, "1.0.0") 511 w.Header().Set(net.HeaderTusExtension, "creation, creation-with-upload, checksum, expiration") 512 } 513 w.Header().Add(net.HeaderVary, net.HeaderPrefer) 514 w.Header().Set(net.HeaderVary, strings.Join(w.Header().Values(net.HeaderVary), ", ")) 515 if returnMinimal { 516 w.Header().Set(net.HeaderPreferenceApplied, "return=minimal") 517 } 518 519 w.WriteHeader(http.StatusMultiStatus) 520 if _, err := w.Write(propRes); err != nil { 521 log.Err(err).Msg("error writing response") 522 } 523 } 524 525 // TODO this is just a stat -> rename 526 func (p *Handler) statSpace(ctx context.Context, ref *provider.Reference, metadataKeys, fieldMaskPaths []string) (*provider.ResourceInfo, *rpc.Status, error) { 527 client, err := p.selector.Next() 528 if err != nil { 529 return nil, nil, err 530 } 531 req := &provider.StatRequest{ 532 Ref: ref, 533 ArbitraryMetadataKeys: metadataKeys, 534 FieldMask: &fieldmaskpb.FieldMask{Paths: fieldMaskPaths}, 535 } 536 res, err := client.Stat(ctx, req) 537 if err != nil { 538 return nil, nil, err 539 } 540 return res.GetInfo(), res.GetStatus(), nil 541 } 542 543 func (p *Handler) getResourceInfos(ctx context.Context, w http.ResponseWriter, r *http.Request, pf XML, spaces []*provider.StorageSpace, requestPath string, depth net.Depth, log zerolog.Logger) ([]*provider.ResourceInfo, bool, bool) { 544 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "get_resource_infos") 545 span.SetAttributes(attribute.KeyValue{Key: "requestPath", Value: attribute.StringValue(requestPath)}) 546 span.SetAttributes(attribute.KeyValue{Key: "depth", Value: attribute.StringValue(depth.String())}) 547 defer span.End() 548 549 metadataKeys, fieldMaskPaths := metadataKeys(pf) 550 551 // we need to stat all spaces to aggregate the root etag, mtime and size 552 // TODO cache per space (hah, no longer per user + per space!) 553 var ( 554 err error 555 rootInfo *provider.ResourceInfo 556 mostRecentChildInfo *provider.ResourceInfo 557 aggregatedChildSize uint64 558 spaceMap = make(map[*provider.ResourceInfo]spaceData, len(spaces)) 559 ) 560 for _, space := range spaces { 561 spacePath := "" 562 if spacePath = utils.ReadPlainFromOpaque(space.Opaque, "path"); spacePath == "" { 563 continue // not mounted 564 } 565 if space.RootInfo == nil { 566 spaceRef, err := spacelookup.MakeStorageSpaceReference(space.Id.OpaqueId, ".") 567 if err != nil { 568 continue 569 } 570 info, status, err := p.statSpace(ctx, &spaceRef, metadataKeys, fieldMaskPaths) 571 if err != nil || status.GetCode() != rpc.Code_CODE_OK { 572 continue 573 } 574 space.RootInfo = info 575 } 576 577 // TODO separate stats to the path or to the children, after statting all children update the mtime/etag 578 // TODO get mtime, and size from space as well, so we no longer have to stat here? would require sending the requested metadata keys as well 579 // root should be a ResourceInfo so it can contain the full stat, not only the id ... do we even need spaces then? 580 // metadata keys could all be prefixed with "root." to indicate we want more than the root id ... 581 // TODO can we reuse the space.rootinfo? 582 spaceRef := spacelookup.MakeRelativeReference(space, requestPath, false) 583 var info *provider.ResourceInfo 584 if spaceRef.Path == "." && utils.ResourceIDEqual(spaceRef.ResourceId, space.Root) { 585 info = space.RootInfo 586 } else { 587 var status *rpc.Status 588 info, status, err = p.statSpace(ctx, spaceRef, metadataKeys, fieldMaskPaths) 589 if err != nil || status.GetCode() != rpc.Code_CODE_OK { 590 continue 591 } 592 } 593 594 // adjust path 595 info.Path = filepath.Join(spacePath, spaceRef.Path) 596 info.Name = filepath.Base(info.Path) 597 598 spaceMap[info] = spaceData{Ref: spaceRef, SpaceType: space.SpaceType} 599 600 if rootInfo == nil && requestPath == info.Path { 601 rootInfo = info 602 } else if requestPath != spacePath && strings.HasPrefix(spacePath, requestPath) { // Check if the space is a child of the requested path 603 // aggregate child metadata 604 aggregatedChildSize += info.Size 605 if mostRecentChildInfo == nil { 606 mostRecentChildInfo = info 607 continue 608 } 609 if mostRecentChildInfo.Mtime == nil || (info.Mtime != nil && utils.TSToUnixNano(info.Mtime) > utils.TSToUnixNano(mostRecentChildInfo.Mtime)) { 610 mostRecentChildInfo = info 611 } 612 } 613 } 614 615 if len(spaceMap) == 0 || rootInfo == nil { 616 // TODO if we have children invent node on the fly 617 w.WriteHeader(http.StatusNotFound) 618 m := "Resource not found" 619 b, err := errors.Marshal(http.StatusNotFound, m, "", "") 620 errors.HandleWebdavError(&log, w, b, err) 621 return nil, false, false 622 } 623 if mostRecentChildInfo != nil { 624 if rootInfo.Mtime == nil || (mostRecentChildInfo.Mtime != nil && utils.TSToUnixNano(mostRecentChildInfo.Mtime) > utils.TSToUnixNano(rootInfo.Mtime)) { 625 rootInfo.Mtime = mostRecentChildInfo.Mtime 626 if mostRecentChildInfo.Etag != "" { 627 rootInfo.Etag = mostRecentChildInfo.Etag 628 } 629 } 630 if rootInfo.Etag == "" { 631 rootInfo.Etag = mostRecentChildInfo.Etag 632 } 633 } 634 635 // add size of children 636 rootInfo.Size += aggregatedChildSize 637 638 resourceInfos := []*provider.ResourceInfo{ 639 rootInfo, // PROPFIND always includes the root resource 640 } 641 642 if rootInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE || depth == net.DepthZero { 643 // If the resource is a file then it can't have any children so we can 644 // stop here. 645 return resourceInfos, true, true 646 } 647 648 childInfos := map[string]*provider.ResourceInfo{} 649 for spaceInfo, spaceData := range spaceMap { 650 switch { 651 case spaceInfo.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER && depth != net.DepthInfinity: 652 addChild(childInfos, spaceInfo, requestPath, rootInfo) 653 654 case spaceInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER && depth == net.DepthOne: 655 switch { 656 case strings.HasPrefix(requestPath, spaceInfo.Path) && spaceData.SpaceType != "virtual": 657 client, err := p.selector.Next() 658 if err != nil { 659 log.Error().Err(err).Msg("error getting grpc client") 660 w.WriteHeader(http.StatusInternalServerError) 661 return nil, false, false 662 } 663 req := &provider.ListContainerRequest{ 664 Ref: spaceData.Ref, 665 ArbitraryMetadataKeys: metadataKeys, 666 } 667 res, err := client.ListContainer(ctx, req) 668 if err != nil { 669 log.Error().Err(err).Msg("error sending list container grpc request") 670 w.WriteHeader(http.StatusInternalServerError) 671 return nil, false, false 672 } 673 674 if res.Status.Code != rpc.Code_CODE_OK { 675 log.Debug().Interface("status", res.Status).Msg("List Container not ok, skipping") 676 continue 677 } 678 for _, info := range res.Infos { 679 info.Path = path.Join(requestPath, info.Path) 680 } 681 resourceInfos = append(resourceInfos, res.Infos...) 682 case strings.HasPrefix(spaceInfo.Path, requestPath): // space is a deep child of the requested path 683 addChild(childInfos, spaceInfo, requestPath, rootInfo) 684 } 685 686 case depth == net.DepthInfinity: 687 // use a stack to explore sub-containers breadth-first 688 if spaceInfo != rootInfo { 689 resourceInfos = append(resourceInfos, spaceInfo) 690 } 691 stack := []*provider.ResourceInfo{spaceInfo} 692 for len(stack) != 0 { 693 info := stack[0] 694 stack = stack[1:] 695 696 if info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER || spaceData.SpaceType == "virtual" { 697 continue 698 } 699 client, err := p.selector.Next() 700 if err != nil { 701 log.Error().Err(err).Msg("error getting grpc client") 702 w.WriteHeader(http.StatusInternalServerError) 703 return nil, false, false 704 } 705 req := &provider.ListContainerRequest{ 706 Ref: &provider.Reference{ 707 ResourceId: spaceInfo.Id, 708 // TODO here we cut of the path that we added after stating the space above 709 Path: utils.MakeRelativePath(strings.TrimPrefix(info.Path, spaceInfo.Path)), 710 }, 711 ArbitraryMetadataKeys: metadataKeys, 712 } 713 res, err := client.ListContainer(ctx, req) // FIXME public link depth infinity -> "gateway: could not find provider: gateway: error calling ListStorageProviders: rpc error: code = PermissionDenied desc = auth: core access token is invalid" 714 if err != nil { 715 log.Error().Err(err).Interface("info", info).Msg("error sending list container grpc request") 716 w.WriteHeader(http.StatusInternalServerError) 717 return nil, false, false 718 } 719 if res.Status.Code != rpc.Code_CODE_OK { 720 log.Debug().Interface("status", res.Status).Msg("List Container not ok, skipping") 721 continue 722 } 723 724 // check sub-containers in reverse order and add them to the stack 725 // the reversed order here will produce a more logical sorting of results 726 for i := len(res.Infos) - 1; i >= 0; i-- { 727 // add path to resource 728 res.Infos[i].Path = filepath.Join(info.Path, res.Infos[i].Path) 729 if res.Infos[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 730 stack = append(stack, res.Infos[i]) 731 } 732 } 733 734 resourceInfos = append(resourceInfos, res.Infos...) 735 // TODO: stream response to avoid storing too many results in memory 736 // we can do that after having stated the root. 737 } 738 } 739 } 740 741 if rootInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 742 // now add all aggregated child infos 743 for _, childInfo := range childInfos { 744 resourceInfos = append(resourceInfos, childInfo) 745 } 746 } 747 748 sendTusHeaders := true 749 // let clients know this collection supports tus.io POST requests to start uploads 750 if rootInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 751 if rootInfo.Opaque != nil { 752 _, ok := rootInfo.Opaque.Map["disable_tus"] 753 sendTusHeaders = !ok 754 } 755 } 756 757 return resourceInfos, sendTusHeaders, true 758 } 759 760 func (p *Handler) getSpaceResourceInfos(ctx context.Context, w http.ResponseWriter, r *http.Request, pf XML, ref *provider.Reference, requestPath string, depth net.Depth, log zerolog.Logger) ([]*provider.ResourceInfo, bool) { 761 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "get_space_resource_infos") 762 span.SetAttributes(attribute.KeyValue{Key: "requestPath", Value: attribute.StringValue(requestPath)}) 763 span.SetAttributes(attribute.KeyValue{Key: "depth", Value: attribute.StringValue(depth.String())}) 764 defer span.End() 765 766 client, err := p.selector.Next() 767 if err != nil { 768 log.Error().Err(err).Msg("error getting grpc client") 769 w.WriteHeader(http.StatusInternalServerError) 770 return nil, false 771 } 772 773 metadataKeys, _ := metadataKeys(pf) 774 775 resourceInfos := []*provider.ResourceInfo{} 776 777 req := &provider.ListContainerRequest{ 778 Ref: ref, 779 ArbitraryMetadataKeys: metadataKeys, 780 FieldMask: &fieldmaskpb.FieldMask{Paths: []string{"*"}}, // TODO use more sophisticated filter 781 } 782 res, err := client.ListContainer(ctx, req) 783 if err != nil { 784 log.Error().Err(err).Msg("error sending list container grpc request") 785 w.WriteHeader(http.StatusInternalServerError) 786 return nil, false 787 } 788 789 if res.Status.Code != rpc.Code_CODE_OK { 790 log.Debug().Interface("status", res.Status).Msg("List Container not ok, skipping") 791 w.WriteHeader(http.StatusInternalServerError) 792 return nil, false 793 } 794 for _, info := range res.Infos { 795 info.Path = path.Join(requestPath, info.Path) 796 } 797 resourceInfos = append(resourceInfos, res.Infos...) 798 799 if depth == net.DepthInfinity { 800 // use a stack to explore sub-containers breadth-first 801 stack := resourceInfos 802 for len(stack) != 0 { 803 info := stack[0] 804 stack = stack[1:] 805 806 if info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER /*|| space.SpaceType == "virtual"*/ { 807 continue 808 } 809 req := &provider.ListContainerRequest{ 810 Ref: &provider.Reference{ 811 ResourceId: info.Id, 812 Path: ".", 813 }, 814 ArbitraryMetadataKeys: metadataKeys, 815 } 816 res, err := client.ListContainer(ctx, req) // FIXME public link depth infinity -> "gateway: could not find provider: gateway: error calling ListStorageProviders: rpc error: code = PermissionDenied desc = auth: core access token is invalid" 817 if err != nil { 818 log.Error().Err(err).Interface("info", info).Msg("error sending list container grpc request") 819 w.WriteHeader(http.StatusInternalServerError) 820 return nil, false 821 } 822 if res.Status.Code != rpc.Code_CODE_OK { 823 log.Debug().Interface("status", res.Status).Msg("List Container not ok, skipping") 824 continue 825 } 826 827 // check sub-containers in reverse order and add them to the stack 828 // the reversed order here will produce a more logical sorting of results 829 for i := len(res.Infos) - 1; i >= 0; i-- { 830 // add path to resource 831 res.Infos[i].Path = filepath.Join(info.Path, res.Infos[i].Path) 832 res.Infos[i].Path = utils.MakeRelativePath(res.Infos[i].Path) 833 if res.Infos[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 834 stack = append(stack, res.Infos[i]) 835 } 836 } 837 838 resourceInfos = append(resourceInfos, res.Infos...) 839 // TODO: stream response to avoid storing too many results in memory 840 // we can do that after having stated the root. 841 } 842 } 843 844 return resourceInfos, true 845 } 846 847 func metadataKeysWithPrefix(prefix string, keys []string) []string { 848 fullKeys := []string{} 849 for _, key := range keys { 850 fullKeys = append(fullKeys, fmt.Sprintf("%s.%s", prefix, key)) 851 } 852 return fullKeys 853 } 854 855 // metadataKeys splits the propfind properties into arbitrary metadata and ResourceInfo field mask paths 856 func metadataKeys(pf XML) ([]string, []string) { 857 858 var metadataKeys []string 859 var fieldMaskKeys []string 860 861 if pf.Allprop != nil { 862 // TODO this changes the behavior and returns all properties if allprops has been set, 863 // but allprops should only return some default properties 864 // see https://tools.ietf.org/html/rfc4918#section-9.1 865 // the description of arbitrary_metadata_keys in https://cs3org.github.io/cs3apis/#cs3.storage.provider.v1beta1.ListContainerRequest an others may need clarification 866 // tracked in https://github.com/cs3org/cs3apis/issues/104 867 metadataKeys = append(metadataKeys, "*") 868 fieldMaskKeys = append(fieldMaskKeys, "*") 869 } else { 870 metadataKeys = make([]string, 0, len(pf.Prop)) 871 fieldMaskKeys = make([]string, 0, len(pf.Prop)) 872 for i := range pf.Prop { 873 if requiresExplicitFetching(&pf.Prop[i]) { 874 key := metadataKeyOf(&pf.Prop[i]) 875 switch key { 876 case "share-types": 877 fieldMaskKeys = append(fieldMaskKeys, key) 878 case "http://owncloud.org/ns/audio": 879 metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.audio", audioKeys)...) 880 case "http://owncloud.org/ns/location": 881 metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.location", locationKeys)...) 882 case "http://owncloud.org/ns/image": 883 metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.image", imageKeys)...) 884 case "http://owncloud.org/ns/photo": 885 metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.photo", photoKeys)...) 886 default: 887 metadataKeys = append(metadataKeys, key) 888 } 889 890 } 891 } 892 } 893 return metadataKeys, fieldMaskKeys 894 } 895 896 func addChild(childInfos map[string]*provider.ResourceInfo, 897 spaceInfo *provider.ResourceInfo, 898 requestPath string, 899 rootInfo *provider.ResourceInfo, 900 ) { 901 if spaceInfo == rootInfo { 902 return // already accounted for 903 } 904 905 childPath := strings.TrimPrefix(spaceInfo.Path, requestPath) 906 childName, tail := router.ShiftPath(childPath) 907 if tail != "/" { 908 spaceInfo.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER 909 spaceInfo.Checksum = nil 910 // TODO unset opaque checksum 911 } 912 spaceInfo.Path = path.Join(requestPath, childName) 913 if existingChild, ok := childInfos[childName]; ok { 914 // aggregate size 915 childInfos[childName].Size += spaceInfo.Size 916 // use most recent child 917 if existingChild.Mtime == nil || (spaceInfo.Mtime != nil && utils.TSToUnixNano(spaceInfo.Mtime) > utils.TSToUnixNano(existingChild.Mtime)) { 918 childInfos[childName].Mtime = spaceInfo.Mtime 919 childInfos[childName].Etag = spaceInfo.Etag 920 } 921 // only update fileid if the resource is a direct child 922 if tail == "/" { 923 childInfos[childName].Id = spaceInfo.Id 924 } 925 } else { 926 childInfos[childName] = spaceInfo 927 } 928 } 929 930 func requiresExplicitFetching(n *xml.Name) bool { 931 switch n.Space { 932 case net.NsDav: 933 switch n.Local { 934 case "quota-available-bytes", "quota-used-bytes", "lockdiscovery": 935 // A <DAV:allprop> PROPFIND request SHOULD NOT return DAV:quota-available-bytes and DAV:quota-used-bytes 936 // from https://www.rfc-editor.org/rfc/rfc4331.html#section-2 937 return true 938 default: 939 return false 940 } 941 case net.NsOwncloud: 942 switch n.Local { 943 case "favorite", "share-types", "checksums", "size", "tags", "audio", "location", "image", "photo": 944 return true 945 default: 946 return false 947 } 948 case net.NsOCS: 949 return false 950 } 951 return true 952 } 953 954 // ReadPropfind extracts and parses the propfind XML information from a Reader 955 // from https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/xml.go#L178-L205 956 func ReadPropfind(r io.Reader) (pf XML, status int, err error) { 957 c := countingReader{r: r} 958 if err = xml.NewDecoder(&c).Decode(&pf); err != nil { 959 if err == io.EOF { 960 if c.n == 0 { 961 // An empty body means to propfind allprop. 962 // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND 963 return XML{Allprop: new(struct{})}, 0, nil 964 } 965 err = errors.ErrInvalidPropfind 966 } 967 return XML{}, http.StatusBadRequest, err 968 } 969 970 if pf.Allprop == nil && pf.Include != nil { 971 return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind 972 } 973 if pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) { 974 return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind 975 } 976 if pf.Prop != nil && pf.Propname != nil { 977 return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind 978 } 979 if pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil { 980 // jfd: I think <d:prop></d:prop> is perfectly valid ... treat it as allprop 981 return XML{Allprop: new(struct{})}, 0, nil 982 } 983 return pf, 0, nil 984 } 985 986 // MultistatusResponse converts a list of resource infos into a multistatus response string 987 func MultistatusResponse(ctx context.Context, pf *XML, mds []*provider.ResourceInfo, publicURL, ns string, linkshares map[string]struct{}, returnMinimal bool) ([]byte, error) { 988 g, ctx := errgroup.WithContext(ctx) 989 990 type work struct { 991 position int 992 info *provider.ResourceInfo 993 } 994 type result struct { 995 position int 996 info *ResponseXML 997 } 998 workChan := make(chan work, len(mds)) 999 resultChan := make(chan result, len(mds)) 1000 1001 // Distribute work 1002 g.Go(func() error { 1003 defer close(workChan) 1004 for i, md := range mds { 1005 select { 1006 case workChan <- work{position: i, info: md}: 1007 case <-ctx.Done(): 1008 return ctx.Err() 1009 } 1010 } 1011 return nil 1012 }) 1013 1014 // Spawn workers that'll concurrently work the queue 1015 numWorkers := 50 1016 if len(mds) < numWorkers { 1017 numWorkers = len(mds) 1018 } 1019 for i := 0; i < numWorkers; i++ { 1020 g.Go(func() error { 1021 for work := range workChan { 1022 res, err := mdToPropResponse(ctx, pf, work.info, publicURL, ns, linkshares, returnMinimal) 1023 if err != nil { 1024 return err 1025 } 1026 select { 1027 case resultChan <- result{position: work.position, info: res}: 1028 case <-ctx.Done(): 1029 return ctx.Err() 1030 } 1031 } 1032 return nil 1033 }) 1034 } 1035 1036 // Wait for things to settle down, then close results chan 1037 go func() { 1038 _ = g.Wait() // error is checked later 1039 close(resultChan) 1040 }() 1041 1042 if err := g.Wait(); err != nil { 1043 return nil, err 1044 } 1045 1046 responses := make([]*ResponseXML, len(mds)) 1047 for res := range resultChan { 1048 responses[res.position] = res.info 1049 } 1050 1051 msr := NewMultiStatusResponseXML() 1052 msr.Responses = responses 1053 msg, err := xml.Marshal(msr) 1054 if err != nil { 1055 return nil, err 1056 } 1057 return msg, nil 1058 } 1059 1060 // mdToPropResponse converts the CS3 metadata into a webdav PropResponse 1061 // ns is the CS3 namespace that needs to be removed from the CS3 path before 1062 // prefixing it with the baseURI 1063 func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, publicURL, ns string, linkshares map[string]struct{}, returnMinimal bool) (*ResponseXML, error) { 1064 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "md_to_prop_response") 1065 span.SetAttributes(attribute.KeyValue{Key: "publicURL", Value: attribute.StringValue(publicURL)}) 1066 span.SetAttributes(attribute.KeyValue{Key: "ns", Value: attribute.StringValue(ns)}) 1067 defer span.End() 1068 1069 sublog := appctx.GetLogger(ctx).With().Interface("md", md).Str("ns", ns).Logger() 1070 id := md.Id 1071 p := strings.TrimPrefix(md.Path, ns) 1072 1073 baseURI := ctx.Value(net.CtxKeyBaseURI).(string) 1074 1075 ref := path.Join(baseURI, p) 1076 if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 1077 ref += "/" 1078 } 1079 1080 response := ResponseXML{ 1081 Href: net.EncodePath(ref), 1082 Propstat: []PropstatXML{}, 1083 } 1084 1085 var ls *link.PublicShare 1086 1087 // -1 indicates uncalculated 1088 // -2 indicates unknown (default) 1089 // -3 indicates unlimited 1090 quota := net.PropQuotaUnknown 1091 size := strconv.FormatUint(md.Size, 10) 1092 var lock *provider.Lock 1093 shareTypes := "" 1094 // TODO refactor helper functions: GetOpaqueJSONEncoded(opaque, key string, *struct) err, GetOpaquePlainEncoded(opaque, key) value, err 1095 // or use ok like pattern and return bool? 1096 if md.Opaque != nil && md.Opaque.Map != nil { 1097 if md.Opaque.Map["link-share"] != nil && md.Opaque.Map["link-share"].Decoder == "json" { 1098 ls = &link.PublicShare{} 1099 err := json.Unmarshal(md.Opaque.Map["link-share"].Value, ls) 1100 if err != nil { 1101 sublog.Error().Err(err).Msg("could not unmarshal link json") 1102 } 1103 } 1104 if quota = utils.ReadPlainFromOpaque(md.Opaque, "quota"); quota == "" { 1105 quota = net.PropQuotaUnknown 1106 } 1107 if md.Opaque.Map["lock"] != nil && md.Opaque.Map["lock"].Decoder == "json" { 1108 lock = &provider.Lock{} 1109 err := json.Unmarshal(md.Opaque.Map["lock"].Value, lock) 1110 if err != nil { 1111 sublog.Error().Err(err).Msg("could not unmarshal locks json") 1112 } 1113 } 1114 shareTypes = utils.ReadPlainFromOpaque(md.Opaque, "share-types") 1115 } 1116 role := conversions.RoleFromResourcePermissions(md.PermissionSet, ls != nil) 1117 1118 if md.Space != nil && md.Space.SpaceType != "grant" && utils.ResourceIDEqual(md.Space.Root, id) { 1119 // a space root is never shared 1120 shareTypes = "" 1121 } 1122 var wdp string 1123 isPublic := ls != nil 1124 isShared := shareTypes != "" && !net.IsCurrentUserOwnerOrManager(ctx, md.Owner, md) 1125 if md.PermissionSet != nil { 1126 wdp = role.WebDAVPermissions( 1127 md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER, 1128 isShared, 1129 false, 1130 isPublic, 1131 ) 1132 } 1133 1134 // replace fileid of /public/{token} mountpoint with grant fileid 1135 if ls != nil && id != nil && id.SpaceId == utils.PublicStorageSpaceID && id.OpaqueId == ls.Token { 1136 id = ls.ResourceId 1137 } 1138 1139 propstatOK := PropstatXML{ 1140 Status: "HTTP/1.1 200 OK", 1141 Prop: []prop.PropertyXML{}, 1142 } 1143 propstatNotFound := PropstatXML{ 1144 Status: "HTTP/1.1 404 Not Found", 1145 Prop: []prop.PropertyXML{}, 1146 } 1147 1148 appendToOK := func(p ...prop.PropertyXML) { 1149 propstatOK.Prop = append(propstatOK.Prop, p...) 1150 } 1151 appendToNotFound := func(p ...prop.PropertyXML) { 1152 propstatNotFound.Prop = append(propstatNotFound.Prop, p...) 1153 } 1154 if returnMinimal { 1155 appendToNotFound = func(p ...prop.PropertyXML) {} 1156 } 1157 1158 appendMetadataProp := func(metadata map[string]string, tagNamespace string, name string, metadataPrefix string, keys []string) { 1159 content := strings.Builder{} 1160 for _, key := range keys { 1161 kebabCaseKey := strcase.ToKebab(key) 1162 if v, ok := metadata[fmt.Sprintf("%s.%s", metadataPrefix, key)]; ok { 1163 content.WriteString("<") 1164 content.WriteString(tagNamespace) 1165 content.WriteString(":") 1166 content.WriteString(kebabCaseKey) 1167 content.WriteString(">") 1168 content.Write(prop.Escaped("", v).InnerXML) 1169 content.WriteString("</") 1170 content.WriteString(tagNamespace) 1171 content.WriteString(":") 1172 content.WriteString(kebabCaseKey) 1173 content.WriteString(">") 1174 } 1175 } 1176 1177 propName := fmt.Sprintf("%s:%s", tagNamespace, name) 1178 if content.Len() > 0 { 1179 appendToOK(prop.Raw(propName, content.String())) 1180 } else { 1181 appendToNotFound(prop.NotFound(propName)) 1182 } 1183 } 1184 1185 // when allprops has been requested 1186 if pf.Allprop != nil { 1187 // return all known properties 1188 1189 if id != nil { 1190 sid := storagespace.FormatResourceID(id) 1191 appendToOK( 1192 prop.Escaped("oc:id", sid), 1193 prop.Escaped("oc:fileid", sid), 1194 prop.Escaped("oc:spaceid", storagespace.FormatStorageID(id.StorageId, id.SpaceId)), 1195 ) 1196 } 1197 1198 if md.ParentId != nil { 1199 appendToOK(prop.Escaped("oc:file-parent", storagespace.FormatResourceID(md.ParentId))) 1200 } else { 1201 appendToNotFound(prop.NotFound("oc:file-parent")) 1202 } 1203 1204 // we need to add the shareid if possible - the only way to extract it here is to parse it from the path 1205 if ref, err := storagespace.ParseReference(strings.TrimPrefix(p, "/")); err == nil && ref.GetResourceId().GetSpaceId() == utils.ShareStorageSpaceID { 1206 appendToOK(prop.Raw("oc:shareid", ref.GetResourceId().GetOpaqueId())) 1207 } 1208 1209 if md.Name != "" { 1210 appendToOK(prop.Escaped("oc:name", md.Name)) 1211 appendToOK(prop.Escaped("d:displayname", md.Name)) 1212 } 1213 1214 if md.Etag != "" { 1215 // etags must be enclosed in double quotes and cannot contain them. 1216 // See https://tools.ietf.org/html/rfc7232#section-2.3 for details 1217 // TODO(jfd) handle weak tags that start with 'W/' 1218 appendToOK(prop.Escaped("d:getetag", quoteEtag(md.Etag))) 1219 } 1220 1221 if md.PermissionSet != nil { 1222 appendToOK(prop.Escaped("oc:permissions", wdp)) 1223 } 1224 1225 // always return size, well nearly always ... public link shares are a little weird 1226 if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 1227 appendToOK(prop.Raw("d:resourcetype", "<d:collection/>")) 1228 if ls == nil { 1229 appendToOK(prop.Escaped("oc:size", size)) 1230 } 1231 // A <DAV:allprop> PROPFIND request SHOULD NOT return DAV:quota-available-bytes and DAV:quota-used-bytes 1232 // from https://www.rfc-editor.org/rfc/rfc4331.html#section-2 1233 // appendToOK(prop.NewProp("d:quota-used-bytes", size)) 1234 // appendToOK(prop.NewProp("d:quota-available-bytes", quota)) 1235 } else { 1236 appendToOK( 1237 prop.Escaped("d:resourcetype", ""), 1238 prop.Escaped("d:getcontentlength", size), 1239 ) 1240 if md.MimeType != "" { 1241 appendToOK(prop.Escaped("d:getcontenttype", md.MimeType)) 1242 } 1243 } 1244 // Finder needs the getLastModified property to work. 1245 if md.Mtime != nil { 1246 t := utils.TSToTime(md.Mtime).UTC() 1247 lastModifiedString := t.Format(net.RFC1123) 1248 appendToOK(prop.Escaped("d:getlastmodified", lastModifiedString)) 1249 } 1250 1251 // stay bug compatible with oc10, see https://github.com/owncloud/core/pull/38304#issuecomment-762185241 1252 var checksums strings.Builder 1253 if md.Checksum != nil { 1254 checksums.WriteString("<oc:checksum>") 1255 checksums.WriteString(strings.ToUpper(string(storageprovider.GRPC2PKGXS(md.Checksum.Type)))) 1256 checksums.WriteString(":") 1257 checksums.WriteString(md.Checksum.Sum) 1258 } 1259 if md.Opaque != nil { 1260 if e, ok := md.Opaque.Map["md5"]; ok { 1261 if checksums.Len() == 0 { 1262 checksums.WriteString("<oc:checksum>MD5:") 1263 } else { 1264 checksums.WriteString(" MD5:") 1265 } 1266 checksums.Write(e.Value) 1267 } 1268 if e, ok := md.Opaque.Map["adler32"]; ok { 1269 if checksums.Len() == 0 { 1270 checksums.WriteString("<oc:checksum>ADLER32:") 1271 } else { 1272 checksums.WriteString(" ADLER32:") 1273 } 1274 checksums.Write(e.Value) 1275 } 1276 } 1277 if checksums.Len() > 0 { 1278 checksums.WriteString("</oc:checksum>") 1279 appendToOK(prop.Raw("oc:checksums", checksums.String())) 1280 } 1281 1282 if k := md.GetArbitraryMetadata().GetMetadata(); k != nil { 1283 propstatOK.Prop = append(propstatOK.Prop, prop.Raw("oc:tags", k["tags"])) 1284 appendMetadataProp(k, "oc", "audio", "libre.graph.audio", audioKeys) 1285 appendMetadataProp(k, "oc", "location", "libre.graph.location", locationKeys) 1286 appendMetadataProp(k, "oc", "image", "libre.graph.image", imageKeys) 1287 appendMetadataProp(k, "oc", "photo", "libre.graph.photo", photoKeys) 1288 } 1289 1290 // ls do not report any properties as missing by default 1291 if ls == nil { 1292 // favorites from arbitrary metadata 1293 if k := md.GetArbitraryMetadata(); k == nil { 1294 appendToOK(prop.Raw("oc:favorite", "0")) 1295 } else if amd := k.GetMetadata(); amd == nil { 1296 appendToOK(prop.Raw("oc:favorite", "0")) 1297 } else if v, ok := amd[net.PropOcFavorite]; ok && v != "" { 1298 appendToOK(prop.Escaped("oc:favorite", v)) 1299 } else { 1300 appendToOK(prop.Raw("oc:favorite", "0")) 1301 } 1302 } 1303 1304 if lock != nil { 1305 appendToOK(prop.Raw("d:lockdiscovery", activeLocks(&sublog, lock))) 1306 } 1307 // TODO return other properties ... but how do we put them in a namespace? 1308 } else { 1309 // otherwise return only the requested properties 1310 for i := range pf.Prop { 1311 switch pf.Prop[i].Space { 1312 case net.NsOwncloud: 1313 switch pf.Prop[i].Local { 1314 // TODO(jfd): maybe phoenix and the other clients can just use this id as an opaque string? 1315 // I tested the desktop client and phoenix to annotate which properties are requestted, see below cases 1316 case "fileid": // phoenix only 1317 if id != nil { 1318 appendToOK(prop.Escaped("oc:fileid", storagespace.FormatResourceID(id))) 1319 } else { 1320 appendToNotFound(prop.NotFound("oc:fileid")) 1321 } 1322 case "id": // desktop client only 1323 if id != nil { 1324 appendToOK(prop.Escaped("oc:id", storagespace.FormatResourceID(id))) 1325 } else { 1326 appendToNotFound(prop.NotFound("oc:id")) 1327 } 1328 case "file-parent": 1329 if md.ParentId != nil { 1330 appendToOK(prop.Escaped("oc:file-parent", storagespace.FormatResourceID(md.ParentId))) 1331 } else { 1332 appendToNotFound(prop.NotFound("oc:file-parent")) 1333 } 1334 case "spaceid": 1335 if id != nil { 1336 appendToOK(prop.Escaped("oc:spaceid", storagespace.FormatStorageID(id.StorageId, id.SpaceId))) 1337 } else { 1338 appendToNotFound(prop.Escaped("oc:spaceid", "")) 1339 } 1340 case "permissions": // both 1341 // oc:permissions take several char flags to indicate the permissions the user has on this node: 1342 // D = delete 1343 // NV = update (renameable moveable) 1344 // W = update (files only) 1345 // CK = create (folders only) 1346 // S = Shared 1347 // R = Shareable (Reshare) 1348 // M = Mounted 1349 // in contrast, the ocs:share-permissions further down below indicate clients the maximum permissions that can be granted 1350 appendToOK(prop.Escaped("oc:permissions", wdp)) 1351 case "public-link-permission": // only on a share root node 1352 if ls != nil && md.PermissionSet != nil { 1353 appendToOK(prop.Escaped("oc:public-link-permission", role.OCSPermissions().String())) 1354 } else { 1355 appendToNotFound(prop.NotFound("oc:public-link-permission")) 1356 } 1357 case "public-link-item-type": // only on a share root node 1358 if ls != nil { 1359 if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 1360 appendToOK(prop.Raw("oc:public-link-item-type", "folder")) 1361 } else { 1362 appendToOK(prop.Raw("oc:public-link-item-type", "file")) 1363 // redirectref is another option 1364 } 1365 } else { 1366 appendToNotFound(prop.NotFound("oc:public-link-item-type")) 1367 } 1368 case "public-link-share-datetime": 1369 if ls != nil && ls.Mtime != nil { 1370 t := utils.TSToTime(ls.Mtime).UTC() // TODO or ctime? 1371 shareTimeString := t.Format(net.RFC1123) 1372 appendToOK(prop.Escaped("oc:public-link-share-datetime", shareTimeString)) 1373 } else { 1374 appendToNotFound(prop.NotFound("oc:public-link-share-datetime")) 1375 } 1376 case "public-link-share-owner": 1377 if ls != nil && ls.Owner != nil { 1378 if net.IsCurrentUserOwnerOrManager(ctx, ls.Owner, nil) { 1379 u := ctxpkg.ContextMustGetUser(ctx) 1380 appendToOK(prop.Escaped("oc:public-link-share-owner", u.Username)) 1381 } else { 1382 u, _ := ctxpkg.ContextGetUser(ctx) 1383 sublog.Error().Interface("share", ls).Interface("user", u).Msg("the current user in the context should be the owner of a public link share") 1384 appendToNotFound(prop.NotFound("oc:public-link-share-owner")) 1385 } 1386 } else { 1387 appendToNotFound(prop.NotFound("oc:public-link-share-owner")) 1388 } 1389 case "public-link-expiration": 1390 if ls != nil && ls.Expiration != nil { 1391 t := utils.TSToTime(ls.Expiration).UTC() 1392 expireTimeString := t.Format(net.RFC1123) 1393 appendToOK(prop.Escaped("oc:public-link-expiration", expireTimeString)) 1394 } else { 1395 appendToNotFound(prop.NotFound("oc:public-link-expiration")) 1396 } 1397 case "size": // phoenix only 1398 // TODO we cannot find out if md.Size is set or not because ints in go default to 0 1399 // TODO what is the difference to d:quota-used-bytes (which only exists for collections)? 1400 // oc:size is available on files and folders and behaves like d:getcontentlength or d:quota-used-bytes respectively 1401 // The hasPrefix is a workaround to make children of the link root show a size if they have 0 bytes 1402 if ls == nil || strings.HasPrefix(p, "/"+ls.Token+"/") { 1403 appendToOK(prop.Escaped("oc:size", size)) 1404 } else { 1405 // link share root collection has no size 1406 appendToNotFound(prop.NotFound("oc:size")) 1407 } 1408 case "owner-id": // phoenix only 1409 if md.Owner != nil { 1410 if net.IsCurrentUserOwnerOrManager(ctx, md.Owner, md) { 1411 u := ctxpkg.ContextMustGetUser(ctx) 1412 appendToOK(prop.Escaped("oc:owner-id", u.Username)) 1413 } else { 1414 sublog.Debug().Msg("TODO fetch user username") 1415 appendToNotFound(prop.NotFound("oc:owner-id")) 1416 } 1417 } else { 1418 appendToNotFound(prop.NotFound("oc:owner-id")) 1419 } 1420 case "favorite": // phoenix only 1421 // TODO: can be 0 or 1?, in oc10 it is present or not 1422 // TODO: read favorite via separate call? that would be expensive? I hope it is in the md 1423 // TODO: this boolean favorite property is so horribly wrong ... either it is presont, or it is not ... unless ... it is possible to have a non binary value ... we need to double check 1424 if ls == nil { 1425 if k := md.GetArbitraryMetadata(); k == nil { 1426 appendToOK(prop.Raw("oc:favorite", "0")) 1427 } else if amd := k.GetMetadata(); amd == nil { 1428 appendToOK(prop.Raw("oc:favorite", "0")) 1429 } else if v, ok := amd[net.PropOcFavorite]; ok && v != "" { 1430 appendToOK(prop.Raw("oc:favorite", "1")) 1431 } else { 1432 appendToOK(prop.Raw("oc:favorite", "0")) 1433 } 1434 } else { 1435 // link share root collection has no favorite 1436 appendToNotFound(prop.NotFound("oc:favorite")) 1437 } 1438 case "checksums": // desktop ... not really ... the desktop sends the OC-Checksum header 1439 1440 // stay bug compatible with oc10, see https://github.com/owncloud/core/pull/38304#issuecomment-762185241 1441 var checksums strings.Builder 1442 if md.Checksum != nil { 1443 checksums.WriteString("<oc:checksum>") 1444 checksums.WriteString(strings.ToUpper(string(storageprovider.GRPC2PKGXS(md.Checksum.Type)))) 1445 checksums.WriteString(":") 1446 checksums.WriteString(md.Checksum.Sum) 1447 } 1448 if md.Opaque != nil { 1449 if e, ok := md.Opaque.Map["md5"]; ok { 1450 if checksums.Len() == 0 { 1451 checksums.WriteString("<oc:checksum>MD5:") 1452 } else { 1453 checksums.WriteString(" MD5:") 1454 } 1455 checksums.Write(e.Value) 1456 } 1457 if e, ok := md.Opaque.Map["adler32"]; ok { 1458 if checksums.Len() == 0 { 1459 checksums.WriteString("<oc:checksum>ADLER32:") 1460 } else { 1461 checksums.WriteString(" ADLER32:") 1462 } 1463 checksums.Write(e.Value) 1464 } 1465 } 1466 if checksums.Len() > 13 { 1467 checksums.WriteString("</oc:checksum>") 1468 appendToOK(prop.Raw("oc:checksums", checksums.String())) 1469 } else { 1470 appendToNotFound(prop.NotFound("oc:checksums")) 1471 } 1472 case "share-types": // used to render share indicators to share owners 1473 var types strings.Builder 1474 1475 sts := strings.Split(shareTypes, ",") 1476 for _, shareType := range sts { 1477 switch shareType { 1478 case "1": // provider.GranteeType_GRANTEE_TYPE_USER 1479 types.WriteString("<oc:share-type>" + strconv.Itoa(int(conversions.ShareTypeUser)) + "</oc:share-type>") 1480 case "2": // provider.GranteeType_GRANTEE_TYPE_GROUP 1481 types.WriteString("<oc:share-type>" + strconv.Itoa(int(conversions.ShareTypeGroup)) + "</oc:share-type>") 1482 default: 1483 sublog.Debug().Interface("shareType", shareType).Msg("unknown share type, ignoring") 1484 } 1485 } 1486 1487 if id != nil { 1488 if _, ok := linkshares[id.OpaqueId]; ok { 1489 types.WriteString("<oc:share-type>3</oc:share-type>") 1490 } 1491 } 1492 1493 if types.Len() != 0 { 1494 appendToOK(prop.Raw("oc:share-types", types.String())) 1495 } else { 1496 appendToNotFound(prop.NotFound("oc:" + pf.Prop[i].Local)) 1497 } 1498 case "owner-display-name": // phoenix only 1499 if md.Owner != nil { 1500 if net.IsCurrentUserOwnerOrManager(ctx, md.Owner, md) { 1501 u := ctxpkg.ContextMustGetUser(ctx) 1502 appendToOK(prop.Escaped("oc:owner-display-name", u.DisplayName)) 1503 } else { 1504 sublog.Debug().Msg("TODO fetch user displayname") 1505 appendToNotFound(prop.NotFound("oc:owner-display-name")) 1506 } 1507 } else { 1508 appendToNotFound(prop.NotFound("oc:owner-display-name")) 1509 } 1510 case "downloadURL": // desktop 1511 if isPublic && md.Type == provider.ResourceType_RESOURCE_TYPE_FILE { 1512 var path string 1513 if !ls.PasswordProtected { 1514 path = p 1515 } else { 1516 expiration := time.Unix(int64(ls.Signature.SignatureExpiration.Seconds), int64(ls.Signature.SignatureExpiration.Nanos)) 1517 var sb strings.Builder 1518 1519 sb.WriteString(p) 1520 sb.WriteString("?signature=") 1521 sb.WriteString(ls.Signature.Signature) 1522 sb.WriteString("&expiration=") 1523 sb.WriteString(url.QueryEscape(expiration.Format(time.RFC3339))) 1524 1525 path = sb.String() 1526 } 1527 appendToOK(prop.Escaped("oc:downloadURL", publicURL+baseURI+path)) 1528 } else { 1529 appendToNotFound(prop.NotFound("oc:" + pf.Prop[i].Local)) 1530 } 1531 case "privatelink": 1532 privateURL, err := url.Parse(publicURL) 1533 if err == nil && id != nil { 1534 privateURL.Path = path.Join(privateURL.Path, "f", storagespace.FormatResourceID(id)) 1535 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:privatelink", privateURL.String())) 1536 } else { 1537 propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("oc:privatelink")) 1538 } 1539 case "signature-auth": 1540 if isPublic { 1541 // We only want to add the attribute to the root of the propfind. 1542 if strings.HasSuffix(p, ls.Token) && ls.Signature != nil { 1543 expiration := time.Unix(int64(ls.Signature.SignatureExpiration.Seconds), int64(ls.Signature.SignatureExpiration.Nanos)) 1544 var sb strings.Builder 1545 sb.WriteString("<oc:signature>") 1546 sb.WriteString(ls.Signature.Signature) 1547 sb.WriteString("</oc:signature>") 1548 sb.WriteString("<oc:expiration>") 1549 sb.WriteString(expiration.Format(time.RFC3339)) 1550 sb.WriteString("</oc:expiration>") 1551 1552 appendToOK(prop.Raw("oc:signature-auth", sb.String())) 1553 } else { 1554 appendToNotFound(prop.NotFound("oc:signature-auth")) 1555 } 1556 } 1557 case "tags": 1558 if k := md.GetArbitraryMetadata().GetMetadata(); k != nil { 1559 propstatOK.Prop = append(propstatOK.Prop, prop.Raw("oc:tags", k["tags"])) 1560 } 1561 case "audio": 1562 if k := md.GetArbitraryMetadata().GetMetadata(); k != nil { 1563 appendMetadataProp(k, "oc", "audio", "libre.graph.audio", audioKeys) 1564 } 1565 case "location": 1566 if k := md.GetArbitraryMetadata().GetMetadata(); k != nil { 1567 appendMetadataProp(k, "oc", "location", "libre.graph.location", locationKeys) 1568 } 1569 case "image": 1570 if k := md.GetArbitraryMetadata().GetMetadata(); k != nil { 1571 appendMetadataProp(k, "oc", "image", "libre.graph.image", imageKeys) 1572 } 1573 case "photo": 1574 if k := md.GetArbitraryMetadata().GetMetadata(); k != nil { 1575 appendMetadataProp(k, "oc", "photo", "libre.graph.photo", photoKeys) 1576 } 1577 case "name": 1578 appendToOK(prop.Escaped("oc:name", md.Name)) 1579 case "shareid": 1580 if ref, err := storagespace.ParseReference(strings.TrimPrefix(p, "/")); err == nil && ref.GetResourceId().GetSpaceId() == utils.ShareStorageSpaceID { 1581 appendToOK(prop.Raw("oc:shareid", ref.GetResourceId().GetOpaqueId())) 1582 } 1583 case "dDC": // desktop 1584 fallthrough 1585 case "data-fingerprint": // desktop 1586 // used by admins to indicate a backup has been restored, 1587 // can only occur on the root node 1588 // server implementation in https://github.com/owncloud/core/pull/24054 1589 // see https://doc.owncloud.com/server/admin_manual/configuration/server/occ_command.html#maintenance-commands 1590 // TODO(jfd): double check the client behavior with reva on backup restore 1591 fallthrough 1592 default: 1593 appendToNotFound(prop.NotFound("oc:" + pf.Prop[i].Local)) 1594 } 1595 case net.NsDav: 1596 switch pf.Prop[i].Local { 1597 case "getetag": // both 1598 if md.Etag != "" { 1599 appendToOK(prop.Escaped("d:getetag", quoteEtag(md.Etag))) 1600 } else { 1601 appendToNotFound(prop.NotFound("d:getetag")) 1602 } 1603 case "getcontentlength": // both 1604 // see everts stance on this https://stackoverflow.com/a/31621912, he points to http://tools.ietf.org/html/rfc4918#section-15.3 1605 // > Purpose: Contains the Content-Length header returned by a GET without accept headers. 1606 // which only would make sense when eg. rendering a plain HTML filelisting when GETing a collection, 1607 // which is not the case ... so we don't return it on collections. owncloud has oc:size for that 1608 // TODO we cannot find out if md.Size is set or not because ints in go default to 0 1609 if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 1610 appendToNotFound(prop.NotFound("d:getcontentlength")) 1611 } else { 1612 appendToOK(prop.Escaped("d:getcontentlength", size)) 1613 } 1614 case "resourcetype": // both 1615 if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 1616 appendToOK(prop.Raw("d:resourcetype", "<d:collection/>")) 1617 } else { 1618 appendToOK(prop.Raw("d:resourcetype", "")) 1619 // redirectref is another option 1620 } 1621 case "getcontenttype": // phoenix 1622 if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 1623 // directories have no contenttype 1624 appendToNotFound(prop.NotFound("d:getcontenttype")) 1625 } else if md.MimeType != "" { 1626 appendToOK(prop.Escaped("d:getcontenttype", md.MimeType)) 1627 } 1628 case "getlastmodified": // both 1629 // TODO we cannot find out if md.Mtime is set or not because ints in go default to 0 1630 if md.Mtime != nil { 1631 t := utils.TSToTime(md.Mtime).UTC() 1632 lastModifiedString := t.Format(net.RFC1123) 1633 appendToOK(prop.Escaped("d:getlastmodified", lastModifiedString)) 1634 } else { 1635 appendToNotFound(prop.NotFound("d:getlastmodified")) 1636 } 1637 case "quota-used-bytes": // RFC 4331 1638 if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 1639 // always returns the current usage, 1640 // in oc10 there seems to be a bug that makes the size in webdav differ from the one in the user properties, not taking shares into account 1641 // in ocis we plan to always mak the quota a property of the storage space 1642 appendToOK(prop.Escaped("d:quota-used-bytes", size)) 1643 } else { 1644 appendToNotFound(prop.NotFound("d:quota-used-bytes")) 1645 } 1646 case "quota-available-bytes": // RFC 4331 1647 if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 1648 // oc10 returns -3 for unlimited, -2 for unknown, -1 for uncalculated 1649 appendToOK(prop.Escaped("d:quota-available-bytes", quota)) 1650 } else { 1651 appendToNotFound(prop.NotFound("d:quota-available-bytes")) 1652 } 1653 case "lockdiscovery": // http://www.webdav.org/specs/rfc2518.html#PROPERTY_lockdiscovery 1654 if lock == nil { 1655 appendToNotFound(prop.NotFound("d:lockdiscovery")) 1656 } else { 1657 appendToOK(prop.Raw("d:lockdiscovery", activeLocks(&sublog, lock))) 1658 } 1659 default: 1660 appendToNotFound(prop.NotFound("d:" + pf.Prop[i].Local)) 1661 } 1662 case net.NsOCS: 1663 switch pf.Prop[i].Local { 1664 // ocs:share-permissions indicate clients the maximum permissions that can be granted: 1665 // 1 = read 1666 // 2 = write (update) 1667 // 4 = create 1668 // 8 = delete 1669 // 16 = share 1670 // shared files can never have the create or delete permission bit set 1671 case "share-permissions": 1672 if md.PermissionSet != nil { 1673 perms := role.OCSPermissions() 1674 // shared files cant have the create or delete permission set 1675 if md.Type == provider.ResourceType_RESOURCE_TYPE_FILE { 1676 perms &^= conversions.PermissionCreate 1677 perms &^= conversions.PermissionDelete 1678 } 1679 appendToOK(prop.EscapedNS(pf.Prop[i].Space, pf.Prop[i].Local, perms.String())) 1680 } 1681 default: 1682 appendToNotFound(prop.NotFound("d:" + pf.Prop[i].Local)) 1683 } 1684 default: 1685 // handle custom properties 1686 if k := md.GetArbitraryMetadata(); k == nil { 1687 appendToNotFound(prop.NotFoundNS(pf.Prop[i].Space, pf.Prop[i].Local)) 1688 } else if amd := k.GetMetadata(); amd == nil { 1689 appendToNotFound(prop.NotFoundNS(pf.Prop[i].Space, pf.Prop[i].Local)) 1690 } else if v, ok := amd[metadataKeyOf(&pf.Prop[i])]; ok && v != "" { 1691 appendToOK(prop.EscapedNS(pf.Prop[i].Space, pf.Prop[i].Local, v)) 1692 } else { 1693 appendToNotFound(prop.NotFoundNS(pf.Prop[i].Space, pf.Prop[i].Local)) 1694 } 1695 } 1696 } 1697 } 1698 1699 if status := utils.ReadPlainFromOpaque(md.Opaque, "status"); status == "processing" { 1700 response.Propstat = append(response.Propstat, PropstatXML{ 1701 Status: "HTTP/1.1 425 TOO EARLY", 1702 Prop: propstatOK.Prop, 1703 }) 1704 return &response, nil 1705 } 1706 1707 if len(propstatOK.Prop) > 0 { 1708 response.Propstat = append(response.Propstat, propstatOK) 1709 } 1710 if len(propstatNotFound.Prop) > 0 { 1711 response.Propstat = append(response.Propstat, propstatNotFound) 1712 } 1713 1714 return &response, nil 1715 } 1716 1717 func activeLocks(log *zerolog.Logger, lock *provider.Lock) string { 1718 if lock == nil || lock.Type == provider.LockType_LOCK_TYPE_INVALID { 1719 return "" 1720 } 1721 expiration := "Infinity" 1722 if lock.Expiration != nil { 1723 now := uint64(time.Now().Unix()) 1724 // Should we hide expired locks here? No. 1725 // 1726 // If the timeout expires, then the lock SHOULD be removed. In this 1727 // case the server SHOULD act as if an UNLOCK method was executed by the 1728 // server on the resource using the lock token of the timed-out lock, 1729 // performed with its override authority. 1730 // 1731 // see https://datatracker.ietf.org/doc/html/rfc4918#section-6.6 1732 if lock.Expiration.Seconds >= now { 1733 expiration = "Second-" + strconv.FormatUint(lock.Expiration.Seconds-now, 10) 1734 } else { 1735 expiration = "Second-0" 1736 } 1737 } 1738 1739 // xml.Encode cannot render emptytags like <d:write/>, see https://github.com/golang/go/issues/21399 1740 var activelocks strings.Builder 1741 activelocks.WriteString("<d:activelock>") 1742 // webdav locktype write | transaction 1743 switch lock.Type { 1744 case provider.LockType_LOCK_TYPE_EXCL: 1745 fallthrough 1746 case provider.LockType_LOCK_TYPE_WRITE: 1747 activelocks.WriteString("<d:locktype><d:write/></d:locktype>") 1748 } 1749 // webdav lockscope exclusive, shared, or local 1750 switch lock.Type { 1751 case provider.LockType_LOCK_TYPE_EXCL: 1752 fallthrough 1753 case provider.LockType_LOCK_TYPE_WRITE: 1754 activelocks.WriteString("<d:lockscope><d:exclusive/></d:lockscope>") 1755 case provider.LockType_LOCK_TYPE_SHARED: 1756 activelocks.WriteString("<d:lockscope><d:shared/></d:lockscope>") 1757 } 1758 // we currently only support depth infinity 1759 activelocks.WriteString("<d:depth>Infinity</d:depth>") 1760 1761 if lock.User != nil || lock.AppName != "" { 1762 activelocks.WriteString("<d:owner>") 1763 1764 if lock.User != nil { 1765 // TODO oc10 uses displayname and email, needs a user lookup 1766 activelocks.WriteString(prop.Escape(lock.User.OpaqueId + "@" + lock.User.Idp)) 1767 } 1768 if lock.AppName != "" { 1769 if lock.User != nil { 1770 activelocks.WriteString(" via ") 1771 } 1772 activelocks.WriteString(prop.Escape(lock.AppName)) 1773 } 1774 activelocks.WriteString("</d:owner>") 1775 } 1776 1777 if un := utils.ReadPlainFromOpaque(lock.Opaque, "lockownername"); un != "" { 1778 activelocks.WriteString("<oc:ownername>") 1779 activelocks.WriteString(un) 1780 activelocks.WriteString("</oc:ownername>") 1781 } 1782 if lt := utils.ReadPlainFromOpaque(lock.Opaque, "locktime"); lt != "" { 1783 activelocks.WriteString("<oc:locktime>") 1784 activelocks.WriteString(lt) 1785 activelocks.WriteString("</oc:locktime>") 1786 } 1787 activelocks.WriteString("<d:timeout>") 1788 activelocks.WriteString(expiration) 1789 activelocks.WriteString("</d:timeout>") 1790 if lock.LockId != "" { 1791 activelocks.WriteString("<d:locktoken><d:href>") 1792 activelocks.WriteString(prop.Escape(lock.LockId)) 1793 activelocks.WriteString("</d:href></d:locktoken>") 1794 } 1795 // lockroot is only used when setting the lock 1796 activelocks.WriteString("</d:activelock>") 1797 return activelocks.String() 1798 } 1799 1800 // be defensive about wrong encoded etags 1801 func quoteEtag(etag string) string { 1802 if strings.HasPrefix(etag, "W/") { 1803 return `W/"` + strings.Trim(etag[2:], `"`) + `"` 1804 } 1805 return `"` + strings.Trim(etag, `"`) + `"` 1806 } 1807 1808 func (c *countingReader) Read(p []byte) (int, error) { 1809 n, err := c.r.Read(p) 1810 c.n += n 1811 return n, err 1812 } 1813 1814 func metadataKeyOf(n *xml.Name) string { 1815 switch n.Local { 1816 case "quota-available-bytes": 1817 return "quota" 1818 case "share-types", "tags", "lockdiscovery": 1819 return n.Local 1820 default: 1821 return fmt.Sprintf("%s/%s", n.Space, n.Local) 1822 } 1823 } 1824 1825 // UnmarshalXML appends the property names enclosed within start to pn. 1826 // 1827 // It returns an error if start does not contain any properties or if 1828 // properties contain values. Character data between properties is ignored. 1829 func (pn *Props) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 1830 for { 1831 t, err := prop.Next(d) 1832 if err != nil { 1833 return err 1834 } 1835 switch e := t.(type) { 1836 case xml.EndElement: 1837 // jfd: I think <d:prop></d:prop> is perfectly valid ... treat it as allprop 1838 /* 1839 if len(*pn) == 0 { 1840 return fmt.Errorf("%s must not be empty", start.Name.Local) 1841 } 1842 */ 1843 return nil 1844 case xml.StartElement: 1845 t, err = prop.Next(d) 1846 if err != nil { 1847 return err 1848 } 1849 if _, ok := t.(xml.EndElement); !ok { 1850 return fmt.Errorf("unexpected token %T", t) 1851 } 1852 *pn = append(*pn, e.Name) 1853 } 1854 } 1855 }