[インデックス 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.gosrc/pkg/html/template/escape_test.gosrc/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構造