| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package webdav |
| |
| // The XML encoding is covered by Section 14. |
| // http://www.webdav.org/specs/rfc4918.html#xml.element.definitions |
| |
| import ( |
| "bytes" |
| "encoding/xml" |
| "fmt" |
| "io" |
| "net/http" |
| "time" |
| |
| // As of https://go-review.googlesource.com/#/c/12772/ which was submitted |
| // in July 2015, this package uses an internal fork of the standard |
| // library's encoding/xml package, due to changes in the way namespaces |
| // were encoded. Such changes were introduced in the Go 1.5 cycle, but were |
| // rolled back in response to https://github.com/golang/go/issues/11841 |
| // |
| // However, this package's exported API, specifically the Property and |
| // DeadPropsHolder types, need to refer to the standard library's version |
| // of the xml.Name type, as code that imports this package cannot refer to |
| // the internal version. |
| // |
| // This file therefore imports both the internal and external versions, as |
| // ixml and xml, and converts between them. |
| // |
| // In the long term, this package should use the standard library's version |
| // only, and the internal fork deleted, once |
| // https://github.com/golang/go/issues/13400 is resolved. |
| ixml "golang.org/x/net/webdav/internal/xml" |
| ) |
| |
| // http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo |
| type lockInfo struct { |
| XMLName ixml.Name `xml:"lockinfo"` |
| Exclusive *struct{} `xml:"lockscope>exclusive"` |
| Shared *struct{} `xml:"lockscope>shared"` |
| Write *struct{} `xml:"locktype>write"` |
| Owner owner `xml:"owner"` |
| } |
| |
| // http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner |
| type owner struct { |
| InnerXML string `xml:",innerxml"` |
| } |
| |
| func readLockInfo(r io.Reader) (li lockInfo, status int, err error) { |
| c := &countingReader{r: r} |
| if err = ixml.NewDecoder(c).Decode(&li); err != nil { |
| if err == io.EOF { |
| if c.n == 0 { |
| // An empty body means to refresh the lock. |
| // http://www.webdav.org/specs/rfc4918.html#refreshing-locks |
| return lockInfo{}, 0, nil |
| } |
| err = errInvalidLockInfo |
| } |
| return lockInfo{}, http.StatusBadRequest, err |
| } |
| // We only support exclusive (non-shared) write locks. In practice, these are |
| // the only types of locks that seem to matter. |
| if li.Exclusive == nil || li.Shared != nil || li.Write == nil { |
| return lockInfo{}, http.StatusNotImplemented, errUnsupportedLockInfo |
| } |
| return li, 0, nil |
| } |
| |
| type countingReader struct { |
| n int |
| r io.Reader |
| } |
| |
| func (c *countingReader) Read(p []byte) (int, error) { |
| n, err := c.r.Read(p) |
| c.n += n |
| return n, err |
| } |
| |
| func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) { |
| depth := "infinity" |
| if ld.ZeroDepth { |
| depth = "0" |
| } |
| timeout := ld.Duration / time.Second |
| return fmt.Fprintf(w, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+ |
| "<D:prop xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock>\n"+ |
| " <D:locktype><D:write/></D:locktype>\n"+ |
| " <D:lockscope><D:exclusive/></D:lockscope>\n"+ |
| " <D:depth>%s</D:depth>\n"+ |
| " <D:owner>%s</D:owner>\n"+ |
| " <D:timeout>Second-%d</D:timeout>\n"+ |
| " <D:locktoken><D:href>%s</D:href></D:locktoken>\n"+ |
| " <D:lockroot><D:href>%s</D:href></D:lockroot>\n"+ |
| "</D:activelock></D:lockdiscovery></D:prop>", |
| depth, ld.OwnerXML, timeout, escape(token), escape(ld.Root), |
| ) |
| } |
| |
| func escape(s string) string { |
| for i := 0; i < len(s); i++ { |
| switch s[i] { |
| case '"', '&', '\'', '<', '>': |
| b := bytes.NewBuffer(nil) |
| ixml.EscapeText(b, []byte(s)) |
| return b.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 *ixml.Decoder) (ixml.Token, error) { |
| for { |
| t, err := d.Token() |
| if err != nil { |
| return t, err |
| } |
| switch t.(type) { |
| case ixml.Comment, ixml.Directive, ixml.ProcInst: |
| continue |
| default: |
| return t, nil |
| } |
| } |
| } |
| |
| // http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind) |
| 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 *propfindProps) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error { |
| for { |
| t, err := next(d) |
| if err != nil { |
| return err |
| } |
| switch t.(type) { |
| case ixml.EndElement: |
| if len(*pn) == 0 { |
| return fmt.Errorf("%s must not be empty", start.Name.Local) |
| } |
| return nil |
| case ixml.StartElement: |
| name := t.(ixml.StartElement).Name |
| t, err = next(d) |
| if err != nil { |
| return err |
| } |
| if _, ok := t.(ixml.EndElement); !ok { |
| return fmt.Errorf("unexpected token %T", t) |
| } |
| *pn = append(*pn, xml.Name(name)) |
| } |
| } |
| } |
| |
| // http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind |
| type propfind struct { |
| XMLName ixml.Name `xml:"DAV: propfind"` |
| Allprop *struct{} `xml:"DAV: allprop"` |
| Propname *struct{} `xml:"DAV: propname"` |
| Prop propfindProps `xml:"DAV: prop"` |
| Include propfindProps `xml:"DAV: include"` |
| } |
| |
| func readPropfind(r io.Reader) (pf propfind, status int, err error) { |
| c := countingReader{r: r} |
| if err = ixml.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 |
| } |
| |
| // 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"` |
| } |
| |
| // ixmlProperty is the same as the Property type except it holds an ixml.Name |
| // instead of an xml.Name. |
| type ixmlProperty struct { |
| XMLName ixml.Name |
| Lang string `xml:"xml:lang,attr,omitempty"` |
| InnerXML []byte `xml:",innerxml"` |
| } |
| |
| // http://www.webdav.org/specs/rfc4918.html#ELEMENT_error |
| // See multistatusWriter for the "D:" namespace prefix. |
| type xmlError struct { |
| XMLName ixml.Name `xml:"D:error"` |
| InnerXML []byte `xml:",innerxml"` |
| } |
| |
| // http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat |
| // See multistatusWriter for the "D:" namespace prefix. |
| type propstat struct { |
| Prop []Property `xml:"D:prop>_ignored_"` |
| Status string `xml:"D:status"` |
| Error *xmlError `xml:"D:error"` |
| ResponseDescription string `xml:"D:responsedescription,omitempty"` |
| } |
| |
| // ixmlPropstat is the same as the propstat type except it holds an ixml.Name |
| // instead of an xml.Name. |
| type ixmlPropstat struct { |
| Prop []ixmlProperty `xml:"D:prop>_ignored_"` |
| Status string `xml:"D:status"` |
| Error *xmlError `xml:"D:error"` |
| ResponseDescription string `xml:"D:responsedescription,omitempty"` |
| } |
| |
| // MarshalXML prepends the "D:" namespace prefix on properties in the DAV: namespace |
| // before encoding. See multistatusWriter. |
| func (ps propstat) MarshalXML(e *ixml.Encoder, start ixml.StartElement) error { |
| // Convert from a propstat to an ixmlPropstat. |
| ixmlPs := ixmlPropstat{ |
| Prop: make([]ixmlProperty, len(ps.Prop)), |
| Status: ps.Status, |
| Error: ps.Error, |
| ResponseDescription: ps.ResponseDescription, |
| } |
| for k, prop := range ps.Prop { |
| ixmlPs.Prop[k] = ixmlProperty{ |
| XMLName: ixml.Name(prop.XMLName), |
| Lang: prop.Lang, |
| InnerXML: prop.InnerXML, |
| } |
| } |
| |
| for k, prop := range ixmlPs.Prop { |
| if prop.XMLName.Space == "DAV:" { |
| prop.XMLName = ixml.Name{Space: "", Local: "D:" + prop.XMLName.Local} |
| ixmlPs.Prop[k] = prop |
| } |
| } |
| // Distinct type to avoid infinite recursion of MarshalXML. |
| type newpropstat ixmlPropstat |
| return e.EncodeElement(newpropstat(ixmlPs), start) |
| } |
| |
| // http://www.webdav.org/specs/rfc4918.html#ELEMENT_response |
| // See multistatusWriter for the "D:" namespace prefix. |
| type response struct { |
| XMLName ixml.Name `xml:"D:response"` |
| Href []string `xml:"D:href"` |
| Propstat []propstat `xml:"D:propstat"` |
| Status string `xml:"D:status,omitempty"` |
| Error *xmlError `xml:"D:error"` |
| ResponseDescription string `xml:"D:responsedescription,omitempty"` |
| } |
| |
| // MultistatusWriter marshals one or more Responses into a XML |
| // multistatus response. |
| // See http://www.webdav.org/specs/rfc4918.html#ELEMENT_multistatus |
| // TODO(rsto, mpl): As a workaround, the "D:" namespace prefix, defined as |
| // "DAV:" on this element, is prepended on the nested response, as well as on all |
| // its nested elements. All property names in the DAV: namespace are prefixed as |
| // well. This is because some versions of Mini-Redirector (on windows 7) ignore |
| // elements with a default namespace (no prefixed namespace). A less intrusive fix |
| // should be possible after golang.org/cl/11074. See https://golang.org/issue/11177 |
| 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 *ixml.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 |
| } |
| } |
| err := w.writeHeader() |
| if err != nil { |
| return err |
| } |
| return w.enc.Encode(r) |
| } |
| |
| // writeHeader writes a XML multistatus start element on w's underlying |
| // http.ResponseWriter and returns the result of the write operation. |
| // After the first write attempt, writeHeader becomes a no-op. |
| func (w *multistatusWriter) writeHeader() error { |
| if w.enc != nil { |
| return nil |
| } |
| w.w.Header().Add("Content-Type", "text/xml; charset=utf-8") |
| w.w.WriteHeader(StatusMulti) |
| _, err := fmt.Fprintf(w.w, `<?xml version="1.0" encoding="UTF-8"?>`) |
| if err != nil { |
| return err |
| } |
| w.enc = ixml.NewEncoder(w.w) |
| return w.enc.EncodeToken(ixml.StartElement{ |
| Name: ixml.Name{ |
| Space: "DAV:", |
| Local: "multistatus", |
| }, |
| Attr: []ixml.Attr{{ |
| Name: ixml.Name{Space: "xmlns", Local: "D"}, |
| Value: "DAV:", |
| }}, |
| }) |
| } |
| |
| // 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 |
| } |
| var end []ixml.Token |
| if w.responseDescription != "" { |
| name := ixml.Name{Space: "DAV:", Local: "responsedescription"} |
| end = append(end, |
| ixml.StartElement{Name: name}, |
| ixml.CharData(w.responseDescription), |
| ixml.EndElement{Name: name}, |
| ) |
| } |
| end = append(end, ixml.EndElement{ |
| Name: ixml.Name{Space: "DAV:", Local: "multistatus"}, |
| }) |
| for _, t := range end { |
| err := w.enc.EncodeToken(t) |
| if err != nil { |
| return err |
| } |
| } |
| return w.enc.Flush() |
| } |
| |
| var xmlLangName = ixml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"} |
| |
| func xmlLang(s ixml.StartElement, d string) string { |
| for _, attr := range s.Attr { |
| if attr.Name == xmlLangName { |
| return attr.Value |
| } |
| } |
| return d |
| } |
| |
| type xmlValue []byte |
| |
| func (v *xmlValue) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error { |
| // 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 := ixml.NewEncoder(&b) |
| for { |
| t, err := next(d) |
| if err != nil { |
| return err |
| } |
| if e, ok := t.(ixml.EndElement); ok && e.Name == start.Name { |
| break |
| } |
| if err = e.EncodeToken(t); err != nil { |
| return err |
| } |
| } |
| err := e.Flush() |
| if err != nil { |
| return err |
| } |
| *v = b.Bytes() |
| return nil |
| } |
| |
| // http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for proppatch) |
| type proppatchProps []Property |
| |
| // 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 *ixml.Decoder, start ixml.StartElement) error { |
| lang := xmlLang(start, "") |
| for { |
| t, err := next(d) |
| if err != nil { |
| return err |
| } |
| switch elem := t.(type) { |
| case ixml.EndElement: |
| if len(*ps) == 0 { |
| return fmt.Errorf("%s must not be empty", start.Name.Local) |
| } |
| return nil |
| case ixml.StartElement: |
| p := Property{ |
| XMLName: xml.Name(t.(ixml.StartElement).Name), |
| Lang: xmlLang(t.(ixml.StartElement), lang), |
| } |
| err = d.DecodeElement(((*xmlValue)(&p.InnerXML)), &elem) |
| if err != nil { |
| return err |
| } |
| *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 ixml.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 ixml.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 = ixml.NewDecoder(r).Decode(&pu); err != nil { |
| return nil, http.StatusBadRequest, err |
| } |
| for _, op := range pu.SetRemove { |
| remove := false |
| switch op.XMLName { |
| case ixml.Name{Space: "DAV:", Local: "set"}: |
| // No-op. |
| case ixml.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 |
| } |