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  }