Escape entities in Text and Attrib

This commit is contained in:
Brandon Dyck 2019-04-28 22:03:28 -06:00
parent b3fd1d386b
commit c00d92595e
5 changed files with 113 additions and 35 deletions

View File

@ -1,11 +1,15 @@
# Changelog # Changelog
## [Unreleased] ## [0.0.2] - 2019-04-28
### Added ### Added
- Changelog - Changelog
- `hatmill.Terms` type for representing lists of nodes - `hatmill.Terms` type for representing lists of nodes
### Changed
- Attrib.WriteTo replaces special characters in Value with HTML entities.
- Text.WriteTo replaces special characters with HTML entities.
## [0.0.1] - 2019-04-28 ## [0.0.1] - 2019-04-28
Initial development release Initial development release

5
go.mod
View File

@ -1,6 +1,3 @@
module gitlab.codemonkeysoftware.net/b/hatmill module gitlab.codemonkeysoftware.net/b/hatmill
require ( require github.com/leanovate/gopter v0.2.4
github.com/golang/mock v1.2.0 // indirect
github.com/leanovate/gopter v0.2.4
)

2
go.sum
View File

@ -1,4 +1,2 @@
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/leanovate/gopter v0.2.4 h1:U4YLBggDFhJdqQsG4Na2zX7joVTky9vHaj/AGEwSuXU= github.com/leanovate/gopter v0.2.4 h1:U4YLBggDFhJdqQsG4Na2zX7joVTky9vHaj/AGEwSuXU=
github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=

View File

@ -1,7 +1,11 @@
package hatmill package hatmill
//go:generate go run internal/codegen/codegen.go -input defs.json -elemfile element/generated.go -elempkg element -attribfile attribute/generated.go -attribpkg attribute //go:generate go run internal/codegen/codegen.go -input defs.json -elemfile element/generated.go -elempkg element -attribfile attribute/generated.go -attribpkg attribute
import "io" import (
"bytes"
"html"
"io"
)
// Term represents a fragment of HTML markup, and is one of VoidElement, ParentElement, Terms, or Text. // Term represents a fragment of HTML markup, and is one of VoidElement, ParentElement, Terms, or Text.
type Term interface { type Term interface {
@ -32,7 +36,8 @@ type Attrib struct {
} }
// WriteTo writes a to w as an HTML attribute in the form key="value", or // WriteTo writes a to w as an HTML attribute in the form key="value", or
// simply key if value is empty. It returns the number of bytes written and any // simply key if value is empty. Special characters in value are replaced with
// HTML entities. It returns the number of bytes written and any
// error encountered. // error encountered.
func (a Attrib) WriteTo(w io.Writer) (n int64, err error) { func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
err = writeStringsTo(w, &n, a.Key) err = writeStringsTo(w, &n, a.Key)
@ -40,7 +45,8 @@ func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
return return
} }
if a.Value != "" { if a.Value != "" {
err = writeStringsTo(w, &n, "='", a.Value, "'") escaped := html.EscapeString(a.Value)
err = writeStringsTo(w, &n, "='", escaped, "'")
} }
return return
} }
@ -56,8 +62,6 @@ func (VoidElement) isHtml() {}
// WriteTo writes the HTML markup represented by e to w, returning the number // WriteTo writes the HTML markup represented by e to w, returning the number
// of bytes written and any error encountered. // of bytes written and any error encountered.
//
// See the warning about sanitization in the (Attrib).WriteTo documentation.
func (e VoidElement) WriteTo(w io.Writer) (n int64, err error) { func (e VoidElement) WriteTo(w io.Writer) (n int64, err error) {
err = writeStringsTo(w, &n, "<", e.TagName) err = writeStringsTo(w, &n, "<", e.TagName)
if err != nil { if err != nil {
@ -92,8 +96,6 @@ func (e ParentElement) isHtml() {}
// WriteTo writes the HTML markup represented by e to w, returning the number // WriteTo writes the HTML markup represented by e to w, returning the number
// of bytes written and any error encountered. // of bytes written and any error encountered.
//
// See the warning about sanitization in the (Attrib).WriteTo documentation.
func (e ParentElement) WriteTo(w io.Writer) (n int64, err error) { func (e ParentElement) WriteTo(w io.Writer) (n int64, err error) {
n, err = e.VoidElement.WriteTo(w) n, err = e.VoidElement.WriteTo(w)
if err != nil { if err != nil {
@ -116,11 +118,50 @@ type Text string
func (Text) isHtml() {} func (Text) isHtml() {}
var entities = map[byte][]byte{
'&': []byte("&amp;"),
'<': []byte("&lt;"),
'>': []byte("&gt;"),
'\'': []byte("&#39;"),
'"': []byte("&#34;"),
}
type escapedWriter struct {
io.Writer
}
func (w escapedWriter) Write(p []byte) (n int, err error) {
const specialChars = `&<>'"`
for len(p) > 0 {
var nCurr int
idx := bytes.IndexAny(p, specialChars)
if idx == -1 {
nCurr, err = w.Writer.Write(p)
n += nCurr
return
}
nCurr, err = w.Writer.Write(p[:idx])
n += nCurr
if err != nil {
return n, err
}
entity := entities[p[idx]]
nCurr, err = w.Writer.Write(entity)
n += nCurr
if err != nil {
return n, err
}
p = p[idx+1:]
}
return n, nil
}
// WriteTo writes the contents of t to w, returning the number of bytes written // WriteTo writes the contents of t to w, returning the number of bytes written
// and any error encountered. It does not replace special characters with HTML // and any error encountered. It replaces special characters with HTML entities.
// entities; use html.EscapeString for this purpose.
func (t Text) WriteTo(w io.Writer) (n int64, err error) { func (t Text) WriteTo(w io.Writer) (n int64, err error) {
err = writeStringsTo(w, &n, string(t)) err = writeStringsTo(escapedWriter{Writer: w}, &n, string(t))
return return
} }

View File

@ -9,6 +9,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"unicode"
"github.com/leanovate/gopter" "github.com/leanovate/gopter"
"github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/gen"
@ -39,27 +40,58 @@ func checkWrite(wt io.WriterTo, expected string) bool {
return err == nil && n == int64(buf.Len()) && buf.String() == expected return err == nil && n == int64(buf.Len()) && buf.String() == expected
} }
var ASCII = &unicode.RangeTable{
R16: []unicode.Range16{
{Lo: ' ', Hi: unicode.MaxASCII, Stride: 1},
},
LatinOffset: 1,
}
var dangerousASCII = gen.UnicodeString(ASCII).SuchThat(func(v interface{}) bool {
s := v.(string)
const specialChars = `&<>'"`
for _, c := range specialChars {
if strings.ContainsRune(s, rune(c)) {
return true
}
}
return false
})
func printableASCII() string {
const minPrintable rune = 32
const maxPrintable rune = 126
var buf bytes.Buffer
for r := minPrintable; r <= maxPrintable; r++ {
buf.WriteRune(r)
}
return buf.String()
}
func TestText(t *testing.T) { func TestText(t *testing.T) {
properties := gopter.NewProperties(nil) properties := gopter.NewProperties(nil)
properties.Property("WriteTo escapes special chars and outputs all others unchanged", prop.ForAll(
properties.Property("WriteTo is correct", prop.ForAll(
func(s string) bool { func(s string) bool {
return checkWrite(hatmill.Text(s), s) expected := html.EscapeString(s)
return checkWrite(hatmill.Text(s), expected)
}, },
gen.AnyString(), dangerousASCII,
)) ))
properties.TestingRun(t) properties.TestingRun(t)
} }
var nonEmptyAlphaString = gen.AlphaString().SuchThat(func(v interface{}) bool { func stringNotEmpty(v interface{}) bool {
return v.(string) != "" return v.(string) != ""
}) }
var attribGen = gen.Struct(reflect.TypeOf(hatmill.Attrib{}), map[string]gopter.Gen{ var nonEmptyAlphaString = gen.AlphaString().SuchThat(stringNotEmpty)
func attribGen(value gopter.Gen) gopter.Gen {
return gen.Struct(reflect.TypeOf(hatmill.Attrib{}), map[string]gopter.Gen{
"Key": nonEmptyAlphaString, "Key": nonEmptyAlphaString,
"Value": gen.AlphaString(), "Value": value,
}) })
}
func TestAttrib(t *testing.T) { func TestAttrib(t *testing.T) {
properties := gopter.NewProperties(nil) properties := gopter.NewProperties(nil)
@ -79,9 +111,15 @@ func TestAttrib(t *testing.T) {
expected := fmt.Sprintf("%s='%s'", attrib.Key, attrib.Value) expected := fmt.Sprintf("%s='%s'", attrib.Key, attrib.Value)
return checkWrite(attrib, expected) return checkWrite(attrib, expected)
}, },
attribGen.SuchThat(func(attrib interface{}) bool { attribGen(nonEmptyAlphaString),
return attrib.(hatmill.Attrib).Value != "" ))
}),
properties.Property("WriteTo escapes attribute value", prop.ForAll(
func(attrib hatmill.Attrib) bool {
expected := fmt.Sprintf("%s='%s'", attrib.Key, html.EscapeString(attrib.Value))
return checkWrite(attrib, expected)
},
attribGen(dangerousASCII.SuchThat(stringNotEmpty)),
)) ))
properties.TestingRun(t) properties.TestingRun(t)
@ -120,7 +158,7 @@ func TestEmptyElement(t *testing.T) {
return checkWrite(elem, expected) return checkWrite(elem, expected)
}, },
gen.AnyString(), gen.AnyString(),
gen.SliceOf(attribGen), gen.SliceOf(attribGen(gen.AlphaString())),
)) ))
properties.TestingRun(t) properties.TestingRun(t)
@ -147,7 +185,7 @@ func TestParentElement(t *testing.T) {
return checkWrite(elem, expected) return checkWrite(elem, expected)
}, },
gen.AnyString(), gen.AnyString(),
gen.SliceOf(attribGen), gen.SliceOf(attribGen(gen.AlphaString())),
)) ))
properties.Property("WriteTo writes element correctly with children", prop.ForAll( properties.Property("WriteTo writes element correctly with children", prop.ForAll(
@ -171,7 +209,7 @@ func TestParentElement(t *testing.T) {
return checkWrite(elem, expected) return checkWrite(elem, expected)
}, },
gen.AnyString(), gen.AnyString(),
gen.SliceOf(attribGen), gen.SliceOf(attribGen(gen.AlphaString())),
gen.SliceOf(htmlTextGen), gen.SliceOf(htmlTextGen),
)) ))
@ -199,7 +237,7 @@ func Example() {
document := he.Html()( document := he.Html()(
he.Body()( he.Body()(
he.Img(ha.Src("./photo.jpg"), ha.Contenteditable(true)), he.Img(ha.Src("./photo.jpg"), ha.Contenteditable(true)),
hatmill.Text(html.EscapeString(userInput)), hatmill.Text(userInput),
he.Div(ha.Disabled(), ha.CustomData("coolness", "awesome"))(), he.Div(ha.Disabled(), ha.CustomData("coolness", "awesome"))(),
he.Textarea(ha.Rows(25))(), he.Textarea(ha.Rows(25))(),
he.Meter(ha.Min(-1.3), ha.Max(5.5E12))(), he.Meter(ha.Min(-1.3), ha.Max(5.5E12))(),