Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 17469] ファイルの概要

このコミットは、Go言語の html/template パッケージにおいて、エスケープされたテンプレートの parse.Tree を外部に公開するように変更するものです。これにより、text/template パッケージと同様に、html/template でもテンプレートの内部的な構文木にアクセスできるようになります。

コミット

  • コミットハッシュ: 80f39f7b73fb3353b36014d0c97abc7b2d1bc555
  • Author: Rob Pike r@golang.org
  • Date: Thu Sep 5 08:23:11 2013 +1000

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/80f39f7b73fb3353b36014d0c97abc7b2d1bc555

元コミット内容

    html/template: export the parse.Tree for the escaped template
    The underlying parse tree is visible in text/template, so it should be visible here.
    Done by copying the underlying *parse.Tree up to the top level of the struct, and then making sure it's kept up to date.
    Fixes #6318.
    
    R=mikesamuel
    CC=golang-dev
    https://golang.org/cl/13479044

変更の背景

Go言語の標準ライブラリには、テキストテンプレートを扱う text/template パッケージと、HTMLテンプレートを扱う html/template パッケージがあります。html/templatetext/template をベースにしており、クロスサイトスクリプティング (XSS) などのセキュリティ脆弱性を防ぐために、自動的にコンテンツをエスケープする機能を提供します。

text/template パッケージでは、テンプレートがパースされた後に生成される抽象構文木 (AST) である parse.Tree に、Template 構造体の Tree フィールドを通じて直接アクセスすることができました。しかし、html/template パッケージでは、この parse.Tree が内部的に保持されており、外部から直接アクセスすることはできませんでした。

このコミットの背景には、html/template を利用する開発者が、text/template と同様に、エスケープ処理後のテンプレートの構文木にアクセスしたいという要望があったと考えられます。コミットメッセージにある "Fixes #6318" は、この機能追加の要望がGoのIssueトラッカーで追跡されていたことを示唆しています。text/templatehtml/template の間で一貫性のあるAPIを提供し、開発者がより柔軟にテンプレートを操作できるようにすることが目的です。

前提知識の解説

Go言語のテンプレートパッケージ (text/templatehtml/template)

Go言語には、テキストベースのテンプレートエンジンを提供する text/template パッケージと、HTMLコンテンツの生成に特化した html/template パッケージがあります。

  • text/template: 任意のテキスト形式の出力を生成するための汎用的なテンプレートエンジンです。プレースホルダー、条件分岐、ループなどの基本的なテンプレート機能を提供します。
  • html/template: text/template を基盤としつつ、HTMLコンテンツの生成に特化したパッケージです。最大の特長は、出力されるHTMLコンテンツに対して自動的にコンテキストに応じたエスケープ処理(サニタイズ)を行うことです。これにより、悪意のあるスクリプトの埋め込み(XSS攻撃)などを防ぎ、Webアプリケーションのセキュリティを向上させます。例えば、ユーザー入力がHTML属性やJavaScriptの文字列として挿入される場合、適切なエスケープが自動的に適用されます。

parse.Tree (抽象構文木 - AST)

Goのテンプレートパッケージでは、テンプレート文字列がパースされると、その構造を表現する抽象構文木 (Abstract Syntax Tree, AST) が内部的に構築されます。このASTは parse.Tree 型で表現されます。

  • parse.Tree: テンプレートの構文要素(アクション、パイプライン、コマンド、リテラルなど)をノードとして持つツリー構造です。テンプレートエンジンは、この parse.Tree をトラバースしながら、データと結合して最終的な出力を生成します。
  • text/template では、Template 構造体に Tree *parse.Tree フィールドがあり、パースされたテンプレートのASTに直接アクセスできます。これにより、開発者はテンプレートの構造をプログラム的に検査したり、変更したりすることが可能になります。

html/template のエスケープ処理

html/template は、テンプレートをパースした後、さらにセキュリティのためにエスケープ処理(サニタイズ)を行います。このエスケープ処理は、元の parse.Tree を基にして、HTMLのコンテキスト(例: HTML要素の内部、属性値、JavaScriptコード内など)に応じて適切なエスケープルールを適用し、新しい(エスケープ済みの)ASTを生成します。

このコミット以前は、html/templateTemplate 構造体は、内部的に text/template.Template のインスタンスを保持しており、その text.Template インスタンスが parse.Tree を持っていました。しかし、html/templateTemplate 構造体自体は、エスケープ処理後の parse.Tree を直接公開していませんでした。

技術的詳細

このコミットの主要な目的は、html/templateTemplate 構造体から、エスケープ処理が適用された後の parse.Tree に直接アクセスできるようにすることです。

変更前、html/template.Template 構造体は、内部に text *template.Template というフィールドを持っていました。この text フィールドが、実際のテンプレートのパース結果である parse.Tree を保持していました。しかし、html/template が提供するセキュリティのためのエスケープ処理は、この text.Tree を直接変更するのではなく、エスケープ処理後の新しいASTを生成し、それを内部的に利用していました。そのため、html/template.Template の外部からは、エスケープ後の parse.Tree にアクセスする手段がありませんでした。

このコミットでは、html/template.Template 構造体に新たに Tree *parse.Tree フィールドを追加します。この新しい Tree フィールドは、html/template のエスケープ処理によって生成された、HTMLセーフな parse.Tree を指すように設計されています。

変更の具体的なアプローチは以下の通りです。

  1. Template 構造体への Tree フィールドの追加: src/pkg/html/template/template.go 内の Template 構造体に Tree *parse.Tree フィールドが追加されます。
  2. Tree フィールドの初期化と更新:
    • New 関数や AddParseTree 関数など、新しい html/template.Template インスタンスが作成される際に、text.Tree の値が新しい Template インスタンスの Tree フィールドにコピーされるように変更されます。
    • 特に重要なのは、escape.go 内の escapeTemplates 関数です。この関数は、テンプレートのエスケープ処理を実行する中心的なロジックを含んでいます。エスケープ処理が完了し、HTMLセーフなテンプレートが生成された後、tmpl.Tree = tmpl.text.Tree という行が追加され、エスケープ後の parse.Treehtml/template.TemplateTree フィールドに反映されるようになります。
  3. テストの追加: src/pkg/html/template/escape_test.go に、tmpl.Treetmpl.text.Tree が一致することを確認するテストが追加されています。これは、エスケープ処理後に html/template.TemplateTree フィールドが正しく更新されていることを保証するためです。

この変更により、html/template を利用する開発者は、text/template と同様に、Template インスタンスの Tree フィールドを通じて、エスケープ処理後のテンプレートの構文木にアクセスできるようになり、より高度なテンプレート操作や分析が可能になります。

コアとなるコードの変更箇所

このコミットによって変更されたファイルは以下の3つです。

  1. src/pkg/html/template/escape.go
  2. src/pkg/html/template/escape_test.go
  3. src/pkg/html/template/template.go

src/pkg/html/template/escape.go

--- a/src/pkg/html/template/escape.go
+++ b/src/pkg/html/template/escape.go
@@ -35,11 +35,13 @@ func escapeTemplates(tmpl *Template, names ...string) error {
 			for _, name := range names {
 				if t := tmpl.set[name]; t != nil {
 					t.text.Tree = nil
+					t.Tree = nil
 				}
 			}
 			return err
 		}
 		tmpl.escaped = true
+		tmpl.Tree = tmpl.text.Tree
 	}
 	e.commit()
 	return nil
  • escapeTemplates 関数内で、エラー発生時に t.Tree = nil が追加されました。これは、t.text.Tree = nil と同様に、エスケープ処理が失敗した場合に Tree フィールドもクリアすることを保証します。
  • エスケープ処理が成功し、tmpl.escaped = true が設定された後に、tmpl.Tree = tmpl.text.Tree が追加されました。これは、エスケープ処理によって更新された text.Tree の参照を、html/template.Template の新しい Tree フィールドにコピーするものです。

src/pkg/html/template/escape_test.go

--- a/src/pkg/html/template/escape_test.go
+++ b/src/pkg/html/template/escape_test.go
@@ -673,6 +673,10 @@ func TestEscape(t *testing.T) {
 			t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
 			continue
 		}
+		if tmpl.Tree != tmpl.text.Tree {
+			t.Errorf("%s: tree mismatch", test.name)
+			continue
+		}
 	}
 }
  • TestEscape 関数に新しいテストケースが追加されました。tmpl.Tree != tmpl.text.Tree のチェックは、エスケープ処理後に html/template.TemplateTree フィールドが、内部の text.TemplateTree フィールドと同一の参照を指していることを検証します。これにより、Tree フィールドが正しく更新されていることが保証されます。

src/pkg/html/template/template.go

--- a/src/pkg/html/template/template.go
+++ b/src/pkg/html/template/template.go
@@ -21,7 +21,9 @@ type Template struct {
 	// We could embed the text/template field, but it's safer not to because
 	// we need to keep our version of the name space and the underlying
 	// template's in sync.
-	text       *template.Template
+	text *template.Template
+	// The underlying template's parse tree, updated to be HTML-safe.
+	Tree       *parse.Tree
 	*nameSpace // common to all associated templates
 }
 
@@ -149,6 +151,7 @@ func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error
 	ret := &Template{
 		false,
 		text,
+		text.Tree,
 		t.nameSpace,
 	}
 	t.set[name] = ret
@@ -176,6 +179,7 @@ func (t *Template) Clone() (*Template, error) {
 	ret := &Template{
 		false,
 		textClone,
+		textClone.Tree,
 		&nameSpace{
 			set: make(map[string]*Template),
 		},
@@ -195,6 +199,7 @@ func (t *Template) Clone() (*Template, error) {
 		ret.set[name] = &Template{
 			false,
 			x,
+			x.Tree,
 			ret.nameSpace,
 		}
 	}
@@ -206,6 +211,7 @@ func New(name string) *Template {
 	tmpl := &Template{
 		false,
 		template.New(name),
+		nil,
 		&nameSpace{
 			set: make(map[string]*Template),
 		},
@@ -228,6 +234,7 @@ func (t *Template) new(name string) *Template {
 	tmpl := &Template{
 		false,
 		t.text.New(name),
+		nil,
 		t.nameSpace,
 	}
 	tmpl.set[name] = tmpl
  • Template 構造体に Tree *parse.Tree フィールドが追加されました。コメントには「The underlying template's parse tree, updated to be HTML-safe.」とあり、HTMLセーフに更新された構文木を保持することが明記されています。
  • AddParseTreeCloneNewnew といった Template インスタンスを生成またはコピーする関数において、新しく追加された Tree フィールドが適切に初期化されるように変更されました。
    • AddParseTreeClone では、内部の text.Tree の値が新しい Template インスタンスの Tree フィールドにコピーされます。
    • Newnew では、初期状態では nil が設定されます。これは、これらの関数が呼ばれた時点ではまだエスケープ処理が完了していないためです。エスケープ処理は escapeTemplates 関数で行われ、その中で Tree フィールドが更新されます。

コアとなるコードの解説

このコミットの核心は、html/template.Template 構造体に Tree *parse.Tree フィールドを追加し、エスケープ処理後の parse.Tree をこのフィールドに反映させることです。

  1. Template 構造体の変更 (src/pkg/html/template/template.go):

    type Template struct {
        // ...
        text *template.Template
        // The underlying template's parse tree, updated to be HTML-safe.
        Tree       *parse.Tree
        *nameSpace // common to all associated templates
    }
    

    この変更により、html/template.Template のインスタンスは、内部の text/template.Template が持つ parse.Tree とは別に、自身の Tree フィールドを持つことになります。この Tree フィールドは、html/template のセキュリティ処理が適用された後の、HTMLセーフな構文木を指すことになります。

  2. escapeTemplates 関数での Tree フィールドの更新 (src/pkg/html/template/escape.go):

    func escapeTemplates(tmpl *Template, names ...string) error {
        // ...
        if err := e.escape(tmpl.text, names...); err != nil {
            // ...
            for _, name := range names {
                if t := tmpl.set[name]; t != nil {
                    t.text.Tree = nil
                    t.Tree = nil // エラー時に自身のTreeもクリア
                }
            }
            return err
        }
        tmpl.escaped = true
        tmpl.Tree = tmpl.text.Tree // エスケープ処理後にTreeを更新
        e.commit()
        return nil
    }
    

    escapeTemplates 関数は、html/template のエスケープ処理の主要な部分です。e.escape(tmpl.text, names...) が呼び出されることで、内部の tmpl.text (つまり text/template.Template のインスタンス) の Tree フィールドが、HTMLセーフな構文木に更新されます。 このコミットでは、その直後に tmpl.Tree = tmpl.text.Tree という行が追加されました。これにより、html/template.TemplateTree フィールドが、エスケープ処理によって更新された text.TemplateTree フィールドと同じ parse.Tree を参照するようになります。つまり、html/template.TemplateTree フィールドは、エスケープ後の最終的な構文木を公開する役割を担います。

  3. コンストラクタおよびクローン関数での Tree フィールドの初期化 (src/pkg/html/template/template.go): AddParseTree, Clone, New, new といった関数は、新しい html/template.Template インスタンスを作成したり、既存のインスタンスをコピーしたりする際に使用されます。これらの関数でも Tree フィールドが適切に初期化されるように変更されました。 例えば、AddParseTreeClone のように、既存の text.Template から新しい html/template.Template を作成する場合は、その text.Tree を新しい html/template.TemplateTree フィールドにコピーします。 一方、Newnew のように、まだパースやエスケープが行われていない新しいテンプレートを作成する場合は、Tree フィールドは nil で初期化されます。これは、Tree フィールドがエスケープ処理後に初めて有効な parse.Tree を指すようになるためです。

これらの変更により、html/templateTemplate インスタンスは、エスケープ処理後の parse.TreeTemplate.Tree フィールドを通じて外部に公開するようになり、text/template とのAPIの一貫性が保たれるとともに、開発者がより詳細なテンプレートの内部構造にアクセスできるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (text/template および html/template パッケージ)
  • Go言語のソースコード (src/pkg/html/template/ ディレクトリ内のファイル)
  • コミットメッセージと差分情報
  • Go言語のテンプレートエンジンの一般的な動作に関する知識
  • Go言語のIssueトラッカーの検索 (Issue #6318 は見つかりませんでした)
  • Go言語のコードレビューシステム (Gerrit) の一般的なURL構造