[インデックス 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/template
は text/template
をベースにしており、クロスサイトスクリプティング (XSS) などのセキュリティ脆弱性を防ぐために、自動的にコンテンツをエスケープする機能を提供します。
text/template
パッケージでは、テンプレートがパースされた後に生成される抽象構文木 (AST) である parse.Tree
に、Template
構造体の Tree
フィールドを通じて直接アクセスすることができました。しかし、html/template
パッケージでは、この parse.Tree
が内部的に保持されており、外部から直接アクセスすることはできませんでした。
このコミットの背景には、html/template
を利用する開発者が、text/template
と同様に、エスケープ処理後のテンプレートの構文木にアクセスしたいという要望があったと考えられます。コミットメッセージにある "Fixes #6318" は、この機能追加の要望がGoのIssueトラッカーで追跡されていたことを示唆しています。text/template
と html/template
の間で一貫性のあるAPIを提供し、開発者がより柔軟にテンプレートを操作できるようにすることが目的です。
前提知識の解説
Go言語のテンプレートパッケージ (text/template
と html/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/template
の Template
構造体は、内部的に text/template.Template
のインスタンスを保持しており、その text.Template
インスタンスが parse.Tree
を持っていました。しかし、html/template
の Template
構造体自体は、エスケープ処理後の parse.Tree
を直接公開していませんでした。
技術的詳細
このコミットの主要な目的は、html/template
の Template
構造体から、エスケープ処理が適用された後の 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
を指すように設計されています。
変更の具体的なアプローチは以下の通りです。
Template
構造体へのTree
フィールドの追加:src/pkg/html/template/template.go
内のTemplate
構造体にTree *parse.Tree
フィールドが追加されます。Tree
フィールドの初期化と更新:New
関数やAddParseTree
関数など、新しいhtml/template.Template
インスタンスが作成される際に、text.Tree
の値が新しいTemplate
インスタンスのTree
フィールドにコピーされるように変更されます。- 特に重要なのは、
escape.go
内のescapeTemplates
関数です。この関数は、テンプレートのエスケープ処理を実行する中心的なロジックを含んでいます。エスケープ処理が完了し、HTMLセーフなテンプレートが生成された後、tmpl.Tree = tmpl.text.Tree
という行が追加され、エスケープ後のparse.Tree
がhtml/template.Template
のTree
フィールドに反映されるようになります。
- テストの追加:
src/pkg/html/template/escape_test.go
に、tmpl.Tree
とtmpl.text.Tree
が一致することを確認するテストが追加されています。これは、エスケープ処理後にhtml/template.Template
のTree
フィールドが正しく更新されていることを保証するためです。
この変更により、html/template
を利用する開発者は、text/template
と同様に、Template
インスタンスの Tree
フィールドを通じて、エスケープ処理後のテンプレートの構文木にアクセスできるようになり、より高度なテンプレート操作や分析が可能になります。
コアとなるコードの変更箇所
このコミットによって変更されたファイルは以下の3つです。
src/pkg/html/template/escape.go
src/pkg/html/template/escape_test.go
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.Template
のTree
フィールドが、内部のtext.Template
のTree
フィールドと同一の参照を指していることを検証します。これにより、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セーフに更新された構文木を保持することが明記されています。AddParseTree
、Clone
、New
、new
といったTemplate
インスタンスを生成またはコピーする関数において、新しく追加されたTree
フィールドが適切に初期化されるように変更されました。AddParseTree
とClone
では、内部のtext.Tree
の値が新しいTemplate
インスタンスのTree
フィールドにコピーされます。New
とnew
では、初期状態ではnil
が設定されます。これは、これらの関数が呼ばれた時点ではまだエスケープ処理が完了していないためです。エスケープ処理はescapeTemplates
関数で行われ、その中でTree
フィールドが更新されます。
コアとなるコードの解説
このコミットの核心は、html/template.Template
構造体に Tree *parse.Tree
フィールドを追加し、エスケープ処理後の parse.Tree
をこのフィールドに反映させることです。
-
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セーフな構文木を指すことになります。 -
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.Template
のTree
フィールドが、エスケープ処理によって更新されたtext.Template
のTree
フィールドと同じparse.Tree
を参照するようになります。つまり、html/template.Template
のTree
フィールドは、エスケープ後の最終的な構文木を公開する役割を担います。 -
コンストラクタおよびクローン関数での
Tree
フィールドの初期化 (src/pkg/html/template/template.go
):AddParseTree
,Clone
,New
,new
といった関数は、新しいhtml/template.Template
インスタンスを作成したり、既存のインスタンスをコピーしたりする際に使用されます。これらの関数でもTree
フィールドが適切に初期化されるように変更されました。 例えば、AddParseTree
やClone
のように、既存のtext.Template
から新しいhtml/template.Template
を作成する場合は、そのtext.Tree
を新しいhtml/template.Template
のTree
フィールドにコピーします。 一方、New
やnew
のように、まだパースやエスケープが行われていない新しいテンプレートを作成する場合は、Tree
フィールドはnil
で初期化されます。これは、Tree
フィールドがエスケープ処理後に初めて有効なparse.Tree
を指すようになるためです。
これらの変更により、html/template
の Template
インスタンスは、エスケープ処理後の parse.Tree
を Template.Tree
フィールドを通じて外部に公開するようになり、text/template
とのAPIの一貫性が保たれるとともに、開発者がより詳細なテンプレートの内部構造にアクセスできるようになりました。
関連リンク
- Go言語の公式ドキュメント:
text/template
パッケージ: https://pkg.go.dev/text/templatehtml/template
パッケージ: https://pkg.go.dev/html/template
- Go言語のIssueトラッカー (Issue #6318 は公開されていないか、非常に古い可能性があります)
- このコミットのGo CL (Code Review) ページ: https://golang.org/cl/13479044
参考にした情報源リンク
- Go言語の公式ドキュメント (
text/template
およびhtml/template
パッケージ) - Go言語のソースコード (
src/pkg/html/template/
ディレクトリ内のファイル) - コミットメッセージと差分情報
- Go言語のテンプレートエンジンの一般的な動作に関する知識
- Go言語のIssueトラッカーの検索 (Issue #6318 は見つかりませんでした)
- Go言語のコードレビューシステム (Gerrit) の一般的なURL構造