소스 검색

webdav: implement multistatus response and property.

This change contains a proposal for marshalling multistatus responses to
support the lock and property systems. In addition, a type for properties
is defined for the to-be-implemented property system.

Change-Id: I3d768d8d61bb9495bc70e4699dee8958d444147f
Reviewed-on: https://go-review.googlesource.com/3160
Reviewed-by: Nigel Tao <nigeltao@golang.org>
Robert Stepanek 11 년 전
부모
커밋
b8c11bbe94
3개의 변경된 파일351개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      webdav/webdav.go
  2. 117 0
      webdav/xml.go
  3. 233 0
      webdav/xml_test.go

+ 1 - 0
webdav/webdav.go

@@ -355,6 +355,7 @@ var (
 	errInvalidLockInfo     = errors.New("webdav: invalid lock info")
 	errInvalidLockToken    = errors.New("webdav: invalid lock token")
 	errInvalidPropfind     = errors.New("webdav: invalid propfind")
+	errInvalidResponse     = errors.New("webdav: invalid response")
 	errInvalidTimeout      = errors.New("webdav: invalid timeout")
 	errNoFileSystem        = errors.New("webdav: no file system")
 	errNoLockSystem        = errors.New("webdav: no lock system")

+ 117 - 0
webdav/xml.go

@@ -183,3 +183,120 @@ func readPropfind(r io.Reader) (pf propfind, status int, err error) {
 	}
 	return pf, 0, nil
 }
+
+// Property represents a single DAV resource property as defined in RFC 4918.
+// See http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties
+type Property struct {
+	// XMLName is the fully qualified name that identifies this property.
+	XMLName xml.Name
+
+	// Lang is an optional xml:lang attribute.
+	Lang string `xml:"xml:lang,attr,omitempty"`
+
+	// InnerXML contains the XML representation of the property value.
+	// See http://www.webdav.org/specs/rfc4918.html#property_values
+	//
+	// Property values of complex type or mixed-content must have fully
+	// expanded XML namespaces or be self-contained with according
+	// XML namespace declarations. They must not rely on any XML
+	// namespace declarations within the scope of the XML document,
+	// even including the DAV: namespace.
+	InnerXML []byte `xml:",innerxml"`
+}
+
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
+type xmlError struct {
+	XMLName  xml.Name `xml:"DAV: error"`
+	InnerXML []byte   `xml:",innerxml"`
+}
+
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
+type propstat struct {
+	// Prop requires DAV: to be the default namespace in the enclosing
+	// XML. This is due to the standard encoding/xml package currently
+	// not honoring namespace declarations inside a xmltag with a
+	// parent element for anonymous slice elements.
+	// Use of multistatusWriter takes care of this.
+	Prop                []Property `xml:"prop>_ignored_"`
+	Status              string     `xml:"DAV: status"`
+	Error               *xmlError  `xml:"DAV: error"`
+	ResponseDescription string     `xml:"DAV: responsedescription,omitempty"`
+}
+
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_response
+type response struct {
+	XMLName             xml.Name   `xml:"DAV: response"`
+	Href                []string   `xml:"DAV: href"`
+	Propstat            []propstat `xml:"DAV: propstat"`
+	Status              string     `xml:"DAV: status,omitempty"`
+	Error               *xmlError  `xml:"DAV: error"`
+	ResponseDescription string     `xml:"DAV: responsedescription,omitempty"`
+}
+
+// MultistatusWriter marshals one or more Responses into a XML
+// multistatus response.
+// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_multistatus
+type multistatusWriter struct {
+	// ResponseDescription contains the optional responsedescription
+	// of the multistatus XML element. Only the latest content before
+	// close will be emitted. Empty response descriptions are not
+	// written.
+	responseDescription string
+
+	w   http.ResponseWriter
+	enc *xml.Encoder
+}
+
+// Write validates and emits a DAV response as part of a multistatus response
+// element.
+//
+// It sets the HTTP status code of its underlying http.ResponseWriter to 207
+// (Multi-Status) and populates the Content-Type header. If r is the
+// first, valid response to be written, Write prepends the XML representation
+// of r with a multistatus tag. Callers must call close after the last response
+// has been written.
+func (w *multistatusWriter) write(r *response) error {
+	switch len(r.Href) {
+	case 0:
+		return errInvalidResponse
+	case 1:
+		if len(r.Propstat) > 0 != (r.Status == "") {
+			return errInvalidResponse
+		}
+	default:
+		if len(r.Propstat) > 0 || r.Status == "" {
+			return errInvalidResponse
+		}
+	}
+	if w.enc == nil {
+		w.w.WriteHeader(StatusMulti)
+		w.w.Header().Add("Content-Type", "text/xml; charset=utf-8")
+		_, err := fmt.Fprintf(w.w, `<?xml version="1.0" encoding="UTF-8"?>`+
+			`<D:multistatus xmlns:D="DAV:">`)
+		if err != nil {
+			return err
+		}
+		w.enc = xml.NewEncoder(w.w)
+	}
+	return w.enc.Encode(r)
+}
+
+// Close completes the marshalling of the multistatus response. It returns
+// an error if the multistatus response could not be completed. If both the
+// return value and field enc of w are nil, then no multistatus response has
+// been written.
+func (w *multistatusWriter) close() error {
+	if w.enc == nil {
+		return nil
+	}
+	if w.responseDescription != "" {
+		_, err := fmt.Fprintf(w.w,
+			"<D:responsedescription>%s</D:responsedescription>",
+			w.responseDescription)
+		if err != nil {
+			return err
+		}
+	}
+	_, err := fmt.Fprintf(w.w, "</D:multistatus>")
+	return err
+}

+ 233 - 0
webdav/xml_test.go

@@ -7,6 +7,7 @@ package webdav
 import (
 	"encoding/xml"
 	"net/http"
+	"net/http/httptest"
 	"reflect"
 	"strings"
 	"testing"
@@ -340,3 +341,235 @@ func TestReadPropfind(t *testing.T) {
 		}
 	}
 }
+
+func TestMultistatusWriter(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
+	//
+	// BUG:The following tests compare the actual and expected XML verbatim.
+	// Minor tweaks in the marshalling output of either standard encoding/xml
+	// or this package might break them. A more resilient approach could be
+	// to normalize both actual and expected XML content before comparison.
+	// This also would enhance readibility of the expected XML payload in the
+	// wantXML field.
+	testCases := []struct {
+		desc      string
+		responses []response
+		respdesc  string
+		wantXML   string
+		wantCode  int
+		wantErr   error
+	}{{
+		desc: "section 9.2.2 (failed dependency)",
+		responses: []response{{
+			Href: []string{"http://example.com/foo"},
+			Propstat: []propstat{{
+				Prop: []Property{{
+					XMLName: xml.Name{"http://ns.example.com/", "Authors"},
+				}},
+				Status: "HTTP/1.1 424 Failed Dependency",
+			}, {
+				Prop: []Property{{
+					XMLName: xml.Name{"http://ns.example.com/", "Copyright-Owner"},
+				}},
+				Status: "HTTP/1.1 409 Conflict",
+			}},
+			ResponseDescription: " Copyright Owner cannot be deleted or altered.",
+		}},
+		wantXML: `<?xml version="1.0" encoding="UTF-8"?>` +
+			`<D:multistatus xmlns:D="DAV:">` +
+			`<response xmlns="DAV:">` +
+			`<href xmlns="DAV:">http://example.com/foo</href>` +
+			`<propstat xmlns="DAV:">` +
+			`<prop>` +
+			`<Authors xmlns="http://ns.example.com/"></Authors>` +
+			`</prop>` +
+			`<status xmlns="DAV:">HTTP/1.1 424 Failed Dependency</status>` +
+			`</propstat>` +
+			`<propstat xmlns="DAV:">` +
+			`<prop>` +
+			`<Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` +
+			`</prop>` +
+			`<status xmlns="DAV:">HTTP/1.1 409 Conflict</status>` +
+			`</propstat>` +
+			`<responsedescription xmlns="DAV:">` +
+			` Copyright Owner cannot be deleted or altered.` +
+			`</responsedescription>` +
+			`</response>` +
+			`</D:multistatus>`,
+		wantCode: StatusMulti,
+	}, {
+		desc: "section 9.6.2 (lock-token-submitted)",
+		responses: []response{{
+			Href:   []string{"http://example.com/foo"},
+			Status: "HTTP/1.1 423 Locked",
+			Error: &xmlError{
+				InnerXML: []byte(`<lock-token-submitted xmlns="DAV:"/>`),
+			},
+		}},
+		wantXML: `<?xml version="1.0" encoding="UTF-8"?>` +
+			`<D:multistatus xmlns:D="DAV:">` +
+			`<response xmlns="DAV:">` +
+			`<href xmlns="DAV:">http://example.com/foo</href>` +
+			`<status xmlns="DAV:">HTTP/1.1 423 Locked</status>` +
+			`<error xmlns="DAV:"><lock-token-submitted xmlns="DAV:"/></error>` +
+			`</response>` +
+			`</D:multistatus>`,
+		wantCode: StatusMulti,
+	}, {
+		desc: "section 9.1.3",
+		responses: []response{{
+			Href: []string{"http://example.com/foo"},
+			Propstat: []propstat{{
+				Prop: []Property{{
+					XMLName: xml.Name{"http://ns.example.com/boxschema/", "bigbox"},
+					InnerXML: []byte(`` +
+						`<BoxType xmlns="http://ns.example.com/boxschema/">` +
+						`Box type A` +
+						`</BoxType>`),
+				}, {
+					XMLName: xml.Name{"http://ns.example.com/boxschema/", "author"},
+					InnerXML: []byte(`` +
+						`<Name xmlns="http://ns.example.com/boxschema/">` +
+						`J.J. Johnson` +
+						`</Name>`),
+				}},
+				Status: "HTTP/1.1 200 OK",
+			}, {
+				Prop: []Property{{
+					XMLName: xml.Name{"http://ns.example.com/boxschema/", "DingALing"},
+				}, {
+					XMLName: xml.Name{"http://ns.example.com/boxschema/", "Random"},
+				}},
+				Status:              "HTTP/1.1 403 Forbidden",
+				ResponseDescription: " The user does not have access to the DingALing property.",
+			}},
+		}},
+		respdesc: " There has been an access violation error.",
+		wantXML: `<?xml version="1.0" encoding="UTF-8"?>` +
+			`<D:multistatus xmlns:D="DAV:">` +
+			`<response xmlns="DAV:">` +
+			`<href xmlns="DAV:">http://example.com/foo</href>` +
+			`<propstat xmlns="DAV:">` +
+			`<prop>` +
+			`<bigbox xmlns="http://ns.example.com/boxschema/">` +
+			`<BoxType xmlns="http://ns.example.com/boxschema/">Box type A</BoxType>` +
+			`</bigbox>` +
+			`<author xmlns="http://ns.example.com/boxschema/">` +
+			`<Name xmlns="http://ns.example.com/boxschema/">J.J. Johnson</Name>` +
+			`</author>` +
+			`</prop>` +
+			`<status xmlns="DAV:">HTTP/1.1 200 OK</status>` +
+			`</propstat>` +
+			`<propstat xmlns="DAV:">` +
+			`<prop>` +
+			`<DingALing xmlns="http://ns.example.com/boxschema/">` +
+			`</DingALing>` +
+			`<Random xmlns="http://ns.example.com/boxschema/">` +
+			`</Random>` +
+			`</prop>` +
+			`<status xmlns="DAV:">HTTP/1.1 403 Forbidden</status>` +
+			`<responsedescription xmlns="DAV:">` +
+			` The user does not have access to the DingALing property.` +
+			`</responsedescription>` +
+			`</propstat>` +
+			`</response>` +
+			`<D:responsedescription>` +
+			` There has been an access violation error.` +
+			`</D:responsedescription>` +
+			`</D:multistatus>`,
+		wantCode: StatusMulti,
+	}, {
+		desc: "bad: no response written",
+		// default of http.responseWriter
+		wantCode: http.StatusOK,
+	}, {
+		desc:     "bad: no response written (with description)",
+		respdesc: "too bad",
+		// default of http.responseWriter
+		wantCode: http.StatusOK,
+	}, {
+		desc: "bad: no href",
+		responses: []response{{
+			Propstat: []propstat{{
+				Prop: []Property{{
+					XMLName: xml.Name{"http://example.com/", "foo"},
+				}},
+				Status: "HTTP/1.1 200 OK",
+			}},
+		}},
+		wantErr: errInvalidResponse,
+		// default of http.responseWriter
+		wantCode: http.StatusOK,
+	}, {
+		desc: "bad: multiple hrefs and no status",
+		responses: []response{{
+			Href: []string{"http://example.com/foo", "http://example.com/bar"},
+		}},
+		wantErr: errInvalidResponse,
+		// default of http.responseWriter
+		wantCode: http.StatusOK,
+	}, {
+		desc: "bad: one href and no propstat",
+		responses: []response{{
+			Href: []string{"http://example.com/foo"},
+		}},
+		wantErr: errInvalidResponse,
+		// default of http.responseWriter
+		wantCode: http.StatusOK,
+	}, {
+		desc: "bad: status with one href and propstat",
+		responses: []response{{
+			Href: []string{"http://example.com/foo"},
+			Propstat: []propstat{{
+				Prop: []Property{{
+					XMLName: xml.Name{"http://example.com/", "foo"},
+				}},
+				Status: "HTTP/1.1 200 OK",
+			}},
+			Status: "HTTP/1.1 200 OK",
+		}},
+		wantErr: errInvalidResponse,
+		// default of http.responseWriter
+		wantCode: http.StatusOK,
+	}, {
+		desc: "bad: multiple hrefs and propstat",
+		responses: []response{{
+			Href: []string{"http://example.com/foo", "http://example.com/bar"},
+			Propstat: []propstat{{
+				Prop: []Property{{
+					XMLName: xml.Name{"http://example.com/", "foo"},
+				}},
+				Status: "HTTP/1.1 200 OK",
+			}},
+		}},
+		wantErr: errInvalidResponse,
+		// default of http.responseWriter
+		wantCode: http.StatusOK,
+	}}
+loop:
+	for _, tc := range testCases {
+		rec := httptest.NewRecorder()
+		w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
+		for _, r := range tc.responses {
+			if err := w.write(&r); err != nil {
+				if err != tc.wantErr {
+					t.Errorf("%s: got write error %v, want %v", tc.desc, err, tc.wantErr)
+				}
+				continue loop
+			}
+		}
+		if err := w.close(); err != tc.wantErr {
+			t.Errorf("%s: got close error %v, want %v", tc.desc, err, tc.wantErr)
+			continue
+		}
+		if rec.Code != tc.wantCode {
+			t.Errorf("%s: got HTTP status code %d, want %d\n", tc.desc, rec.Code, tc.wantCode)
+			continue
+		}
+		if gotXML := rec.Body.String(); gotXML != tc.wantXML {
+			t.Errorf("%s: XML body\ngot  %q\nwant %q", tc.desc, gotXML, tc.wantXML)
+			continue
+		}
+	}
+}