]> Cypherpunks repositories - gostls13.git/commitdiff
html/template: indirect top-level values before printing
authorRob Pike <r@golang.org>
Wed, 16 Nov 2011 17:32:52 +0000 (09:32 -0800)
committerRob Pike <r@golang.org>
Wed, 16 Nov 2011 17:32:52 +0000 (09:32 -0800)
text/template does this (in an entirely different way), so
make html/template do the same. Before this fix, the template
{{.}} given a pointer to a string prints its address instead of its
value.

R=mikesamuel, r
CC=golang-dev
https://golang.org/cl/5370098

src/pkg/html/template/content.go
src/pkg/html/template/escape_test.go
src/pkg/html/template/js.go

index d720d4ba6895df1e0a48463f38ad02bd76815ada..3fb15a6e93f56e294f169a88c99da5b0dc6b7b1a 100644 (file)
@@ -6,6 +6,7 @@ package template
 
 import (
        "fmt"
+       "reflect"
 )
 
 // Strings of content from a trusted source.
@@ -70,10 +71,25 @@ const (
        contentTypeUnsafe
 )
 
+// indirect returns the value, after dereferencing as many times
+// as necessary to reach the base type (or nil).
+func indirect(a interface{}) interface{} {
+       if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr {
+               // Avoid creating a reflect.Value if it's not a pointer.
+               return a
+       }
+       v := reflect.ValueOf(a)
+       for v.Kind() == reflect.Ptr && !v.IsNil() {
+               v = v.Elem()
+       }
+       return v.Interface()
+}
+
 // stringify converts its arguments to a string and the type of the content.
+// All pointers are dereferenced, as in the text/template package.
 func stringify(args ...interface{}) (string, contentType) {
        if len(args) == 1 {
-               switch s := args[0].(type) {
+               switch s := indirect(args[0]).(type) {
                case string:
                        return s, contentTypePlain
                case CSS:
@@ -90,5 +106,8 @@ func stringify(args ...interface{}) (string, contentType) {
                        return string(s), contentTypeURL
                }
        }
+       for i, arg := range args {
+               args[i] = indirect(arg)
+       }
        return fmt.Sprint(args...), contentTypePlain
 }
index d8bfa321121020dd8d8e65bce99f56df4c41662c..4af583097bd55fb165e69e3b0fc8af3019cc9b10 100644 (file)
@@ -28,7 +28,7 @@ func (x *goodMarshaler) MarshalJSON() ([]byte, error) {
 }
 
 func TestEscape(t *testing.T) {
-       var data = struct {
+       data := struct {
                F, T    bool
                C, G, H string
                A, E    []string
@@ -50,6 +50,7 @@ func TestEscape(t *testing.T) {
                Z: nil,
                W: HTML(`&iexcl;<b class="foo">Hello</b>, <textarea>O'World</textarea>!`),
        }
+       pdata := &data
 
        tests := []struct {
                name   string
@@ -668,6 +669,15 @@ func TestEscape(t *testing.T) {
                        t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
                        continue
                }
+               b.Reset()
+               if err := tmpl.Execute(b, pdata); err != nil {
+                       t.Errorf("%s: template execution failed for pointer: %s", test.name, err)
+                       continue
+               }
+               if w, g := test.output, b.String(); w != g {
+                       t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
+                       continue
+               }
        }
 }
 
@@ -1605,6 +1615,29 @@ func TestRedundantFuncs(t *testing.T) {
        }
 }
 
+func TestIndirectPrint(t *testing.T) {
+       a := 3
+       ap := &a
+       b := "hello"
+       bp := &b
+       bpp := &bp
+       tmpl := Must(New("t").Parse(`{{.}}`))
+       var buf bytes.Buffer
+       err := tmpl.Execute(&buf, ap)
+       if err != nil {
+               t.Errorf("Unexpected error: %s", err)
+       } else if buf.String() != "3" {
+               t.Errorf(`Expected "3"; got %q`, buf.String())
+       }
+       buf.Reset()
+       err = tmpl.Execute(&buf, bpp)
+       if err != nil {
+               t.Errorf("Unexpected error: %s", err)
+       } else if buf.String() != "hello" {
+               t.Errorf(`Expected "hello"; got %q`, buf.String())
+       }
+}
+
 func BenchmarkEscapedExecute(b *testing.B) {
        tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`))
        var buf bytes.Buffer
index 68c53e5ca3b549484ae59368c190170ae64497d9..0e632df42201e49661f98d0671069e2b4bcb155e 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "encoding/json"
        "fmt"
+       "reflect"
        "strings"
        "unicode/utf8"
 )
@@ -117,12 +118,24 @@ var regexpPrecederKeywords = map[string]bool{
        "void":       true,
 }
 
+var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
+
+// indirectToJSONMarshaler returns the value, after dereferencing as many times
+// as necessary to reach the base type (or nil) or an implementation of json.Marshal.
+func indirectToJSONMarshaler(a interface{}) interface{} {
+       v := reflect.ValueOf(a)
+       for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Ptr && !v.IsNil() {
+               v = v.Elem()
+       }
+       return v.Interface()
+}
+
 // jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
-// nether side-effects nor free variables outside (NaN, Infinity).
+// neither side-effects nor free variables outside (NaN, Infinity).
 func jsValEscaper(args ...interface{}) string {
        var a interface{}
        if len(args) == 1 {
-               a = args[0]
+               a = indirectToJSONMarshaler(args[0])
                switch t := a.(type) {
                case JS:
                        return string(t)
@@ -135,6 +148,9 @@ func jsValEscaper(args ...interface{}) string {
                        a = t.String()
                }
        } else {
+               for i, arg := range args {
+                       args[i] = indirectToJSONMarshaler(arg)
+               }
                a = fmt.Sprint(args...)
        }
        // TODO: detect cycles before calling Marshal which loops infinitely on