diff --git a/src/pkg/exp/template/html/Makefile b/src/pkg/exp/template/html/Makefile index e53270c9c84..9032aead870 100644 --- a/src/pkg/exp/template/html/Makefile +++ b/src/pkg/exp/template/html/Makefile @@ -11,6 +11,7 @@ GOFILES=\ 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 bfe168f6469..e8812cf8657 100644 --- a/src/pkg/exp/template/html/context.go +++ b/src/pkg/exp/template/html/context.go @@ -21,8 +21,7 @@ type context struct { urlPart urlPart jsCtx jsCtx element element - errLine int - errStr string + err *Error } // eq returns whether two contexts are equal. @@ -32,8 +31,7 @@ func (c context) eq(d context) bool { 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 2751ce834b0..a9b78ca5157 100644 --- a/src/pkg/exp/template/html/doc.go +++ b/src/pkg/exp/template/html/doc.go @@ -69,178 +69,8 @@ in this case, 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: - -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: - - - -Error: "... appears inside a comment" -Example: -*/ -// -// -// -/* -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}}{{end}} <- No quote after foo - - Second, try refactoring your template. - - {{if .C}}{{end}} - - -> - - {{if .C}}{{else}}{{.X}}{{end}} - - Third, check for {{range}}s that have no {{else}} - - - - 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}} - - - - -> - - - - 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: -
{{template "helper"}} {{end}} - {{define "helper"}} document.write('
{{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 .}}

tag is missing a '>'. - EscapeSet cannot tell whether {{.}} is meant to be an HTML class or the - content of a broken

element and complains because the second iteration - would produce something like - -

alert("\{{.X}}") -Discussion: - EscapeSet does not support actions following a backslash. - This is usually an error and there are better solutions; for - our example - - 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: - -Discussion: - EscapeSet does not support interpolation into regular expression literal - character sets. - -Error: "ZgotmplZ" -Example: - - 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 - - If the data can be trusted, giving the string type XXX will exempt - it from filtering. A fuller picture @@ -249,8 +79,6 @@ details necessary to understand escaping contexts and error messages. Most users will not need to understand these details. - - Contexts Assuming {{.}} is `O'Reilly: How are you?`, 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 00000000000..5fa23574335 --- /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: +// +// 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 +// +// 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: + // + // 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: + // + ErrAmbigContext + + // TODO: document + ErrBadHTML + + // ErrBranchEnd: "{{if}} branches end in different contexts" + // Example: + // {{if .C}}{{template "helper"}} {{end}} + // {{define "helper"}} document.write('

+ // + // + // + // 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"}}
{{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: + // + // Discussion: + // EscapeSet does not support interpolation into regular expression + // literal character sets. + ErrPartialCharset + + // ErrPartialEscape: "unfinished escape sequence in ..." + // Example: + // + // Discussion: + // EscapeSet does not support actions following a backslash. + // This is usually an error and there are better solutions; for + // our example + // + // 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 .}}

tag is missing a '>'. + // EscapeSet cannot tell whether {{.}} is meant to be an HTML class or + // the content of a broken

element and complains because the + // second iteration would produce something like + // + //

foo();", - "z ends in a non-text context: {stateJS", + "z: ends in a non-text context: {stateJS", }, { ``, @@ -656,7 +656,7 @@ func TestErrors(t *testing.T) { // or `/-1\.5/i.test(x)` which is a method call on a // case insensitive regular expression. ``, - `: '/' could start div or regexp: "/-"`, + `'/' could start div or regexp: "/-"`, }, { `{{template "foo"}}`, @@ -666,7 +666,7 @@ func TestErrors(t *testing.T) { `{{define "z"}}{{end}}` + // Illegal starting in stateTag but not in stateText. `{{define "y"}} fooreverseList = [{{template "t"}}]{{end}}` + @@ -701,7 +701,7 @@ func TestErrors(t *testing.T) { 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 117b20a5bff..2449a501108 100644 --- a/src/pkg/exp/template/html/transition.go +++ b/src/pkg/exp/template/html/transition.go @@ -6,11 +6,12 @@ package html 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 @@ func tTag(c context, s []byte) (context, []byte) { 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 @@ func tJS(c context, s []byte) (context, []byte) { 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 @@ func tJSStr(c context, s []byte) (context, []byte) { 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 @@ func tJSRegexp(c context, s []byte) (context, []byte) { 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 @@ func tJSRegexp(c context, s []byte) (context, []byte) { // 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 @@ func tCSSStr(c context, s []byte) (context, []byte) { 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 @@ func tError(c context, s []byte) (context, []byte) { // 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 @@ func eatAttrName(s []byte, i int) (int, os.Error) { // 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. }