Skip to content

Commit

Permalink
ocdav: Adjust propstat response for removed properties (#742)
Browse files Browse the repository at this point in the history
* ocdav: Adjust propstat response for removed properties

Removed properties now appear in a propstat block with status 204.
The list of properties appearing in propstat blocks now depend on the
request and no more on the values returned by the Stat call.

* ocdav: process PROPPATCH instructions in the correct order

Webdav PROPPATCH spec requires the instructions to be processed in the
correct order.
This adjusts the logic to now only set or remove a single arbitrary
metadata at a time, in the order given in the request.
  • Loading branch information
Vincent Petry authored May 15, 2020
1 parent 2b018b1 commit 3bf55a4
Showing 1 changed file with 113 additions and 65 deletions.
178 changes: 113 additions & 65 deletions internal/http/services/owncloud/ocdav/proppatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
package ocdav

import (
"context"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"

Expand All @@ -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)
Expand All @@ -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{
Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -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 := `<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:" `
msg += `xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">`
msg += string(responsesXML) + `</d:multistatus>`
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"
Expand Down

0 comments on commit 3bf55a4

Please sign in to comment.