exp/template/html: moved error docs out of package docs onto error codes
This replaces the errStr & errLine members of context with a single err
*Error, and introduces a number of const error codes, one per
escape-time failure mode, that can be separately documented.
The changes to the error documentation moved from doc.go to error.go
are cosmetic.
R=r, nigeltao
CC=golang-dev
https://golang.org/cl/5026041
diff --git a/src/pkg/exp/template/html/Makefile b/src/pkg/exp/template/html/Makefile
index e53270c..9032aea 100644
--- a/src/pkg/exp/template/html/Makefile
+++ b/src/pkg/exp/template/html/Makefile
@@ -11,6 +11,7 @@
context.go\
css.go\
doc.go\
+ error.go\
escape.go\
html.go\
js.go\
diff --git a/src/pkg/exp/template/html/context.go b/src/pkg/exp/template/html/context.go
index bfe168f..e8812cf 100644
--- a/src/pkg/exp/template/html/context.go
+++ b/src/pkg/exp/template/html/context.go
@@ -21,8 +21,7 @@
urlPart urlPart
jsCtx jsCtx
element element
- errLine int
- errStr string
+ err *Error
}
// eq returns whether two contexts are equal.
@@ -32,8 +31,7 @@
c.urlPart == d.urlPart &&
c.jsCtx == d.jsCtx &&
c.element == d.element &&
- c.errLine == d.errLine &&
- c.errStr == d.errStr
+ c.err == d.err
}
// mangle produces an identifier that includes a suffix that distinguishes it
diff --git a/src/pkg/exp/template/html/doc.go b/src/pkg/exp/template/html/doc.go
index 2751ce8..a9b78ca 100644
--- a/src/pkg/exp/template/html/doc.go
+++ b/src/pkg/exp/template/html/doc.go
@@ -69,178 +69,8 @@
Errors
-This section describes the errors returned by EscapeSet. Each error is
-illustrated by an example that triggers the error, followed by an explanation
-of the problem.
+See the documentation of ErrorCode for details.
-Error: "... appears in an ambiguous URL context"
-Example:
- <a href="
- {{if .C}}
- /path/
- {{else}}
- /search?q=
- {{end}}
- {{.X}}
- ">
-Discussion:
- {{.X}} is in an ambiguous URL context since, depending on {{.C}}, it may be
- either a URL suffix or a query parameter.
- Moving {{.X}} into the condition removes the ambiguity:
- <a href="{{if .C}}/path/{{.X}}{{else}}/search?q={{.X}}">
-
-
-Error: "... appears inside a comment"
-Example:
-*/
-// <!-- {{.X}} -->
-// <script>/* {{.X}} */</script>
-// <style>/* {{.X}} */</style>
-/*
-Discussion:
- {{.X}} appears inside a comment. There is no escaping convention for
- comments. To use IE conditional comments, inject the
- whole comment as a type string (see below).
- To comment out code, break the {{...}}.
-
-Error: "{{if}} branches end in different contexts"
-Example:
- {{if .C}}<a href="{{end}}{{.X}}
-Discussion:
- EscapeSet statically examines each possible path when it encounters a {{if}},
- {{range}}, or {{with}} to escape any following pipelines. The example is
- ambiguous since {{.X}} might be an HTML text node, or a URL prefix in an
- HTML attribute. EscapeSet needs to understand the context of {{.X}} to escape
- it, but that depends on the run-time value of {{.C}}.
-
- The problem is usually something like missing quotes or angle brackets, or
- can be avoided by refactoring to put the two contexts into different
- branches of an if, range or with. Adding an {{else}} might help.
-
- First, look for a bug in your template. Missing quotes or '>' can trigger
- this error.
-
- {{if .C}}<div ... class="foo>{{end}} <- No quote after foo
-
- Second, try refactoring your template.
-
- {{if .C}}<script>alert({{end}}{{.X}}{{if .C}})</script>{{end}}
-
- ->
-
- {{if .C}}<script>alert({{.X}})</script>{{else}}{{.X}}{{end}}
-
- Third, check for {{range}}s that have no {{else}}
-
- <a href="/search
- {{range $i, $v := .}}
- {{if $i}}&{{else}}?{{end}}
- v={{$v}}
- {{end}}
- &x={{.X}}
- ">
-
- looks good, but if {{.}} is empty then the URL is /search&x=...
- where {{.X}} is not guaranteed to be in a URL query.
- EscapeSet cannot prove which {{range}} collections are never non-empty, so
- add an {{else}}
-
- <a href="{{range ...}}...{{end}}&x={{X}}">
-
- ->
-
- <a href="{{range ...}}...{{else}}?{{end}}&x={{.X}}">
-
- Fourth, contact the mailing list. You may have a useful pattern that
- EscapeSet does not yet support, and we can work with you.
-
-
-Error: "... ends in a non-text context: ..."
-Examples:
- <div
- <div title="no close quote>
- <script>f()
-Discussion:
- EscapeSet assumes the ouput is a DocumentFragment of HTML.
- Templates that end without closing tags will trigger this warning.
- Templates that produce incomplete Fragments should not be named
- in the call to EscapeSet.
-
-
-If you have a helper template in your set that is not meant to produce a
- document fragment, then do not pass its name to EscapeSet(set, ...names).
-
- {{define "main"}} <script>{{template "helper"}}</script> {{end}}
- {{define "helper"}} document.write(' <div title=" ') {{end}}
-
- "helper" does not produce a valid document fragment, though it does
- produce a valid JavaScript Program.
-
-"must specify names of top level templates"
-
- EscapeSet does not assume that all templates in a set produce HTML.
- Some may be helpers that produce snippets of other languages.
- Passing in no template names is most likely an error, so EscapeSet(set) will
- panic.
- If you call EscapeSet with a slice of names, guard it with a len check:
-
- if len(names) != 0 {
- set, err := EscapeSet(set, ...names)
- }
-
-Error: "no such template ..."
-Examples:
- {{define "main"}}<div {{template "attrs"}}>{{end}}
- {{define "attrs"}}href="{{.URL}}"{{end}}
-Discussion:
- EscapeSet looks through template calls to compute the context.
- Here the {{.URL}} in "attrs" must be treated as a URL when called from "main",
- but if "attrs" is not in set when EscapeSet(&set, "main") is called, this
- error will arise.
-
-Error: "on range loop re-entry: ..."
-Example:
- {{range .}}<p class={{.}}{{end}}
-Discussion:
- If an iteration through a range would cause it to end in
- a different context than an earlier pass, there is no single context.
- In the example, the <p> tag is missing a '>'.
- EscapeSet cannot tell whether {{.}} is meant to be an HTML class or the
- content of a broken <p> element and complains because the second iteration
- would produce something like
-
- <p class=foo<p class=bar
-
-Error: "unfinished escape sequence in ..."
-Example:
- <script>alert("\{{.X}}")</script>
-Discussion:
- EscapeSet does not support actions following a backslash.
- This is usually an error and there are better solutions; for
- our example
- <script>alert("{{.X}}")</script>
- should work, and if {{.X}} is a partial escape sequence such as
- "xA0", give it the type ContentTypeJSStr and include the whole
- sequence, as in
- {`\xA0`, ContentTypeJSStr}
-
-Error: "unfinished JS regexp charset in ..."
-Example:
- <script>var pattern = /foo[{{.Chars}}]/</script>
-Discussion:
- EscapeSet does not support interpolation into regular expression literal
- character sets.
-
-Error: "ZgotmplZ"
-Example:
- <img src="{{.X}}">
- where {{.X}} evaluates to `javascript:...`
-Discussion:
- "ZgotmplZ" is a special value that indicates that unsafe content reached
- a CSS or URL context at runtime. The output of the example will be
- <img src="#ZgotmplZ">
- If the data can be trusted, giving the string type XXX will exempt
- it from filtering.
A fuller picture
@@ -249,8 +79,6 @@
will not need to understand these details.
-
-
Contexts
Assuming {{.}} is `O'Reilly: How are <i>you</i>?`, the table below shows
diff --git a/src/pkg/exp/template/html/error.go b/src/pkg/exp/template/html/error.go
new file mode 100644
index 0000000..5fa2357
--- /dev/null
+++ b/src/pkg/exp/template/html/error.go
@@ -0,0 +1,194 @@
+// Copyright 2011 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 html
+
+import (
+ "fmt"
+)
+
+// Error describes a problem encountered during template Escaping.
+type Error struct {
+ // ErrorCode describes the kind of error.
+ ErrorCode ErrorCode
+ // Name is the name of the template in which the error was encountered.
+ Name string
+ // Line is the line number of the error in the template source or 0.
+ Line int
+ // Description is a human-readable description of the problem.
+ Description string
+}
+
+// ErrorCode is a code for a kind of error.
+type ErrorCode int
+
+// We define codes for each error that manifests while escaping templates, but
+// escaped templates may also fail at runtime.
+//
+// Output: "ZgotmplZ"
+// Example:
+// <img src="{{.X}}">
+// where {{.X}} evaluates to `javascript:...`
+// Discussion:
+// "ZgotmplZ" is a special value that indicates that unsafe content reached a
+// CSS or URL context at runtime. The output of the example will be
+// <img src="#ZgotmplZ">
+// If the data comes from a trusted source, use content types to exempt it
+// from filtering: URL(`javascript:...`).
+const (
+ // OK indicates the lack of an error.
+ OK ErrorCode = iota
+
+ // ErrorAmbigContext: "... appears in an ambiguous URL context"
+ // Example:
+ // <a href="
+ // {{if .C}}
+ // /path/
+ // {{else}}
+ // /search?q=
+ // {{end}}
+ // {{.X}}
+ // ">
+ // Discussion:
+ // {{.X}} is in an ambiguous URL context since, depending on {{.C}},
+ // it may be either a URL suffix or a query parameter.
+ // Moving {{.X}} into the condition removes the ambiguity:
+ // <a href="{{if .C}}/path/{{.X}}{{else}}/search?q={{.X}}">
+ ErrAmbigContext
+
+ // TODO: document
+ ErrBadHTML
+
+ // ErrBranchEnd: "{{if}} branches end in different contexts"
+ // Example:
+ // {{if .C}}<a href="{{end}}{{.X}}
+ // Discussion:
+ // EscapeSet statically examines each possible path when it encounters
+ // a {{if}}, {{range}}, or {{with}} to escape any following pipelines.
+ // The example is ambiguous since {{.X}} might be an HTML text node,
+ // or a URL prefix in an HTML attribute. EscapeSet needs to understand
+ // the context of {{.X}} to escape it, but that depends on the
+ // run-time value of {{.C}}.
+ //
+ // The problem is usually something like missing quotes or angle
+ // brackets, or can be avoided by refactoring to put the two contexts
+ // into different branches of an if, range or with. If the problem
+ // is in a {{range}} over a collection that should never be empty,
+ // adding a dummy {{else}} can help.
+ ErrBranchEnd
+
+ // ErrEndContext: "... ends in a non-text context: ..."
+ // Examples:
+ // <div
+ // <div title="no close quote>
+ // <script>f()
+ // Discussion:
+ // EscapeSet assumes the ouput is a DocumentFragment of HTML.
+ // Templates that end without closing tags will trigger this error.
+ // Templates that produce incomplete Fragments should not be named
+ // in the call to EscapeSet.
+ //
+ // If you have a helper template in your set that is not meant to
+ // produce a document fragment, then do not pass its name to
+ // EscapeSet(set, ...names).
+ //
+ // {{define "main"}} <script>{{template "helper"}}</script> {{end}}
+ // {{define "helper"}} document.write(' <div title=" ') {{end}}
+ //
+ // "helper" does not produce a valid document fragment, though it does
+ // produce a valid JavaScript Program.
+ ErrEndContext
+
+ // ErrInsideComment: "... appears inside a comment"
+ // Example:
+ // <!-- {{.X}} -->
+ // <script>/* {{.X}} */</script>
+ // <style>/* {{.X}} */</style>
+ //
+ // Discussion:
+ // {{.X}} appears inside a comment. There is no escaping convention for
+ // comments. To use IE conditional comments, inject the whole comment
+ // as an HTML, JS, or CSS value (see content.go).
+ // To comment out code, break the {{...}}.
+ ErrInsideComment
+
+ // ErrNoNames: "must specify names of top level templates"
+ //
+ // EscapeSet does not assume that all templates in a set produce HTML.
+ // Some may be helpers that produce snippets of other languages.
+ // Passing in no template names is most likely an error,
+ // so EscapeSet(set) will panic.
+ // If you call EscapeSet with a slice of names, guard it with len:
+ //
+ // if len(names) != 0 {
+ // set, err := EscapeSet(set, ...names)
+ // }
+ ErrNoNames
+
+ // ErrNoSuchTemplate: "no such template ..."
+ // Examples:
+ // {{define "main"}}<div {{template "attrs"}}>{{end}}
+ // {{define "attrs"}}href="{{.URL}}"{{end}}
+ // Discussion:
+ // EscapeSet looks through template calls to compute the context.
+ // Here the {{.URL}} in "attrs" must be treated as a URL when called
+ // from "main", but if "attrs" is not in set when
+ // EscapeSet(&set, "main") is called, this error will arise.
+ ErrNoSuchTemplate
+
+ // TODO: document
+ ErrOutputContext
+
+ // ErrPartialCharset: "unfinished JS regexp charset in ..."
+ // Example:
+ // <script>var pattern = /foo[{{.Chars}}]/</script>
+ // Discussion:
+ // EscapeSet does not support interpolation into regular expression
+ // literal character sets.
+ ErrPartialCharset
+
+ // ErrPartialEscape: "unfinished escape sequence in ..."
+ // Example:
+ // <script>alert("\{{.X}}")</script>
+ // Discussion:
+ // EscapeSet does not support actions following a backslash.
+ // This is usually an error and there are better solutions; for
+ // our example
+ // <script>alert("{{.X}}")</script>
+ // should work, and if {{.X}} is a partial escape sequence such as
+ // "xA0", mark the whole sequence as safe content: JSStr(`\xA0`)
+ ErrPartialEscape
+
+ // ErrRangeLoopReentry: "on range loop re-entry: ..."
+ // Example:
+ // {{range .}}<p class={{.}}{{end}}
+ // Discussion:
+ // If an iteration through a range would cause it to end in a
+ // different context than an earlier pass, there is no single context.
+ // In the example, the <p> tag is missing a '>'.
+ // EscapeSet cannot tell whether {{.}} is meant to be an HTML class or
+ // the content of a broken <p> element and complains because the
+ // second iteration would produce something like
+ //
+ // <p class=foo<p class=bar
+ ErrRangeLoopReentry
+
+ // TODO: document
+ ErrSlashAmbig
+)
+
+func (e *Error) String() string {
+ if e.Line != 0 {
+ return fmt.Sprintf("exp/template/html:%s:%d: %s", e.Name, e.Line, e.Description)
+ } else if e.Name != "" {
+ return fmt.Sprintf("exp/template/html:%s: %s", e.Name, e.Description)
+ }
+ return "exp/template/html: " + e.Description
+}
+
+// errorf creates an error given a format string f and args.
+// The template Name still needs to be supplied.
+func errorf(k ErrorCode, line int, f string, args ...interface{}) *Error {
+ return &Error{k, "", line, fmt.Sprintf(f, args...)}
+}
diff --git a/src/pkg/exp/template/html/escape.go b/src/pkg/exp/template/html/escape.go
index b0acf48..3fa92cc 100644
--- a/src/pkg/exp/template/html/escape.go
+++ b/src/pkg/exp/template/html/escape.go
@@ -36,7 +36,7 @@
if len(names) == 0 {
// TODO: Maybe add a method to Set to enumerate template names
// and use those instead.
- return nil, os.NewError("must specify names of top level templates")
+ return nil, &Error{ErrNoNames, "", 0, "must specify names of top level templates"}
}
e := escaper{
s,
@@ -49,10 +49,10 @@
for _, name := range names {
c, _ := e.escapeTree(context{}, name, 0)
var err os.Error
- if c.errStr != "" {
- err = fmt.Errorf("%s:%d: %s", name, c.errLine, c.errStr)
+ if c.err != nil {
+ err, c.err.Name = c.err, name
} else if c.state != stateText {
- err = fmt.Errorf("%s ends in a non-text context: %v", name, c)
+ err = &Error{ErrEndContext, name, 0, fmt.Sprintf("ends in a non-text context: %v", c)}
}
if err != nil {
// Prevent execution of unsafe templates.
@@ -163,9 +163,8 @@
s = append(s, "exp_template_html_urlescaper")
case urlPartUnknown:
return context{
- state: stateError,
- errLine: n.Line,
- errStr: fmt.Sprintf("%s appears in an ambiguous URL context", n),
+ state: stateError,
+ err: errorf(ErrAmbigContext, n.Line, "%s appears in an ambiguous URL context", n),
}
default:
panic(c.urlPart.String())
@@ -180,9 +179,8 @@
s = append(s, "exp_template_html_jsregexpescaper")
case stateComment, stateJSBlockCmt, stateJSLineCmt, stateCSSBlockCmt, stateCSSLineCmt:
return context{
- state: stateError,
- errLine: n.Line,
- errStr: fmt.Sprintf("%s appears inside a comment", n),
+ state: stateError,
+ err: errorf(ErrInsideComment, n.Line, "%s appears inside a comment", n),
}
case stateCSS:
s = append(s, "exp_template_html_cssvaluefilter")
@@ -319,9 +317,8 @@
}
return context{
- state: stateError,
- errLine: line,
- errStr: fmt.Sprintf("{{%s}} branches end in different contexts: %v, %v", nodeName, a, b),
+ state: stateError,
+ err: errorf(ErrBranchEnd, line, "{{%s}} branches end in different contexts: %v, %v", nodeName, a, b),
}
}
@@ -340,8 +337,8 @@
// Make clear that this is a problem on loop re-entry
// since developers tend to overlook that branch when
// debugging templates.
- c0.errLine = n.Line
- c0.errStr = "on range loop re-entry: " + c0.errStr
+ c0.err.Line = n.Line
+ c0.err.Description = "on range loop re-entry: " + c0.err.Description
return c0
}
}
@@ -386,9 +383,8 @@
t := e.template(name)
if t == nil {
return context{
- state: stateError,
- errStr: fmt.Sprintf("no such template %s", name),
- errLine: line,
+ state: stateError,
+ err: errorf(ErrNoSuchTemplate, line, "no such template %s", name),
}, dname
}
if dname != name {
@@ -428,8 +424,7 @@
d = context{
state: stateError,
// TODO: Find the first node with a line in t.Tree.Root
- errLine: 0,
- errStr: fmt.Sprintf("cannot compute output context for template %s", n),
+ err: errorf(ErrOutputContext, 0, "cannot compute output context for template %s", n),
}
// TODO: If necessary, compute a fixed point by assuming d
// as the input context, and recursing to escapeList with a
diff --git a/src/pkg/exp/template/html/escape_test.go b/src/pkg/exp/template/html/escape_test.go
index 0ab326c..4adf367 100644
--- a/src/pkg/exp/template/html/escape_test.go
+++ b/src/pkg/exp/template/html/escape_test.go
@@ -603,11 +603,11 @@
},
{
"<a b=1 c={{.H}}",
- "z ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
+ "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
},
{
"<script>foo();",
- "z ends in a non-text context: {stateJS",
+ "z: ends in a non-text context: {stateJS",
},
{
`<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`,
@@ -656,7 +656,7 @@
// or `/-1\.5/i.test(x)` which is a method call on a
// case insensitive regular expression.
`<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`,
- `: '/' could start div or regexp: "/-"`,
+ `'/' could start div or regexp: "/-"`,
},
{
`{{template "foo"}}`,
@@ -666,7 +666,7 @@
`{{define "z"}}<div{{template "y"}}>{{end}}` +
// Illegal starting in stateTag but not in stateText.
`{{define "y"}} foo<b{{end}}`,
- `z:0: "<" in attribute name: " foo<b"`,
+ `"<" in attribute name: " foo<b"`,
},
{
`{{define "z"}}<script>reverseList = [{{template "t"}}]</script>{{end}}` +
@@ -701,7 +701,7 @@
continue
}
if strings.Index(got, test.err) == -1 {
- t.Errorf("input=%q: error %q does not contain expected string %q", test.input, got, test.err)
+ t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err)
continue
}
}
diff --git a/src/pkg/exp/template/html/transition.go b/src/pkg/exp/template/html/transition.go
index 117b20a..2449a50 100644
--- a/src/pkg/exp/template/html/transition.go
+++ b/src/pkg/exp/template/html/transition.go
@@ -6,11 +6,12 @@
import (
"bytes"
- "fmt"
- "os"
"strings"
)
+// TODO: ensure transition error messages contain template name and ideally
+// line info.
+
// transitionFunc is the array of context transition functions for text nodes.
// A transition function takes a context and template text input, and returns
// the updated context and any unconsumed text.
@@ -82,8 +83,8 @@
i, err := eatAttrName(s, attrStart)
if err != nil {
return context{
- state: stateError,
- errStr: err.String(),
+ state: stateError,
+ err: err,
}, nil
}
if i == len(s) {
@@ -204,8 +205,8 @@
c.jsCtx = jsCtxRegexp
default:
return context{
- state: stateError,
- errStr: fmt.Sprintf("'/' could start div or regexp: %.32q", s[i:]),
+ state: stateError,
+ err: errorf(ErrSlashAmbig, 0, "'/' could start div or regexp: %.32q", s[i:]),
}, nil
}
default:
@@ -235,8 +236,8 @@
i++
if i == len(b) {
return context{
- state: stateError,
- errStr: fmt.Sprintf("unfinished escape sequence in JS string: %q", s),
+ state: stateError,
+ err: errorf(ErrPartialEscape, 0, "unfinished escape sequence in JS string: %q", s),
}, nil
}
} else {
@@ -271,8 +272,8 @@
i++
if i == len(b) {
return context{
- state: stateError,
- errStr: fmt.Sprintf("unfinished escape sequence in JS regexp: %q", s),
+ state: stateError,
+ err: errorf(ErrPartialEscape, 0, "unfinished escape sequence in JS regexp: %q", s),
}, nil
}
case '[':
@@ -289,8 +290,8 @@
// This can be fixed by making context richer if interpolation
// into charsets is desired.
return context{
- state: stateError,
- errStr: fmt.Sprintf("unfinished JS regexp charset: %q", s),
+ state: stateError,
+ err: errorf(ErrPartialCharset, 0, "unfinished JS regexp charset: %q", s),
}, nil
}
@@ -463,8 +464,8 @@
i++
if i == len(b) {
return context{
- state: stateError,
- errStr: fmt.Sprintf("unfinished escape sequence in CSS string: %q", s),
+ state: stateError,
+ err: errorf(ErrPartialEscape, 0, "unfinished escape sequence in CSS string: %q", s),
}, nil
}
} else {
@@ -486,7 +487,7 @@
// It returns an error if s[i:] does not look like it begins with an
// attribute name, such as encountering a quote mark without a preceding
// equals sign.
-func eatAttrName(s []byte, i int) (int, os.Error) {
+func eatAttrName(s []byte, i int) (int, *Error) {
for j := i; j < len(s); j++ {
switch s[j] {
case ' ', '\t', '\n', '\f', '\r', '=', '>':
@@ -495,7 +496,7 @@
// These result in a parse warning in HTML5 and are
// indicative of serious problems if seen in an attr
// name in a template.
- return 0, fmt.Errorf("%q in attribute name: %.32q", s[j:j+1], s)
+ return -1, errorf(ErrBadHTML, 0, "%q in attribute name: %.32q", s[j:j+1], s)
default:
// No-op.
}