diff --git a/go.mod b/go.mod index b3341dccdc0..9fa3497b8cb 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect github.com/armon/go-radix v1.0.0 github.com/aws/aws-sdk-go v1.32.7 - github.com/beevik/etree v0.0.0-20170418002358-cda1c0026246 + github.com/beevik/etree v1.1.0 github.com/boombuler/barcode v0.0.0-20161226211916-fe0f26ff6d26 // indirect github.com/cjbassi/drawille-go v0.1.0 // indirect github.com/codahale/hdrhistogram v0.0.0-20150708134006-954f16e8b9ef @@ -44,7 +44,7 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/iovisor/gobpf v0.0.1 github.com/johannesboyne/gofakes3 v0.0.0-20191228161223-9aee1c78a252 - github.com/jonboulle/clockwork v0.1.1-0.20190114141812-62fb9bc030d1 + github.com/jonboulle/clockwork v0.2.1 github.com/json-iterator/go v1.1.9 github.com/julienschmidt/httprouter v1.2.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 @@ -66,7 +66,7 @@ require ( github.com/prometheus/common v0.6.0 github.com/prometheus/procfs v0.0.4 // indirect github.com/russellhaering/gosaml2 v0.0.0-20170515204909-8908227c114a - github.com/russellhaering/goxmldsig v0.0.0-20170515183101-605161228693 + github.com/russellhaering/goxmldsig v1.1.0 github.com/satori/go.uuid v1.1.1-0.20170321230731-5bf94b69c6b6 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect diff --git a/go.sum b/go.sum index a72c15c7b78..6ac81efb74b 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.32.7 h1:H4VgdCSF1cHw0VD8zGc98T1bGdACoLkh/vK2L6wgOUU= github.com/aws/aws-sdk-go v1.32.7/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/beevik/etree v0.0.0-20170418002358-cda1c0026246 h1:ug+AeM+Rh5O1beNwuhXlR29jr11nUuB7ZB+X9SzPSJQ= -github.com/beevik/etree v0.0.0-20170418002358-cda1c0026246/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -201,8 +201,10 @@ github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeY github.com/johannesboyne/gofakes3 v0.0.0-20191228161223-9aee1c78a252 h1:ZABLXRnnFNP5nkVzVBx2kQ/4GvSLUqcD2YUc+9Uc2Mo= github.com/johannesboyne/gofakes3 v0.0.0-20191228161223-9aee1c78a252/go.mod h1:cPDudDcSR9fls3ZmrXgt0GU2QpQGQRJc4JBNtKyNr1s= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.1.1-0.20190114141812-62fb9bc030d1 h1:qBCV/RLV02TSfQa7tFmxTihnG+u+7JXByOkhlkR5rmQ= -github.com/jonboulle/clockwork v0.1.1-0.20190114141812-62fb9bc030d1/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.0 h1:J2SLSdy7HgElq8ekSl2Mxh6vrRNFxqbXGenYH2I02Vs= +github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.2.1 h1:S/EaQvW6FpWMYAvYvY+OBDvpaM+izu0oiwo5y0MH7U0= +github.com/jonboulle/clockwork v0.2.1/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -299,8 +301,8 @@ github.com/prometheus/procfs v0.0.4/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/russellhaering/gosaml2 v0.0.0-20170515204909-8908227c114a h1:t10m/WbIQiAAggqE6J2401FInpp50XPi249G7nE45tc= github.com/russellhaering/gosaml2 v0.0.0-20170515204909-8908227c114a/go.mod h1:niieRtQaw+opTVp9jzZo1nAAoksI2eNpd+weDcjZ+Mk= -github.com/russellhaering/goxmldsig v0.0.0-20170515183101-605161228693 h1:N6BLFjXGfV1dGbLqHVVqe7u6ii+JHrl7L2lRZK7JzrY= -github.com/russellhaering/goxmldsig v0.0.0-20170515183101-605161228693/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM= +github.com/russellhaering/goxmldsig v1.1.0 h1:lK/zeJie2sqG52ZAlPNn1oBBqsIsEKypUUBGpYYF6lk= +github.com/russellhaering/goxmldsig v1.1.0/go.mod h1:QK8GhXPB3+AfuCrfo0oRISa9NfzeCpWmxeGnqEpDF9o= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/satori/go.uuid v1.1.1-0.20170321230731-5bf94b69c6b6 h1:oZag5hylqWwZrDdj/laMwWQnXaeWBQf66qm4PGQI6Wc= diff --git a/lib/services/saml.go b/lib/services/saml.go index a0da1bbb375..52a0954d689 100644 --- a/lib/services/saml.go +++ b/lib/services/saml.go @@ -561,15 +561,17 @@ func (o *SAMLConnectorV2) GetServiceProvider(clock clockwork.Clock) (*saml2.SAML } for _, kd := range metadata.IDPSSODescriptor.KeyDescriptors { - certData, err := base64.StdEncoding.DecodeString(strings.TrimSpace(kd.KeyInfo.X509Data.X509Certificate.Data)) - if err != nil { - return nil, trace.Wrap(err) + for _, samlCert := range kd.KeyInfo.X509Data.X509Certificates { + certData, err := base64.StdEncoding.DecodeString(strings.TrimSpace(samlCert.Data)) + if err != nil { + return nil, trace.Wrap(err) + } + cert, err := x509.ParseCertificate(certData) + if err != nil { + return nil, trace.Wrap(err, "failed to parse certificate in metadata") + } + certStore.Roots = append(certStore.Roots, cert) } - cert, err := x509.ParseCertificate(certData) - if err != nil { - return nil, trace.Wrap(err, "failed to parse certificate in metadata") - } - certStore.Roots = append(certStore.Roots, cert) } o.Spec.Issuer = metadata.EntityID o.Spec.SSO = metadata.IDPSSODescriptor.SingleSignOnService.Location diff --git a/vendor/github.com/beevik/etree/.travis.yml b/vendor/github.com/beevik/etree/.travis.yml index 7caa2477c6d..f4cb25d473a 100644 --- a/vendor/github.com/beevik/etree/.travis.yml +++ b/vendor/github.com/beevik/etree/.travis.yml @@ -2,9 +2,7 @@ language: go sudo: false go: - - 1.4.2 - - 1.5.1 - - 1.6 + - 1.11.x - tip matrix: diff --git a/vendor/github.com/beevik/etree/CONTRIBUTORS b/vendor/github.com/beevik/etree/CONTRIBUTORS index 084662c3a83..03211a85eb6 100644 --- a/vendor/github.com/beevik/etree/CONTRIBUTORS +++ b/vendor/github.com/beevik/etree/CONTRIBUTORS @@ -6,3 +6,5 @@ Matt Smith (ma314smith) Michal Jemala (michaljemala) Nicolas Piganeau (npiganeau) Chris Brown (ccbrown) +Earncef Sequeira (earncef) +Gabriel de Labachelerie (wuzuf) diff --git a/vendor/github.com/beevik/etree/LICENSE b/vendor/github.com/beevik/etree/LICENSE index e14ad682a0d..26f1f7751b6 100644 --- a/vendor/github.com/beevik/etree/LICENSE +++ b/vendor/github.com/beevik/etree/LICENSE @@ -1,4 +1,4 @@ -Copyright 2015 Brett Vickers. All rights reserved. +Copyright 2015-2019 Brett Vickers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/vendor/github.com/beevik/etree/README.md b/vendor/github.com/beevik/etree/README.md index 28558433c80..08ec26b0ac6 100644 --- a/vendor/github.com/beevik/etree/README.md +++ b/vendor/github.com/beevik/etree/README.md @@ -7,7 +7,9 @@ etree The etree package is a lightweight, pure go package that expresses XML in the form of an element tree. Its design was inspired by the Python [ElementTree](http://docs.python.org/2/library/xml.etree.elementtree.html) -module. Some of the package's features include: +module. + +Some of the package's capabilities and features: * Represents XML documents as trees of elements for easy traversal. * Imports, serializes, modifies or creates XML documents from scratch. diff --git a/vendor/github.com/beevik/etree/RELEASE_NOTES.md b/vendor/github.com/beevik/etree/RELEASE_NOTES.md new file mode 100644 index 00000000000..ee59d7abf4c --- /dev/null +++ b/vendor/github.com/beevik/etree/RELEASE_NOTES.md @@ -0,0 +1,109 @@ +Release v1.1.0 +============== + +**New Features** + +* New attribute helpers. + * Added the `Element.SortAttrs` method, which lexicographically sorts an + element's attributes by key. +* New `ReadSettings` properties. + * Added `Entity` for the support of custom entity maps. +* New `WriteSettings` properties. + * Added `UseCRLF` to allow the output of CR-LF newlines instead of the + default LF newlines. This is useful on Windows systems. +* Additional support for text and CDATA sections. + * The `Element.Text` method now returns the concatenation of all consecutive + character data tokens immediately following an element's opening tag. + * Added `Element.SetCData` to replace the character data immediately + following an element's opening tag with a CDATA section. + * Added `Element.CreateCData` to create and add a CDATA section child + `CharData` token to an element. + * Added `Element.CreateText` to create and add a child text `CharData` token + to an element. + * Added `NewCData` to create a parentless CDATA section `CharData` token. + * Added `NewText` to create a parentless text `CharData` + token. + * Added `CharData.IsCData` to detect if the token contains a CDATA section. + * Added `CharData.IsWhitespace` to detect if the token contains whitespace + inserted by one of the document Indent functions. + * Modified `Element.SetText` so that it replaces a run of consecutive + character data tokens following the element's opening tag (instead of just + the first one). +* New "tail text" support. + * Added the `Element.Tail` method, which returns the text immediately + following an element's closing tag. + * Added the `Element.SetTail` method, which modifies the text immediately + following an element's closing tag. +* New element child insertion and removal methods. + * Added the `Element.InsertChildAt` method, which inserts a new child token + before the specified child token index. + * Added the `Element.RemoveChildAt` method, which removes the child token at + the specified child token index. +* New element and attribute queries. + * Added the `Element.Index` method, which returns the element's index within + its parent element's child token list. + * Added the `Element.NamespaceURI` method to return the namespace URI + associated with an element. + * Added the `Attr.NamespaceURI` method to return the namespace URI + associated with an element. + * Added the `Attr.Element` method to return the element that an attribute + belongs to. +* New Path filter functions. + * Added `[local-name()='val']` to keep elements whose unprefixed tag matches + the desired value. + * Added `[name()='val']` to keep elements whose full tag matches the desired + value. + * Added `[namespace-prefix()='val']` to keep elements whose namespace prefix + matches the desired value. + * Added `[namespace-uri()='val']` to keep elements whose namespace URI + matches the desired value. + +**Bug Fixes** + +* A default XML `CharSetReader` is now used to prevent failed parsing of XML + documents using certain encodings. + ([Issue](https://github.com/beevik/etree/issues/53)). +* All characters are now properly escaped according to XML parsing rules. + ([Issue](https://github.com/beevik/etree/issues/55)). +* The `Document.Indent` and `Document.IndentTabs` functions no longer insert + empty string `CharData` tokens. + +**Deprecated** + +* `Element` + * The `InsertChild` method is deprecated. Use `InsertChildAt` instead. + * The `CreateCharData` method is deprecated. Use `CreateText` instead. +* `CharData` + * The `NewCharData` method is deprecated. Use `NewText` instead. + + +Release v1.0.1 +============== + +**Changes** + +* Added support for absolute etree Path queries. An absolute path begins with + `/` or `//` and begins its search from the element's document root. +* Added [`GetPath`](https://godoc.org/github.com/beevik/etree#Element.GetPath) + and [`GetRelativePath`](https://godoc.org/github.com/beevik/etree#Element.GetRelativePath) + functions to the [`Element`](https://godoc.org/github.com/beevik/etree#Element) + type. + +**Breaking changes** + +* A path starting with `//` is now interpreted as an absolute path. + Previously, it was interpreted as a relative path starting from the element + whose + [`FindElement`](https://godoc.org/github.com/beevik/etree#Element.FindElement) + method was called. To remain compatible with this release, all paths + prefixed with `//` should be prefixed with `.//` when called from any + element other than the document's root. +* [**edit 2/1/2019**]: Minor releases should not contain breaking changes. + Even though this breaking change was very minor, it was a mistake to include + it in this minor release. In the future, all breaking changes will be + limited to major releases (e.g., version 2.0.0). + +Release v1.0.0 +============== + +Initial release. diff --git a/vendor/github.com/beevik/etree/etree.go b/vendor/github.com/beevik/etree/etree.go index 36b279f6003..9e24f901267 100644 --- a/vendor/github.com/beevik/etree/etree.go +++ b/vendor/github.com/beevik/etree/etree.go @@ -1,4 +1,4 @@ -// Copyright 2015 Brett Vickers. +// Copyright 2015-2019 Brett Vickers. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -13,6 +13,7 @@ import ( "errors" "io" "os" + "sort" "strings" ) @@ -33,11 +34,19 @@ type ReadSettings struct { // Permissive allows input containing common mistakes such as missing tags // or attribute values. Default: false. Permissive bool + + // Entity to be passed to standard xml.Decoder. Default: nil. + Entity map[string]string } // newReadSettings creates a default ReadSettings record. func newReadSettings() ReadSettings { - return ReadSettings{} + return ReadSettings{ + CharsetReader: func(label string, input io.Reader) (io.Reader, error) { + return input, nil + }, + Permissive: false, + } } // WriteSettings allow for changing the serialization behavior of the WriteTo* @@ -56,6 +65,11 @@ type WriteSettings struct { // attribute value characters &, < and ". If false, XML character // references are also produced for > and '. Default: false. CanonicalAttrVal bool + + // When outputting indented XML, use a carriage return and linefeed + // ("\r\n") as a new-line delimiter instead of just a linefeed ("\n"). + // This is useful on Windows-based systems. + UseCRLF bool } // newWriteSettings creates a default WriteSettings record. @@ -64,6 +78,7 @@ func newWriteSettings() WriteSettings { CanonicalEndTags: false, CanonicalText: false, CanonicalAttrVal: false, + UseCRLF: false, } } @@ -71,8 +86,10 @@ func newWriteSettings() WriteSettings { // Comment, Directive, or ProcInst. type Token interface { Parent() *Element + Index() int dup(parent *Element) Token setParent(parent *Element) + setIndex(index int) writeTo(w *bufio.Writer, s *WriteSettings) } @@ -88,35 +105,52 @@ type Document struct { // An Element represents an XML element, its attributes, and its child tokens. type Element struct { - Space, Tag string // namespace and tag + Space, Tag string // namespace prefix and tag Attr []Attr // key-value attribute pairs Child []Token // child tokens (elements, comments, etc.) parent *Element // parent element + index int // token index in parent's children } // An Attr represents a key-value attribute of an XML element. type Attr struct { - Space, Key string // The attribute's namespace and key - Value string // The attribute value string + Space, Key string // The attribute's namespace prefix and key + Value string // The attribute value string + element *Element // element containing the attribute } -// CharData represents character data within XML. +// charDataFlags are used with CharData tokens to store additional settings. +type charDataFlags uint8 + +const ( + // The CharData was created by an indent function as whitespace. + whitespaceFlag charDataFlags = 1 << iota + + // The CharData contains a CDATA section. + cdataFlag +) + +// CharData can be used to represent character data or a CDATA section within +// an XML document. type CharData struct { - Data string - parent *Element - whitespace bool + Data string + parent *Element + index int + flags charDataFlags } // A Comment represents an XML comment. type Comment struct { Data string parent *Element + index int } // A Directive represents an XML directive. type Directive struct { Data string parent *Element + index int } // A ProcInst represents an XML processing instruction. @@ -124,6 +158,7 @@ type ProcInst struct { Target string Inst string parent *Element + index int } // NewDocument creates an XML document without a root element. @@ -160,16 +195,23 @@ func (d *Document) SetRoot(e *Element) { if e.parent != nil { e.parent.RemoveChild(e) } - e.setParent(&d.Element) - for i, t := range d.Child { + p := &d.Element + e.setParent(p) + + // If there is already a root element, replace it. + for i, t := range p.Child { if _, ok := t.(*Element); ok { t.setParent(nil) - d.Child[i] = e + t.setIndex(-1) + p.Child[i] = e + e.setIndex(i) return } } - d.Child = append(d.Child, e) + + // No existing root element, so add it. + p.addChild(e) } // ReadFrom reads XML from the reader r into the document d. It returns the @@ -246,8 +288,8 @@ func (d *Document) WriteToString() (s string, err error) { type indentFunc func(depth int) string -// Indent modifies the document's element tree by inserting CharData entities -// containing carriage returns and indentation. The amount of indentation per +// Indent modifies the document's element tree by inserting character data +// tokens containing newlines and indentation. The amount of indentation per // depth level is given as spaces. Pass etree.NoIndent for spaces if you want // no indentation at all. func (d *Document) Indent(spaces int) { @@ -255,22 +297,30 @@ func (d *Document) Indent(spaces int) { switch { case spaces < 0: indent = func(depth int) string { return "" } + case d.WriteSettings.UseCRLF == true: + indent = func(depth int) string { return indentCRLF(depth*spaces, indentSpaces) } default: - indent = func(depth int) string { return crIndent(depth*spaces, crsp) } + indent = func(depth int) string { return indentLF(depth*spaces, indentSpaces) } } d.Element.indent(0, indent) } // IndentTabs modifies the document's element tree by inserting CharData -// entities containing carriage returns and tabs for indentation. One tab is -// used per indentation level. +// tokens containing newlines and tabs for indentation. One tab is used per +// indentation level. func (d *Document) IndentTabs() { - indent := func(depth int) string { return crIndent(depth, crtab) } + var indent indentFunc + switch d.WriteSettings.UseCRLF { + case true: + indent = func(depth int) string { return indentCRLF(depth, indentTabs) } + default: + indent = func(depth int) string { return indentLF(depth, indentTabs) } + } d.Element.indent(0, indent) } // NewElement creates an unparented element with the specified tag. The tag -// may be prefixed by a namespace and a colon. +// may be prefixed by a namespace prefix and a colon. func NewElement(tag string) *Element { space, stag := spaceDecompose(tag) return newElement(space, stag, nil) @@ -285,6 +335,7 @@ func newElement(space, tag string, parent *Element) *Element { Attr: make([]Attr, 0), Child: make([]Token, 0), parent: parent, + index: -1, } if parent != nil { parent.addChild(e) @@ -296,38 +347,210 @@ func newElement(space, tag string, parent *Element) *Element { // and children. The returned element has no parent but can be parented to a // another element using AddElement, or to a document using SetRoot. func (e *Element) Copy() *Element { - var parent *Element - return e.dup(parent).(*Element) + return e.dup(nil).(*Element) } -// Text returns the characters immediately following the element's -// opening tag. +// FullTag returns the element e's complete tag, including namespace prefix if +// present. +func (e *Element) FullTag() string { + if e.Space == "" { + return e.Tag + } + return e.Space + ":" + e.Tag +} + +// NamespaceURI returns the XML namespace URI associated with the element. If +// the element is part of the XML default namespace, NamespaceURI returns the +// empty string. +func (e *Element) NamespaceURI() string { + if e.Space == "" { + return e.findDefaultNamespaceURI() + } + return e.findLocalNamespaceURI(e.Space) +} + +// findLocalNamespaceURI finds the namespace URI corresponding to the +// requested prefix. +func (e *Element) findLocalNamespaceURI(prefix string) string { + for _, a := range e.Attr { + if a.Space == "xmlns" && a.Key == prefix { + return a.Value + } + } + + if e.parent == nil { + return "" + } + + return e.parent.findLocalNamespaceURI(prefix) +} + +// findDefaultNamespaceURI finds the default namespace URI of the element. +func (e *Element) findDefaultNamespaceURI() string { + for _, a := range e.Attr { + if a.Space == "" && a.Key == "xmlns" { + return a.Value + } + } + + if e.parent == nil { + return "" + } + + return e.parent.findDefaultNamespaceURI() +} + +// hasText returns true if the element has character data immediately +// folllowing the element's opening tag. +func (e *Element) hasText() bool { + if len(e.Child) == 0 { + return false + } + _, ok := e.Child[0].(*CharData) + return ok +} + +// namespacePrefix returns the namespace prefix associated with the element. +func (e *Element) namespacePrefix() string { + return e.Space +} + +// name returns the tag associated with the element. +func (e *Element) name() string { + return e.Tag +} + +// Text returns all character data immediately following the element's opening +// tag. func (e *Element) Text() string { if len(e.Child) == 0 { return "" } - if cd, ok := e.Child[0].(*CharData); ok { - return cd.Data - } - return "" -} -// SetText replaces an element's subsidiary CharData text with a new string. -func (e *Element) SetText(text string) { - if len(e.Child) > 0 { - if cd, ok := e.Child[0].(*CharData); ok { - cd.Data = text - return + text := "" + for _, ch := range e.Child { + if cd, ok := ch.(*CharData); ok { + if text == "" { + text = cd.Data + } else { + text = text + cd.Data + } + } else { + break } } - cd := newCharData(text, false, e) - copy(e.Child[1:], e.Child[0:]) - e.Child[0] = cd + return text +} + +// SetText replaces all character data immediately following an element's +// opening tag with the requested string. +func (e *Element) SetText(text string) { + e.replaceText(0, text, 0) +} + +// SetCData replaces all character data immediately following an element's +// opening tag with a CDATA section. +func (e *Element) SetCData(text string) { + e.replaceText(0, text, cdataFlag) +} + +// Tail returns all character data immediately following the element's end +// tag. +func (e *Element) Tail() string { + if e.Parent() == nil { + return "" + } + + p := e.Parent() + i := e.Index() + + text := "" + for _, ch := range p.Child[i+1:] { + if cd, ok := ch.(*CharData); ok { + if text == "" { + text = cd.Data + } else { + text = text + cd.Data + } + } else { + break + } + } + return text +} + +// SetTail replaces all character data immediately following the element's end +// tag with the requested string. +func (e *Element) SetTail(text string) { + if e.Parent() == nil { + return + } + + p := e.Parent() + p.replaceText(e.Index()+1, text, 0) +} + +// replaceText is a helper function that replaces a series of chardata tokens +// starting at index i with the requested text. +func (e *Element) replaceText(i int, text string, flags charDataFlags) { + end := e.findTermCharDataIndex(i) + + switch { + case end == i: + if text != "" { + // insert a new chardata token at index i + cd := newCharData(text, flags, nil) + e.InsertChildAt(i, cd) + } + + case end == i+1: + if text == "" { + // remove the chardata token at index i + e.RemoveChildAt(i) + } else { + // replace the first and only character token at index i + cd := e.Child[i].(*CharData) + cd.Data, cd.flags = text, flags + } + + default: + if text == "" { + // remove all chardata tokens starting from index i + copy(e.Child[i:], e.Child[end:]) + removed := end - i + e.Child = e.Child[:len(e.Child)-removed] + for j := i; j < len(e.Child); j++ { + e.Child[j].setIndex(j) + } + } else { + // replace the first chardata token at index i and remove all + // subsequent chardata tokens + cd := e.Child[i].(*CharData) + cd.Data, cd.flags = text, flags + copy(e.Child[i+1:], e.Child[end:]) + removed := end - (i + 1) + e.Child = e.Child[:len(e.Child)-removed] + for j := i + 1; j < len(e.Child); j++ { + e.Child[j].setIndex(j) + } + } + } +} + +// findTermCharDataIndex finds the index of the first child token that isn't +// a CharData token. It starts from the requested start index. +func (e *Element) findTermCharDataIndex(start int) int { + for i := start; i < len(e.Child); i++ { + if _, ok := e.Child[i].(*CharData); !ok { + return i + } + } + return len(e.Child) } // CreateElement creates an element with the specified tag and adds it as the // last child element of the element e. The tag may be prefixed by a namespace -// and a colon. +// prefix and a colon. func (e *Element) CreateElement(tag string) *Element { space, stag := spaceDecompose(tag) return newElement(space, stag, e) @@ -340,43 +563,93 @@ func (e *Element) AddChild(t Token) { if t.Parent() != nil { t.Parent().RemoveChild(t) } + t.setParent(e) e.addChild(t) } // InsertChild inserts the token t before e's existing child token ex. If ex -// is nil (or if ex is not a child of e), then t is added to the end of e's -// child token list. If token t was already the child of another element, it -// is first removed from its current parent element. +// is nil or ex is not a child of e, then t is added to the end of e's child +// token list. If token t was already the child of another element, it is +// first removed from its current parent element. +// +// Deprecated: InsertChild is deprecated. Use InsertChildAt instead. func (e *Element) InsertChild(ex Token, t Token) { + if ex == nil || ex.Parent() != e { + e.AddChild(t) + return + } + if t.Parent() != nil { t.Parent().RemoveChild(t) } + t.setParent(e) - for i, c := range e.Child { - if c == ex { - e.Child = append(e.Child, nil) - copy(e.Child[i+1:], e.Child[i:]) - e.Child[i] = t - return + i := ex.Index() + e.Child = append(e.Child, nil) + copy(e.Child[i+1:], e.Child[i:]) + e.Child[i] = t + + for j := i; j < len(e.Child); j++ { + e.Child[j].setIndex(j) + } +} + +// InsertChildAt inserts the token t into the element e's list of child tokens +// just before the requested index. If the index is greater than or equal to +// the length of the list of child tokens, the token t is added to the end of +// the list. +func (e *Element) InsertChildAt(index int, t Token) { + if index >= len(e.Child) { + e.AddChild(t) + return + } + + if t.Parent() != nil { + if t.Parent() == e && t.Index() > index { + index-- } + t.Parent().RemoveChild(t) + } + + t.setParent(e) + + e.Child = append(e.Child, nil) + copy(e.Child[index+1:], e.Child[index:]) + e.Child[index] = t + + for j := index; j < len(e.Child); j++ { + e.Child[j].setIndex(j) } - e.addChild(t) } // RemoveChild attempts to remove the token t from element e's list of // children. If the token t is a child of e, then it is returned. Otherwise, // nil is returned. func (e *Element) RemoveChild(t Token) Token { - for i, c := range e.Child { - if c == t { - e.Child = append(e.Child[:i], e.Child[i+1:]...) - c.setParent(nil) - return t - } + if t.Parent() != e { + return nil } - return nil + return e.RemoveChildAt(t.Index()) +} + +// RemoveChildAt removes the index-th child token from the element e. The +// removed child token is returned. If the index is out of bounds, no child is +// removed and nil is returned. +func (e *Element) RemoveChildAt(index int) Token { + if index >= len(e.Child) { + return nil + } + + t := e.Child[index] + for j := index + 1; j < len(e.Child); j++ { + e.Child[j].setIndex(j - 1) + } + e.Child = append(e.Child[:index], e.Child[index+1:]...) + t.setIndex(-1) + t.setParent(nil) + return t } // ReadFrom reads XML from the reader r and stores the result as a new child @@ -386,6 +659,7 @@ func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err er dec := xml.NewDecoder(r) dec.CharsetReader = settings.CharsetReader dec.Strict = !settings.Permissive + dec.Entity = settings.Entity var stack stack stack.push(e) for { @@ -405,14 +679,18 @@ func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err er case xml.StartElement: e := newElement(t.Name.Space, t.Name.Local, top) for _, a := range t.Attr { - e.createAttr(a.Name.Space, a.Name.Local, a.Value) + e.createAttr(a.Name.Space, a.Name.Local, a.Value, e) } stack.push(e) case xml.EndElement: stack.pop() case xml.CharData: data := string(t) - newCharData(data, isWhitespace(data), top) + var flags charDataFlags + if isWhitespace(data) { + flags = whitespaceFlag + } + newCharData(data, flags, top) case xml.Comment: newComment(string(t), top) case xml.Directive: @@ -424,7 +702,8 @@ func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err er } // SelectAttr finds an element attribute matching the requested key and -// returns it if found. The key may be prefixed by a namespace and a colon. +// returns it if found. Returns nil if no matching attribute is found. The key +// may be prefixed by a namespace prefix and a colon. func (e *Element) SelectAttr(key string) *Attr { space, skey := spaceDecompose(key) for i, a := range e.Attr { @@ -436,8 +715,8 @@ func (e *Element) SelectAttr(key string) *Attr { } // SelectAttrValue finds an element attribute matching the requested key and -// returns its value if found. The key may be prefixed by a namespace and a -// colon. If the key is not found, the dflt value is returned instead. +// returns its value if found. The key may be prefixed by a namespace prefix +// and a colon. If the key is not found, the dflt value is returned instead. func (e *Element) SelectAttrValue(key, dflt string) string { space, skey := spaceDecompose(key) for _, a := range e.Attr { @@ -460,7 +739,8 @@ func (e *Element) ChildElements() []*Element { } // SelectElement returns the first child element with the given tag. The tag -// may be prefixed by a namespace and a colon. +// may be prefixed by a namespace prefix and a colon. Returns nil if no +// element with a matching tag was found. func (e *Element) SelectElement(tag string) *Element { space, stag := spaceDecompose(tag) for _, t := range e.Child { @@ -472,7 +752,7 @@ func (e *Element) SelectElement(tag string) *Element { } // SelectElements returns a slice of all child elements with the given tag. -// The tag may be prefixed by a namespace and a colon. +// The tag may be prefixed by a namespace prefix and a colon. func (e *Element) SelectElements(tag string) []*Element { space, stag := spaceDecompose(tag) var elements []*Element @@ -485,13 +765,14 @@ func (e *Element) SelectElements(tag string) []*Element { } // FindElement returns the first element matched by the XPath-like path -// string. Panics if an invalid path string is supplied. +// string. Returns nil if no element is found using the path. Panics if an +// invalid path string is supplied. func (e *Element) FindElement(path string) *Element { return e.FindElementPath(MustCompilePath(path)) } // FindElementPath returns the first element matched by the XPath-like path -// string. +// string. Returns nil if no element is found using the path. func (e *Element) FindElementPath(path Path) *Element { p := newPather() elements := p.traverse(e, path) @@ -515,6 +796,94 @@ func (e *Element) FindElementsPath(path Path) []*Element { return p.traverse(e, path) } +// GetPath returns the absolute path of the element. +func (e *Element) GetPath() string { + path := []string{} + for seg := e; seg != nil; seg = seg.Parent() { + if seg.Tag != "" { + path = append(path, seg.Tag) + } + } + + // Reverse the path. + for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 { + path[i], path[j] = path[j], path[i] + } + + return "/" + strings.Join(path, "/") +} + +// GetRelativePath returns the path of the element relative to the source +// element. If the two elements are not part of the same element tree, then +// GetRelativePath returns the empty string. +func (e *Element) GetRelativePath(source *Element) string { + var path []*Element + + if source == nil { + return "" + } + + // Build a reverse path from the element toward the root. Stop if the + // source element is encountered. + var seg *Element + for seg = e; seg != nil && seg != source; seg = seg.Parent() { + path = append(path, seg) + } + + // If we found the source element, reverse the path and compose the + // string. + if seg == source { + if len(path) == 0 { + return "." + } + parts := []string{} + for i := len(path) - 1; i >= 0; i-- { + parts = append(parts, path[i].Tag) + } + return "./" + strings.Join(parts, "/") + } + + // The source wasn't encountered, so climb from the source element toward + // the root of the tree until an element in the reversed path is + // encountered. + + findPathIndex := func(e *Element, path []*Element) int { + for i, ee := range path { + if e == ee { + return i + } + } + return -1 + } + + climb := 0 + for seg = source; seg != nil; seg = seg.Parent() { + i := findPathIndex(seg, path) + if i >= 0 { + path = path[:i] // truncate at found segment + break + } + climb++ + } + + // No element in the reversed path was encountered, so the two elements + // must not be part of the same tree. + if seg == nil { + return "" + } + + // Reverse the (possibly truncated) path and prepend ".." segments to + // climb. + parts := []string{} + for i := 0; i < climb; i++ { + parts = append(parts, "..") + } + for i := len(path) - 1; i >= 0; i-- { + parts = append(parts, path[i].Tag) + } + return strings.Join(parts, "/") +} + // indent recursively inserts proper indentation between an // XML element's child tokens. func (e *Element) indent(depth int, indent indentFunc) { @@ -528,14 +897,16 @@ func (e *Element) indent(depth int, indent indentFunc) { e.Child = make([]Token, 0, n*2+1) isCharData, firstNonCharData := false, true for _, c := range oldChild { - - // Insert CR+indent before child if it's not character data. + // Insert NL+indent before child if it's not character data. // Exceptions: when it's the first non-character-data child, or when // the child is at root depth. _, isCharData = c.(*CharData) if !isCharData { if !firstNonCharData || depth > 0 { - newCharData(indent(depth), true, e) + s := indent(depth) + if s != "" { + newCharData(s, whitespaceFlag, e) + } } firstNonCharData = false } @@ -548,10 +919,13 @@ func (e *Element) indent(depth int, indent indentFunc) { } } - // Insert CR+indent before the last child. + // Insert NL+indent before the last child. if !isCharData { if !firstNonCharData || depth > 0 { - newCharData(indent(depth-1), true, e) + s := indent(depth - 1) + if s != "" { + newCharData(s, whitespaceFlag, e) + } } } } @@ -561,7 +935,7 @@ func (e *Element) stripIndent() { // Count the number of non-indent child tokens n := len(e.Child) for _, c := range e.Child { - if cd, ok := c.(*CharData); ok && cd.whitespace { + if cd, ok := c.(*CharData); ok && cd.IsWhitespace() { n-- } } @@ -573,10 +947,11 @@ func (e *Element) stripIndent() { newChild := make([]Token, n) j := 0 for _, c := range e.Child { - if cd, ok := c.(*CharData); ok && cd.whitespace { + if cd, ok := c.(*CharData); ok && cd.IsWhitespace() { continue } newChild[j] = c + newChild[j].setIndex(j) j++ } e.Child = newChild @@ -590,6 +965,7 @@ func (e *Element) dup(parent *Element) Token { Attr: make([]Attr, len(e.Attr)), Child: make([]Token, len(e.Child)), parent: parent, + index: e.index, } for i, t := range e.Child { ne.Child[i] = t.dup(ne) @@ -606,19 +982,27 @@ func (e *Element) Parent() *Element { return e.parent } +// Index returns the index of this element within its parent element's +// list of child tokens. If this element has no parent element, the index +// is -1. +func (e *Element) Index() int { + return e.index +} + // setParent replaces the element token's parent. func (e *Element) setParent(parent *Element) { e.parent = parent } +// setIndex sets the element token's index within its parent's Child slice. +func (e *Element) setIndex(index int) { + e.index = index +} + // writeTo serializes the element to the writer w. func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { w.WriteByte('<') - if e.Space != "" { - w.WriteString(e.Space) - w.WriteByte(':') - } - w.WriteString(e.Tag) + w.WriteString(e.FullTag()) for _, a := range e.Attr { w.WriteByte(' ') a.writeTo(w, s) @@ -629,20 +1013,12 @@ func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { c.writeTo(w, s) } w.Write([]byte{'<', '/'}) - if e.Space != "" { - w.WriteString(e.Space) - w.WriteByte(':') - } - w.WriteString(e.Tag) + w.WriteString(e.FullTag()) w.WriteByte('>') } else { if s.CanonicalEndTags { w.Write([]byte{'>', '<', '/'}) - if e.Space != "" { - w.WriteString(e.Space) - w.WriteByte(':') - } - w.WriteString(e.Tag) + w.WriteString(e.FullTag()) w.WriteByte('>') } else { w.Write([]byte{'/', '>'}) @@ -652,98 +1028,140 @@ func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { // addChild adds a child token to the element e. func (e *Element) addChild(t Token) { + t.setIndex(len(e.Child)) e.Child = append(e.Child, t) } // CreateAttr creates an attribute and adds it to element e. The key may be -// prefixed by a namespace and a colon. If an attribute with the key already -// exists, its value is replaced. +// prefixed by a namespace prefix and a colon. If an attribute with the key +// already exists, its value is replaced. func (e *Element) CreateAttr(key, value string) *Attr { space, skey := spaceDecompose(key) - return e.createAttr(space, skey, value) + return e.createAttr(space, skey, value, e) } // createAttr is a helper function that creates attributes. -func (e *Element) createAttr(space, key, value string) *Attr { +func (e *Element) createAttr(space, key, value string, parent *Element) *Attr { for i, a := range e.Attr { if space == a.Space && key == a.Key { e.Attr[i].Value = value return &e.Attr[i] } } - a := Attr{space, key, value} + a := Attr{ + Space: space, + Key: key, + Value: value, + element: parent, + } e.Attr = append(e.Attr, a) return &e.Attr[len(e.Attr)-1] } -// RemoveAttr removes and returns the first attribute of the element whose key -// matches the given key. The key may be prefixed by a namespace and a colon. -// If an equal attribute does not exist, nil is returned. +// RemoveAttr removes and returns a copy of the first attribute of the element +// whose key matches the given key. The key may be prefixed by a namespace +// prefix and a colon. If a matching attribute does not exist, nil is +// returned. func (e *Element) RemoveAttr(key string) *Attr { space, skey := spaceDecompose(key) for i, a := range e.Attr { if space == a.Space && skey == a.Key { e.Attr = append(e.Attr[0:i], e.Attr[i+1:]...) - return &a + return &Attr{ + Space: a.Space, + Key: a.Key, + Value: a.Value, + element: nil, + } } } return nil } -var xmlReplacerNormal = strings.NewReplacer( - "&", "&", - "<", "<", - ">", ">", - "'", "'", - `"`, """, -) +// SortAttrs sorts the element's attributes lexicographically by key. +func (e *Element) SortAttrs() { + sort.Sort(byAttr(e.Attr)) +} -var xmlReplacerCanonicalText = strings.NewReplacer( - "&", "&", - "<", "<", - ">", ">", - "\r", " ", -) +type byAttr []Attr -var xmlReplacerCanonicalAttrVal = strings.NewReplacer( - "&", "&", - "<", "<", - `"`, """, - "\t", " ", - "\n", " ", - "\r", " ", -) +func (a byAttr) Len() int { + return len(a) +} + +func (a byAttr) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a byAttr) Less(i, j int) bool { + sp := strings.Compare(a[i].Space, a[j].Space) + if sp == 0 { + return strings.Compare(a[i].Key, a[j].Key) < 0 + } + return sp < 0 +} + +// FullKey returns the attribute a's complete key, including namespace prefix +// if present. +func (a *Attr) FullKey() string { + if a.Space == "" { + return a.Key + } + return a.Space + ":" + a.Key +} + +// Element returns the element containing the attribute. +func (a *Attr) Element() *Element { + return a.element +} + +// NamespaceURI returns the XML namespace URI associated with the attribute. +// If the element is part of the XML default namespace, NamespaceURI returns +// the empty string. +func (a *Attr) NamespaceURI() string { + return a.element.NamespaceURI() +} // writeTo serializes the attribute to the writer. func (a *Attr) writeTo(w *bufio.Writer, s *WriteSettings) { - if a.Space != "" { - w.WriteString(a.Space) - w.WriteByte(':') - } - w.WriteString(a.Key) + w.WriteString(a.FullKey()) w.WriteString(`="`) - var r *strings.Replacer + var m escapeMode if s.CanonicalAttrVal { - r = xmlReplacerCanonicalAttrVal + m = escapeCanonicalAttr } else { - r = xmlReplacerNormal + m = escapeNormal } - w.WriteString(r.Replace(a.Value)) + escapeString(w, a.Value, m) w.WriteByte('"') } -// NewCharData creates a parentless XML character data entity. -func NewCharData(data string) *CharData { - return newCharData(data, false, nil) +// NewText creates a parentless CharData token containing character data. +func NewText(text string) *CharData { + return newCharData(text, 0, nil) } -// newCharData creates an XML character data entity and binds it to a parent +// NewCData creates a parentless XML character CDATA section. +func NewCData(data string) *CharData { + return newCharData(data, cdataFlag, nil) +} + +// NewCharData creates a parentless CharData token containing character data. +// +// Deprecated: NewCharData is deprecated. Instead, use NewText, which does the +// same thing. +func NewCharData(data string) *CharData { + return newCharData(data, 0, nil) +} + +// newCharData creates a character data token and binds it to a parent // element. If parent is nil, the CharData token remains unbound. -func newCharData(data string, whitespace bool, parent *Element) *CharData { +func newCharData(data string, flags charDataFlags, parent *Element) *CharData { c := &CharData{ - Data: data, - whitespace: whitespace, - parent: parent, + Data: data, + parent: parent, + index: -1, + flags: flags, } if parent != nil { parent.addChild(c) @@ -751,41 +1169,88 @@ func newCharData(data string, whitespace bool, parent *Element) *CharData { return c } -// CreateCharData creates an XML character data entity and adds it as a child -// of element e. +// CreateText creates a CharData token containing character data and adds it +// as a child of element e. +func (e *Element) CreateText(text string) *CharData { + return newCharData(text, 0, e) +} + +// CreateCData creates a CharData token containing a CDATA section and adds it +// as a child of element e. +func (e *Element) CreateCData(data string) *CharData { + return newCharData(data, cdataFlag, e) +} + +// CreateCharData creates a CharData token containing character data and adds +// it as a child of element e. +// +// Deprecated: CreateCharData is deprecated. Instead, use CreateText, which +// does the same thing. func (e *Element) CreateCharData(data string) *CharData { - return newCharData(data, false, e) + return newCharData(data, 0, e) } // dup duplicates the character data. func (c *CharData) dup(parent *Element) Token { return &CharData{ - Data: c.Data, - whitespace: c.whitespace, - parent: parent, + Data: c.Data, + flags: c.flags, + parent: parent, + index: c.index, } } +// IsCData returns true if the character data token is to be encoded as a +// CDATA section. +func (c *CharData) IsCData() bool { + return (c.flags & cdataFlag) != 0 +} + +// IsWhitespace returns true if the character data token was created by one of +// the document Indent methods to contain only whitespace. +func (c *CharData) IsWhitespace() bool { + return (c.flags & whitespaceFlag) != 0 +} + // Parent returns the character data token's parent element, or nil if it has // no parent. func (c *CharData) Parent() *Element { return c.parent } +// Index returns the index of this CharData token within its parent element's +// list of child tokens. If this CharData token has no parent element, the +// index is -1. +func (c *CharData) Index() int { + return c.index +} + // setParent replaces the character data token's parent. func (c *CharData) setParent(parent *Element) { c.parent = parent } -// writeTo serializes the character data entity to the writer. +// setIndex sets the CharData token's index within its parent element's Child +// slice. +func (c *CharData) setIndex(index int) { + c.index = index +} + +// writeTo serializes character data to the writer. func (c *CharData) writeTo(w *bufio.Writer, s *WriteSettings) { - var r *strings.Replacer - if s.CanonicalText { - r = xmlReplacerCanonicalText + if c.IsCData() { + w.WriteString(``) } else { - r = xmlReplacerNormal + var m escapeMode + if s.CanonicalText { + m = escapeCanonicalText + } else { + m = escapeNormal + } + escapeString(w, c.Data, m) } - w.WriteString(r.Replace(c.Data)) } // NewComment creates a parentless XML comment. @@ -799,6 +1264,7 @@ func newComment(comment string, parent *Element) *Comment { c := &Comment{ Data: comment, parent: parent, + index: -1, } if parent != nil { parent.addChild(c) @@ -816,6 +1282,7 @@ func (c *Comment) dup(parent *Element) Token { return &Comment{ Data: c.Data, parent: parent, + index: c.index, } } @@ -824,11 +1291,24 @@ func (c *Comment) Parent() *Element { return c.parent } +// Index returns the index of this Comment token within its parent element's +// list of child tokens. If this Comment token has no parent element, the +// index is -1. +func (c *Comment) Index() int { + return c.index +} + // setParent replaces the comment token's parent. func (c *Comment) setParent(parent *Element) { c.parent = parent } +// setIndex sets the Comment token's index within its parent element's Child +// slice. +func (c *Comment) setIndex(index int) { + c.index = index +} + // writeTo serialies the comment to the writer. func (c *Comment) writeTo(w *bufio.Writer, s *WriteSettings) { w.WriteString("