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
## [Unreleased]
## [0.0.2] - 2019-04-28
### Added
- Changelog
- `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
Initial development release

5
go.mod
View File

@ -1,6 +1,3 @@
module gitlab.codemonkeysoftware.net/b/hatmill
require (
github.com/golang/mock v1.2.0 // indirect
github.com/leanovate/gopter v0.2.4
)
require 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/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=

View File

@ -1,7 +1,11 @@
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
import "io"
import (
"bytes"
"html"
"io"
)
// Term represents a fragment of HTML markup, and is one of VoidElement, ParentElement, Terms, or Text.
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
// 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.
func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
err = writeStringsTo(w, &n, a.Key)
@ -40,7 +45,8 @@ func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
return
}
if a.Value != "" {
err = writeStringsTo(w, &n, "='", a.Value, "'")
escaped := html.EscapeString(a.Value)
err = writeStringsTo(w, &n, "='", escaped, "'")
}
return
}
@ -56,8 +62,6 @@ func (VoidElement) isHtml() {}
// WriteTo writes the HTML markup represented by e to w, returning the number
// 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) {
err = writeStringsTo(w, &n, "<", e.TagName)
if err != nil {
@ -92,8 +96,6 @@ func (e ParentElement) isHtml() {}
// WriteTo writes the HTML markup represented by e to w, returning the number
// 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) {
n, err = e.VoidElement.WriteTo(w)
if err != nil {
@ -116,11 +118,50 @@ type Text string
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
// and any error encountered. It does not replace special characters with HTML
// entities; use html.EscapeString for this purpose.
// and any error encountered. It replaces special characters with HTML entities.
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
}

View File

@ -9,6 +9,7 @@ import (
"reflect"
"strings"
"testing"
"unicode"
"github.com/leanovate/gopter"
"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
}
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) {
properties := gopter.NewProperties(nil)
properties.Property("WriteTo is correct", prop.ForAll(
properties.Property("WriteTo escapes special chars and outputs all others unchanged", prop.ForAll(
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)
}
var nonEmptyAlphaString = gen.AlphaString().SuchThat(func(v interface{}) bool {
func stringNotEmpty(v interface{}) bool {
return v.(string) != ""
})
}
var attribGen = gen.Struct(reflect.TypeOf(hatmill.Attrib{}), map[string]gopter.Gen{
"Key": nonEmptyAlphaString,
"Value": gen.AlphaString(),
})
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,
"Value": value,
})
}
func TestAttrib(t *testing.T) {
properties := gopter.NewProperties(nil)
@ -79,9 +111,15 @@ func TestAttrib(t *testing.T) {
expected := fmt.Sprintf("%s='%s'", attrib.Key, attrib.Value)
return checkWrite(attrib, expected)
},
attribGen.SuchThat(func(attrib interface{}) bool {
return attrib.(hatmill.Attrib).Value != ""
}),
attribGen(nonEmptyAlphaString),
))
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)
@ -120,7 +158,7 @@ func TestEmptyElement(t *testing.T) {
return checkWrite(elem, expected)
},
gen.AnyString(),
gen.SliceOf(attribGen),
gen.SliceOf(attribGen(gen.AlphaString())),
))
properties.TestingRun(t)
@ -147,7 +185,7 @@ func TestParentElement(t *testing.T) {
return checkWrite(elem, expected)
},
gen.AnyString(),
gen.SliceOf(attribGen),
gen.SliceOf(attribGen(gen.AlphaString())),
))
properties.Property("WriteTo writes element correctly with children", prop.ForAll(
@ -171,7 +209,7 @@ func TestParentElement(t *testing.T) {
return checkWrite(elem, expected)
},
gen.AnyString(),
gen.SliceOf(attribGen),
gen.SliceOf(attribGen(gen.AlphaString())),
gen.SliceOf(htmlTextGen),
))
@ -199,7 +237,7 @@ func Example() {
document := he.Html()(
he.Body()(
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.Textarea(ha.Rows(25))(),
he.Meter(ha.Min(-1.3), ha.Max(5.5E12))(),