Ver código fonte

webdav: added xml unmarshaler for PROPFIND requests.

Change-Id: Ib51221be5dbf3a8a4f624ca3bff4b283f311e43b
Reviewed-on: https://go-review.googlesource.com/2757
Reviewed-by: Nigel Tao <nigeltao@golang.org>
Robert Stepanek 11 anos atrás
pai
commit
d46df1aba1
3 arquivos alterados com 305 adições e 1 exclusões
  1. 1 0
      webdav/webdav.go
  2. 90 0
      webdav/xml.go
  3. 214 1
      webdav/xml_test.go

+ 1 - 0
webdav/webdav.go

@@ -359,6 +359,7 @@ var (
 	errInvalidIfHeader     = errors.New("webdav: invalid If header")
 	errInvalidLockInfo     = errors.New("webdav: invalid lock info")
 	errInvalidLockToken    = errors.New("webdav: invalid lock token")
+	errInvalidPropfind     = errors.New("webdav: invalid propfind")
 	errNoFileSystem        = errors.New("webdav: no file system")
 	errNoLockSystem        = errors.New("webdav: no lock system")
 	errNotADirectory       = errors.New("webdav: not a directory")

+ 90 - 0
webdav/xml.go

@@ -93,3 +93,93 @@ func escape(s string) string {
 	}
 	return s
 }
+
+// Next returns the next token, if any, in the XML stream of d.
+// RFC 4918 requires to ignore comments, processing instructions
+// and directives.
+// http://www.webdav.org/specs/rfc4918.html#property_values
+// http://www.webdav.org/specs/rfc4918.html#xml-extensibility
+func next(d *xml.Decoder) (xml.Token, error) {
+	for {
+		t, err := d.Token()
+		if err != nil {
+			return t, err
+		}
+		switch t.(type) {
+		case xml.Comment, xml.Directive, xml.ProcInst:
+			continue
+		default:
+			return t, nil
+		}
+	}
+}
+
+type propnames []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 {
+	for {
+		t, err := next(d)
+		if err != nil {
+			return err
+		}
+		switch t.(type) {
+		case xml.EndElement:
+			if len(*pn) == 0 {
+				return fmt.Errorf("%s must not be empty", start.Name.Local)
+			}
+			return nil
+		case xml.StartElement:
+			name := t.(xml.StartElement).Name
+			t, err = next(d)
+			if err != nil {
+				return err
+			}
+			if _, ok := t.(xml.EndElement); !ok {
+				return fmt.Errorf("unexpected token %T", t)
+			}
+			*pn = append(*pn, name)
+		}
+	}
+}
+
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind
+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"`
+}
+
+func readPropfind(r io.Reader) (pf propfind, status int, err error) {
+	c := countingReader{r: r}
+	if err = xml.NewDecoder(&c).Decode(&pf); err != nil {
+		if err == io.EOF {
+			if c.n == 0 {
+				// An empty body means to propfind allprop.
+				// http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
+				return propfind{Allprop: new(struct{})}, 0, nil
+			}
+			err = errInvalidPropfind
+		}
+		return propfind{}, http.StatusBadRequest, err
+	}
+
+	if pf.Allprop == nil && pf.Include != nil {
+		return propfind{}, http.StatusBadRequest, errInvalidPropfind
+	}
+	if pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) {
+		return propfind{}, http.StatusBadRequest, errInvalidPropfind
+	}
+	if pf.Prop != nil && pf.Propname != nil {
+		return propfind{}, http.StatusBadRequest, errInvalidPropfind
+	}
+	if pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil {
+		return propfind{}, http.StatusBadRequest, errInvalidPropfind
+	}
+	return pf, 0, nil
+}

+ 214 - 1
webdav/xml_test.go

@@ -12,7 +12,7 @@ import (
 	"testing"
 )
 
-func TestParseLockInfo(t *testing.T) {
+func TestReadLockInfo(t *testing.T) {
 	// The "section x.y.z" test cases come from section x.y.z of the spec at
 	// http://www.webdav.org/specs/rfc4918.html
 	testCases := []struct {
@@ -127,3 +127,216 @@ func TestParseLockInfo(t *testing.T) {
 		}
 	}
 }
+
+func TestReadPropfind(t *testing.T) {
+	testCases := []struct {
+		desc       string
+		input      string
+		wantPF     propfind
+		wantStatus int
+	}{{
+		desc: "propfind: propname",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:propname/>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName:  xml.Name{"DAV:", "propfind"},
+			Propname: new(struct{}),
+		},
+	}, {
+		desc:  "propfind: empty body means allprop",
+		input: "",
+		wantPF: propfind{
+			Allprop: new(struct{}),
+		},
+	}, {
+		desc: "propfind: allprop",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"   <A:allprop/>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName: xml.Name{"DAV:", "propfind"},
+			Allprop: new(struct{}),
+		},
+	}, {
+		desc: "propfind: allprop followed by include",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:allprop/>\n" +
+			"  <A:include><A:displayname/></A:include>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName: xml.Name{"DAV:", "propfind"},
+			Allprop: new(struct{}),
+			Include: propnames{xml.Name{"DAV:", "displayname"}},
+		},
+	}, {
+		desc: "propfind: include followed by allprop",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:include><A:displayname/></A:include>\n" +
+			"  <A:allprop/>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName: xml.Name{"DAV:", "propfind"},
+			Allprop: new(struct{}),
+			Include: propnames{xml.Name{"DAV:", "displayname"}},
+		},
+	}, {
+		desc: "propfind: propfind",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop><A:displayname/></A:prop>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName: xml.Name{"DAV:", "propfind"},
+			Prop:    propnames{xml.Name{"DAV:", "displayname"}},
+		},
+	}, {
+		desc: "propfind: prop with ignored comments",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop>\n" +
+			"    <!-- ignore -->\n" +
+			"    <A:displayname><!-- ignore --></A:displayname>\n" +
+			"  </A:prop>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName: xml.Name{"DAV:", "propfind"},
+			Prop:    propnames{xml.Name{"DAV:", "displayname"}},
+		},
+	}, {
+		desc: "propfind: propfind with ignored whitespace",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop>   <A:displayname/></A:prop>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName: xml.Name{"DAV:", "propfind"},
+			Prop:    propnames{xml.Name{"DAV:", "displayname"}},
+		},
+	}, {
+		desc: "propfind: propfind with ignored mixed-content",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop>foo<A:displayname/>bar</A:prop>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName: xml.Name{"DAV:", "propfind"},
+			Prop:    propnames{xml.Name{"DAV:", "displayname"}},
+		},
+	}, {
+		desc: "propfind: propname with ignored element (section A.4)",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:propname/>\n" +
+			"  <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" +
+			"</A:propfind>",
+		wantPF: propfind{
+			XMLName:  xml.Name{"DAV:", "propfind"},
+			Propname: new(struct{}),
+		},
+	}, {
+		desc:       "propfind: bad: junk",
+		input:      "xxx",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: propname and allprop (section A.3)",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:propname/>" +
+			"  <A:allprop/>" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: propname and prop",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop><A:displayname/></A:prop>\n" +
+			"  <A:propname/>\n" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: allprop and prop",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:allprop/>\n" +
+			"  <A:prop><A:foo/><A:/prop>\n" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: empty propfind with ignored element (section A.4)",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <E:expired-props/>\n" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: empty prop",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop/>\n" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: prop with just chardata",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop>foo</A:prop>\n" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "bad: interrupted prop",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop><A:foo></A:prop>\n",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "bad: malformed end element prop",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop><A:foo/></A:bar></A:prop>\n",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: property with chardata value",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop><A:foo>bar</A:foo></A:prop>\n" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: property with whitespace value",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:prop><A:foo> </A:foo></A:prop>\n" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}, {
+		desc: "propfind: bad: include without allprop",
+		input: "" +
+			"<A:propfind xmlns:A='DAV:'>\n" +
+			"  <A:include><A:foo/></A:include>\n" +
+			"</A:propfind>",
+		wantStatus: http.StatusBadRequest,
+	}}
+
+	for _, tc := range testCases {
+		pf, status, err := readPropfind(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 !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {
+			t.Errorf("%s:\ngot  propfind=%v, status=%v\nwant propfind=%v, status=%v",
+				tc.desc, pf, status, tc.wantPF, tc.wantStatus)
+			continue
+		}
+	}
+}