How and when Xslate escapes html special characters

Xslateエスケープポリシーについて考えたので、ここでまとめておく*1
Xslate のエスケープ処理について、覚えることは以下の三つである*2

  • テンプテートタグ内で生成した値は自動的にエスケープされる
  • エスケープ処理をさせたくないときはmark_raw フィルタを使う
  • エスケープ処理を強制させたいときは unmark_rawフィルタを使う

以下、詳しく解説する。
まず、基本的には Text::MicroTemplate のポリシーを踏襲している*3。すなわち、テンプレートタグ内で生成された文字列については、HTMLのメタ文字(< > & " &apos)が自動的にエスケープされる。エスケープ処理は、一般式に対する出力コマンドが担っている。

# Text::Xslate version 0.1032
$ xslate -e '<foo>'
# 地の文字列はそのまま出力
<foo>
$ xslate -e '<: $ARGV[0] :>' '<foo>'
# 変数はエスケープされる
&lt;foo&gt;
$ xslate -e '<: "<" ~ $ARGV[0] ~ ">" :>' foo
# 式の結果もエスケープされる
&lt;foo&gt;
$ xslate -e '<: $ARGV.join(" ") :>' '<foo>' '<bar>'
# 関数やメソッドの戻り値もエスケープされる
&lt;foo&gt; &lt;bar&gt;

エスケープを抑制するためには、raw markをつける。これの実体は文字列化演算子オーバーロードしたラッパーオブジェクトであり、出力コマンドはこのraw markを見るとその値を加工せずにそのまま出力する*4。この raw mark は 'mark_raw' というフィルタでつけられる*5

$ xslate -e '<: $ARGV[0] | mark_raw :>' '<foo>'
# そのまま出力される
<foo>
$ 

強制的にエスケープしたいときは、このraw markを解除してから出力すればよい*6。raw markの解除には 'unmark_raw' フィルタを使う。たとえば、マクロはraw文字列を返すため、マクロの戻り値をエスケープする時などにはこのフィルタを使う。

$ xslate -e '<: macro foo -> {:><foo><: } :><: foo() :>'
# マクロの戻り値はraw文字列である
<foo>
$ xslate -e '<: macro foo -> {:><foo><: } :><: foo() | unmark_raw :>'
# raw markを解除するとエスケープされる
&lt;foo&gt;

'mark_raw' と 'unmark_raw' は何度重ねても効果は重複しない。

$ xslate -e '<: $ARGV[0] | mark_raw | mark_raw | unmark_raw :>' '<foo>'
# 何度markを重ねがけしても、最後にunmarkすると強制的にエスケープされる
&lt;foo&gt;

なお、raw文字列を連結した際の挙動や、また、関数に渡した際の扱いは未定義である。このあたりは安全性をもっと検討してから決めたい。

最後に、これらのオーバーヘッドだが、'mark_raw'は新たにSVを生成したりbless()したりしなければならないため、その適用にはある程度時間がかかる。ただし、出力する直前に 'mark_raw' を使う場合はフィルタの適用が最適化で取り除かれるため、オーバーヘッドは全くなくなる。'unmark_raw' については、フィルタの適用そのものは取り除けないものの、新たにSVを生成することはないため、オーバーヘッドは極めて少ない。したがって、ほとんどのケースではこれらのオーバーヘッドは無視できる程度だと考えてよい。

*1:as of version 0.1032

*2:なお、newのオプション escape => 'none' のもとでは挙動が異なるが、これはメールなどの非HTMLテキストを出力するために用意されているモードであり、このモードでHTMLを出力するべきではない。したがって詳細は語らない。

*3:Kazuhoさんの記事Text::MicroTemplate - テンプレートエンジンのセキュリティと利便性も参照のこと。

*4:このあたりの実装はMTと同じである。

*5:0.1031までは 'raw' という名前だった。なお、最新版でも古い名前はサポートされる。

*6:Xslate 0.1031 までは、'html' フィルタでエスケープするしかなかった。こちらは、実際にエスケープ処理を行ってからraw文字列を返す。ただし、このフィルタはraw文字列を受け取るとエスケープせずにそのまま返すため、マクロの出力を強制エスケープできない。