Commit 169f4226 authored by Robert Stepanek's avatar Robert Stepanek Committed by Nigel Tao

webdav: Add XML and HTTP handler support for PROPPATCH.

This change adds support to parse and handle PROPPATCH requests. It adds the
Patch method to the PropSystem interface. In order to keep this CL small,
it does not add support to set "dead" DAV properties. Instead, any patched
property is reported with HTTP status code 403 Forbidden. Once this CL is
accepted, I will submit a CL to store dead DAV properties in-memory. The
litmus test coverage of the 'props' test suite remains as-is:

16 tests were skipped, 14 tests run. 10 passed, 4 failed. 71.4%

Change-Id: I14a25464e94b3316c16976f79b4457bf646d0d24
Reviewed-on: https://go-review.googlesource.com/8937Reviewed-by: 's avatarNigel Tao <nigeltao@golang.org>
parent 84ba27dd
......@@ -27,7 +27,6 @@ type PropSystem interface {
// only be part of one Propstat element.
Find(name string, propnames []xml.Name) ([]Propstat, error)
// TODO(rost) PROPPATCH.
// TODO(nigeltao) merge Find and Allprop?
// Allprop returns the properties defined for resource name and the
......@@ -44,9 +43,30 @@ type PropSystem interface {
// Propnames returns the property names defined for resource name.
Propnames(name string) ([]xml.Name, error)
// Patch patches the properties of resource name.
//
// If all patches can be applied without conflict, Patch returns a slice
// of length one and a Propstat element of status 200, naming all patched
// properties. In case of conflict, Patch returns an arbitrary long slice
// and no Propstat element must have status 200. In either case, properties
// in Propstat must not have values.
//
// Note that the WebDAV RFC requires either all patches to succeed or none.
Patch(name string, patches []Proppatch) ([]Propstat, error)
// TODO(rost) COPY/MOVE/DELETE.
}
// Proppatch describes a property update instruction as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
type Proppatch struct {
// Remove specifies whether this patch removes properties. If it does not
// remove them, it sets them.
Remove bool
// Props contains the properties to be set or removed.
Props []Property
}
// Propstat describes a XML propstat element as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
type Propstat struct {
......@@ -73,7 +93,6 @@ type Propstat struct {
// memPS implements an in-memory PropSystem. It supports all of the mandatory
// live properties of RFC 4918.
type memPS struct {
// TODO(rost) memPS will get writeable in the next CLs.
fs FileSystem
ls LockSystem
}
......@@ -196,6 +215,17 @@ func (ps *memPS) Allprop(name string, include []xml.Name) ([]Propstat, error) {
return ps.Find(name, propnames)
}
func (ps *memPS) Patch(name string, patches []Proppatch) ([]Propstat, error) {
// TODO(rost): Support to patch "dead" DAV properties in the next CL.
pstat := Propstat{Status: http.StatusForbidden}
for _, patch := range patches {
for _, p := range patch.Props {
pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
}
}
return []Propstat{pstat}, nil
}
func (ps *memPS) findResourceType(name string, fi os.FileInfo) (string, error) {
if fi.IsDir() {
return `<collection xmlns="DAV:"/>`, nil
......
......@@ -58,6 +58,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
status, err = h.handleUnlock(w, r)
case "PROPFIND":
status, err = h.handlePropfind(w, r)
case "PROPPATCH":
status, err = h.handleProppatch(w, r)
}
}
......@@ -512,6 +514,36 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
return 0, mw.close()
}
func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) {
release, status, err := h.confirmLocks(r, r.URL.Path, "")
if err != nil {
return status, err
}
defer release()
if _, err := h.FileSystem.Stat(r.URL.Path); err != nil {
if err == os.ErrNotExist {
return http.StatusNotFound, err
}
return http.StatusMethodNotAllowed, err
}
patches, status, err := readProppatch(r.Body)
if err != nil {
return status, err
}
pstats, err := h.PropSystem.Patch(r.URL.Path, patches)
if err != nil {
return http.StatusInternalServerError, err
}
mw := multistatusWriter{w: w}
writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats))
closeErr := mw.close()
if writeErr != nil {
return http.StatusInternalServerError, writeErr
}
return 0, closeErr
}
// davHeaderNames maps the names of DAV properties to their corresponding
// HTTP response headers.
var davHeaderNames = map[xml.Name]string{
......@@ -613,6 +645,7 @@ var (
errInvalidLockInfo = errors.New("webdav: invalid lock info")
errInvalidLockToken = errors.New("webdav: invalid lock token")
errInvalidPropfind = errors.New("webdav: invalid propfind")
errInvalidProppatch = errors.New("webdav: invalid proppatch")
errInvalidResponse = errors.New("webdav: invalid response")
errInvalidTimeout = errors.New("webdav: invalid timeout")
errNoFileSystem = errors.New("webdav: no file system")
......
......@@ -115,13 +115,13 @@ func next(d *xml.Decoder) (xml.Token, error) {
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind)
type propnames []xml.Name
type propfindProps []xml.Name
// UnmarshalXML appends the property names enclosed within start to pn.
//
// It returns an error if start does not contain any properties or if
// properties contain values. Character data between properties is ignored.
func (pn *propnames) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
func (pn *propfindProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for {
t, err := next(d)
if err != nil {
......@@ -152,8 +152,8 @@ type propfind struct {
XMLName xml.Name `xml:"DAV: propfind"`
Allprop *struct{} `xml:"DAV: allprop"`
Propname *struct{} `xml:"DAV: propname"`
Prop propnames `xml:"DAV: prop"`
Include propnames `xml:"DAV: include"`
Prop propfindProps `xml:"DAV: prop"`
Include propfindProps `xml:"DAV: include"`
}
func readPropfind(r io.Reader) (pf propfind, status int, err error) {
......@@ -317,3 +317,111 @@ func (w *multistatusWriter) close() error {
}
return w.enc.Flush()
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for proppatch)
type proppatchProps []Property
var xmlLangName = xml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"}
func xmlLang(s xml.StartElement, d string) string {
for _, attr := range s.Attr {
if attr.Name == xmlLangName {
return attr.Value
}
}
return d
}
// UnmarshalXML appends the property names and values enclosed within start
// to ps.
//
// An xml:lang attribute that is defined either on the DAV:prop or property
// name XML element is propagated to the property's Lang field.
//
// UnmarshalXML returns an error if start does not contain any properties or if
// property values contain syntactically incorrect XML.
func (ps *proppatchProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
lang := xmlLang(start, "")
for {
t, err := next(d)
if err != nil {
return err
}
switch t.(type) {
case xml.EndElement:
if len(*ps) == 0 {
return fmt.Errorf("%s must not be empty", start.Name.Local)
}
return nil
case xml.StartElement:
p := Property{
XMLName: t.(xml.StartElement).Name,
Lang: xmlLang(t.(xml.StartElement), lang),
}
// The XML value of a property can be arbitrary, mixed-content XML.
// To make sure that the unmarshalled value contains all required
// namespaces, we encode all the property value XML tokens into a
// buffer. This forces the encoder to redeclare any used namespaces.
var b bytes.Buffer
e := xml.NewEncoder(&b)
for {
t, err = next(d)
if err != nil {
return err
}
if e, ok := t.(xml.EndElement); ok && e.Name == p.XMLName {
break
}
if err = e.EncodeToken(t); err != nil {
return err
}
}
err = e.Flush()
if err != nil {
return err
}
p.InnerXML = b.Bytes()
*ps = append(*ps, p)
}
}
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_set
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_remove
type setRemove struct {
XMLName xml.Name
Lang string `xml:"xml:lang,attr,omitempty"`
Prop proppatchProps `xml:"DAV: prop"`
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propertyupdate
type propertyupdate struct {
XMLName xml.Name `xml:"DAV: propertyupdate"`
Lang string `xml:"xml:lang,attr,omitempty"`
SetRemove []setRemove `xml:",any"`
}
func readProppatch(r io.Reader) (patches []Proppatch, status int, err error) {
var pu propertyupdate
if err = xml.NewDecoder(r).Decode(&pu); err != nil {
return nil, http.StatusBadRequest, err
}
for _, op := range pu.SetRemove {
remove := false
switch op.XMLName {
case xml.Name{Space: "DAV:", Local: "set"}:
// No-op.
case xml.Name{Space: "DAV:", Local: "remove"}:
for _, p := range op.Prop {
if len(p.InnerXML) > 0 {
return nil, http.StatusBadRequest, errInvalidProppatch
}
}
remove = true
default:
return nil, http.StatusBadRequest, errInvalidProppatch
}
patches = append(patches, Proppatch{Remove: remove, Props: op.Prop})
}
return patches, 0, nil
}
......@@ -173,7 +173,7 @@ func TestReadPropfind(t *testing.T) {
wantPF: propfind{
XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
Allprop: new(struct{}),
Include: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: include followed by allprop",
......@@ -185,7 +185,7 @@ func TestReadPropfind(t *testing.T) {
wantPF: propfind{
XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
Allprop: new(struct{}),
Include: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: propfind",
......@@ -195,7 +195,7 @@ func TestReadPropfind(t *testing.T) {
"</A:propfind>",
wantPF: propfind{
XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
Prop: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: prop with ignored comments",
......@@ -208,7 +208,7 @@ func TestReadPropfind(t *testing.T) {
"</A:propfind>",
wantPF: propfind{
XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
Prop: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: propfind with ignored whitespace",
......@@ -218,7 +218,7 @@ func TestReadPropfind(t *testing.T) {
"</A:propfind>",
wantPF: propfind{
XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
Prop: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: propfind with ignored mixed-content",
......@@ -228,7 +228,7 @@ func TestReadPropfind(t *testing.T) {
"</A:propfind>",
wantPF: propfind{
XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
Prop: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: propname with ignored element (section A.4)",
......@@ -616,3 +616,181 @@ loop:
}
}
}
func TestReadProppatch(t *testing.T) {
// TODO(rost): These "golden XML" tests easily break with changes in the
// xml package. A whitespace-preserving normalizer of XML content is
// required to make these tests more robust.
testCases := []struct {
desc string
input string
wantPP []Proppatch
wantStatus int
}{{
desc: "proppatch: section 9.2",
input: `` +
`<?xml version="1.0" encoding="utf-8" ?>` +
`<D:propertyupdate xmlns:D="DAV:"` +
` xmlns:Z="http://ns.example.com/z/">` +
` <D:set>` +
` <D:prop>` +
` <Z:Authors>` +
` <Z:Author>Jim Whitehead</Z:Author>` +
` <Z:Author>Roy Fielding</Z:Author>` +
` </Z:Authors>` +
` </D:prop>` +
` </D:set>` +
` <D:remove>` +
` <D:prop><Z:Copyright-Owner/></D:prop>` +
` </D:remove>` +
`</D:propertyupdate>`,
wantPP: []Proppatch{{
Props: []Property{{
xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
"",
[]byte(`` +
` ` +
`<z:Author xmlns:z="http://ns.example.com/z/">` +
`Jim Whitehead` +
`</z:Author>` +
` ` +
`<z:Author xmlns:z="http://ns.example.com/z/">` +
`Roy Fielding` +
`</z:Author>` +
` `,
),
}},
}, {
Remove: true,
Props: []Property{{
xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"},
"",
nil,
}},
}},
}, {
desc: "proppatch: section 4.3.1 (mixed content)",
input: `` +
`<?xml version="1.0" encoding="utf-8" ?>` +
`<D:propertyupdate xmlns:D="DAV:"` +
` xmlns:Z="http://ns.example.com/z/">` +
` <D:set>` +
` <D:prop xml:lang="en" xmlns:D="DAV:">` +
` <x:author xmlns:x='http://example.com/ns'>` +
` <x:name>Jane Doe</x:name>` +
` <!-- Jane's contact info -->` +
` <x:uri type='email'` +
` added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +
` <x:uri type='web'` +
` added='2005-11-27'>http://www.example.com</x:uri>` +
` <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +
` Jane has been working way <h:em>too</h:em> long on the` +
` long-awaited revision of <![CDATA[<RFC2518>]]>.` +
` </x:notes>` +
` </x:author>` +
` </D:prop>` +
` </D:set>` +
`</D:propertyupdate>`,
wantPP: []Proppatch{{
Props: []Property{{
xml.Name{Space: "http://example.com/ns", Local: "author"},
"en",
[]byte(`` +
` ` +
`<ns:name xmlns:ns="http://example.com/ns">Jane Doe</ns:name>` +
` ` +
`<ns:uri xmlns:ns="http://example.com/ns" type="email" added="2005-11-26">` +
`mailto:jane.doe@example.com` +
`</ns:uri>` +
` ` +
`<ns:uri xmlns:ns="http://example.com/ns" type="web" added="2005-11-27">` +
`http://www.example.com` +
`</ns:uri>` +
` ` +
`<ns:notes xmlns:ns="http://example.com/ns"` +
` xmlns:h="http://www.w3.org/1999/xhtml">` +
` ` +
` Jane has been working way` +
` <h:em>too</h:em>` +
` long on the` + ` ` +
` long-awaited revision of &lt;RFC2518&gt;.` +
` ` +
`</ns:notes>` +
` `,
),
}},
}},
}, {
desc: "proppatch: lang attribute on prop",
input: `` +
`<?xml version="1.0" encoding="utf-8" ?>` +
`<D:propertyupdate xmlns:D="DAV:">` +
` <D:set>` +
` <D:prop xml:lang="en">` +
` <foo xmlns="http://example.com/ns"/>` +
` </D:prop>` +
` </D:set>` +
`</D:propertyupdate>`,
wantPP: []Proppatch{{
Props: []Property{{
xml.Name{Space: "http://example.com/ns", Local: "foo"},
"en",
nil,
}},
}},
}, {
desc: "bad: remove with value",
input: `` +
`<?xml version="1.0" encoding="utf-8" ?>` +
`<D:propertyupdate xmlns:D="DAV:"` +
` xmlns:Z="http://ns.example.com/z/">` +
` <D:remove>` +
` <D:prop>` +
` <Z:Authors>` +
` <Z:Author>Jim Whitehead</Z:Author>` +
` </Z:Authors>` +
` </D:prop>` +
` </D:remove>` +
`</D:propertyupdate>`,
wantStatus: http.StatusBadRequest,
}, {
desc: "bad: empty propertyupdate",
input: `` +
`<?xml version="1.0" encoding="utf-8" ?>` +
`<D:propertyupdate xmlns:D="DAV:"` +
`</D:propertyupdate>`,
wantStatus: http.StatusBadRequest,
}, {
desc: "bad: empty prop",
input: `` +
`<?xml version="1.0" encoding="utf-8" ?>` +
`<D:propertyupdate xmlns:D="DAV:"` +
` xmlns:Z="http://ns.example.com/z/">` +
` <D:remove>` +
` <D:prop/>` +
` </D:remove>` +
`</D:propertyupdate>`,
wantStatus: http.StatusBadRequest,
}}
for _, tc := range testCases {
pp, status, err := readProppatch(strings.NewReader(tc.input))
if tc.wantStatus != 0 {
if err == nil {
t.Errorf("%s: got nil error, want non-nil", tc.desc)
continue
}
} else if err != nil {
t.Errorf("%s: %v", tc.desc, err)
continue
}
if status != tc.wantStatus {
t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus)
continue
}
if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {
t.Errorf("%s: proppatch\ngot %v\nwant %v", tc.desc, pp, tc.wantPP)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment