diff --git a/internal/http/services/owncloud/ocdav/proppatch.go b/internal/http/services/owncloud/ocdav/proppatch.go index ce2de0a34e..46cbd53959 100644 --- a/internal/http/services/owncloud/ocdav/proppatch.go +++ b/internal/http/services/owncloud/ocdav/proppatch.go @@ -19,10 +19,12 @@ package ocdav import ( + "context" "encoding/xml" "fmt" "io" "net/http" + "net/url" "path" "strings" @@ -40,6 +42,9 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) defer span.End() log := appctx.GetLogger(ctx) + acceptedProps := []xml.Name{} + removedProps := []xml.Name{} + ns = applyLayout(ctx, ns) fn := path.Join(ns, r.URL.Path) @@ -58,16 +63,34 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) return } - mkeys := []string{} + // check if resource exists + statReq := &provider.StatRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Path{Path: fn}, + }, + } + statRes, err := c.Stat(ctx, statReq) + if err != nil { + log.Error().Err(err).Msg("error sending a grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return + } - pf := &propfindXML{ - Prop: propfindProps{}, + if statRes.Status.Code != rpc.Code_CODE_OK { + if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + log.Warn().Str("path", fn).Msg("resource not found") + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) + return } + rreq := &provider.UnsetArbitraryMetadataRequest{ Ref: &provider.Reference{ Spec: &provider.Reference_Path{Path: fn}, }, - ArbitraryMetadataKeys: []string{}, + ArbitraryMetadataKeys: []string{""}, } sreq := &provider.SetArbitraryMetadataRequest{ Ref: &provider.Reference{ @@ -82,7 +105,7 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) continue } for j := range pp[i].Props { - pf.Prop = append(pf.Prop, pp[i].Props[j].XMLName) + propNameXML := pp[i].Props[j].XMLName // don't use path.Join. It removes the double slash! concatenate with a / key := fmt.Sprintf("%s/%s", pp[i].Props[j].XMLName.Space, pp[i].Props[j].XMLName.Local) value := string(pp[i].Props[j].InnerXML) @@ -95,81 +118,65 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) remove = true } } + // Webdav spec requires the operations to be executed in the order + // specified in the PROPPATCH request + // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 + // FIXME: batch this somehow if remove { - rreq.ArbitraryMetadataKeys = append(rreq.ArbitraryMetadataKeys, key) + rreq.ArbitraryMetadataKeys[0] = key + res, err := c.UnsetArbitraryMetadata(ctx, rreq) + if err != nil { + log.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if res.Status.Code != rpc.Code_CODE_OK { + if res.Status.Code == rpc.Code_CODE_NOT_FOUND { + log.Warn().Str("path", fn).Msg("resource not found") + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + removedProps = append(removedProps, propNameXML) } else { sreq.ArbitraryMetadata.Metadata[key] = value - } - mkeys = append(mkeys, key) - } - // what do we need to unset - if len(rreq.ArbitraryMetadataKeys) > 0 { - res, err := c.UnsetArbitraryMetadata(ctx, rreq) - if err != nil { - log.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if res.Status.Code != rpc.Code_CODE_OK { - if res.Status.Code == rpc.Code_CODE_NOT_FOUND { - log.Warn().Str("path", fn).Msg("resource not found") - w.WriteHeader(http.StatusNotFound) + res, err := c.SetArbitraryMetadata(ctx, sreq) + if err != nil { + log.Error().Err(err).Str("key", key).Str("value", value).Msg("error sending a grpc SetArbitraryMetadata request") + w.WriteHeader(http.StatusInternalServerError) return } - w.WriteHeader(http.StatusInternalServerError) - return - } - } - if len(sreq.ArbitraryMetadata.Metadata) > 0 { - res, err := c.SetArbitraryMetadata(ctx, sreq) - if err != nil { - log.Error().Err(err).Msg("error sending a grpc SetArbitraryMetadata request") - w.WriteHeader(http.StatusInternalServerError) - return - } - if res.Status.Code != rpc.Code_CODE_OK { - if res.Status.Code == rpc.Code_CODE_NOT_FOUND { - log.Warn().Str("path", fn).Msg("resource not found") - w.WriteHeader(http.StatusNotFound) + if res.Status.Code != rpc.Code_CODE_OK { + if res.Status.Code == rpc.Code_CODE_NOT_FOUND { + log.Warn().Str("path", fn).Msg("resource not found") + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) return } - w.WriteHeader(http.StatusInternalServerError) - return + acceptedProps = append(acceptedProps, propNameXML) + delete(sreq.ArbitraryMetadata.Metadata, key) } } + // FIXME: in case of error, need to set all properties back to the original state, + // and return the error in the matching propstat block, if applicable + // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 } - req := &provider.StatRequest{ - Ref: &provider.Reference{ - Spec: &provider.Reference_Path{Path: fn}, - }, - ArbitraryMetadataKeys: mkeys, - } - res, err := c.Stat(ctx, req) - if err != nil { - log.Error().Err(err).Msg("error sending a grpc stat request") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if res.Status.Code != rpc.Code_CODE_OK { - if res.Status.Code == rpc.Code_CODE_NOT_FOUND { - log.Warn().Str("path", fn).Msg("resource not found") - w.WriteHeader(http.StatusNotFound) - return - } - w.WriteHeader(http.StatusInternalServerError) - return + ref := strings.TrimPrefix(fn, ns) + ref = path.Join(ctx.Value(ctxKeyBaseURI).(string), ref) + if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + ref += "/" } - info := res.Info - infos := []*provider.ResourceInfo{info} - - propRes, err := s.formatPropfind(ctx, pf, infos, ns) + propRes, err := s.formatProppatchResponse(ctx, acceptedProps, removedProps, ref) if err != nil { - log.Error().Err(err).Msg("error formatting propfind") + log.Error().Err(err).Msg("error formatting proppatch response") w.WriteHeader(http.StatusInternalServerError) return } @@ -181,6 +188,47 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) } } +func (s *svc) formatProppatchResponse(ctx context.Context, acceptedProps []xml.Name, removedProps []xml.Name, ref string) (string, error) { + responses := make([]responseXML, 0, 1) + response := responseXML{ + Href: (&url.URL{Path: ref}).EscapedPath(), // url encode response.Href + Propstat: []propstatXML{}, + } + + if len(acceptedProps) > 0 { + propstatBody := []*propertyXML{} + for i := range acceptedProps { + propstatBody = append(propstatBody, s.newPropNS(acceptedProps[i].Space, acceptedProps[i].Local, "")) + } + response.Propstat = append(response.Propstat, propstatXML{ + Status: "HTTP/1.1 200 OK", + Prop: propstatBody, + }) + } + + if len(removedProps) > 0 { + propstatBody := []*propertyXML{} + for i := range removedProps { + propstatBody = append(propstatBody, s.newPropNS(removedProps[i].Space, removedProps[i].Local, "")) + } + response.Propstat = append(response.Propstat, propstatXML{ + Status: "HTTP/1.1 204 No Content", + Prop: propstatBody, + }) + } + + responses = append(responses, response) + responsesXML, err := xml.Marshal(&responses) + if err != nil { + return "", err + } + + msg := `` + msg += string(responsesXML) + `` + return msg, nil +} + func (s *svc) isBooleanProperty(prop string) bool { // TODO add other properties we know to be boolean? return prop == "http://owncloud.org/ns/favorite"