何の話?
この記事のテーマは eRuby に <%= %>
などの手段で文字列を挿入するときの HTML エスケープについて。
「eRuby」はテキスト(要するに文字列)に Ruby のコードを埋め込むことができるテンプレート言語の名前。しかし,この名称よりも「ERB」とか「erb」という呼び方で認識している人のほうが多いかもしれない。
「HTML エスケープ」は <
とか &
といった文字を <
とか &
といったものに変換する文字列処理。
eRuby の典型的な応用は HTML の生成なので1,式の値(を文字列化したもの)をそのまま挿入したい場合もあれば,HTML エスケープして挿入したい場合もある。
Ruby on Rails では HTML エスケープについてきちんと意識しなくてもよしなに取り計らってくれたりするが,本記事ではテンプレートエンジンをあらわに扱って処理する場合に知っておきたいことをまとめる。
eRuby のテンプレートエンジンとしては,ERB と Erubi を主に取り上げる。
なお,eRuby という名称や eRuby のテンプレートエンジンについては下記の拙記事も参考にしてほしい。
Erubi とは何か - Qiita
ところで,「エスケープ」にもいろいろあるが,本記事では HTML エスケープしか扱わないので,以下では「HTML」を略すことがある。
テンプレートエンジン
eRuby のテンプレートエンジンはいくつもあるが,2021 年時点では,標準添付ライブラリーの ERB と,Rails で採用されている Erubi だけ知っておけばいいと思う。
本記事では説明の都合で,Erubi の前身にあたる Erubis にも触れる。
歴史的には ERB → Erubis → Erubi の順に現れた。
以下,本記事では「ERB」という表記は標準添付ライブラリーで定義された ERB クラスのみを指すこととする。テンプレート言語の名前は「eRuby」とのみ表記する。
例題
この記事では,単純に "&"
という文字列を,HTML エスケープ無しと有りの二とおりに挿入したテキストを生成してみる。
以下,ERB と,Erubis,Erubi で処理するコードを見ていくが,Tilt というライブラリーを援用すると記述が楽で説明もしやすいので,そうする。
ERB の場合
ERB ではテキスト中に式の値(を文字列化したもの)を挿入する手段は <%= %>
しかない。
また,HTML エスケープは自動的にはされない。
レンダリングの仕方
では処理コードを見てみよう。
あらかじめ
gem install tilt
で Tilt をインストールしておき,
require "tilt"
erb_text = "<%= '&' %>"
p Tilt::ERBTemplate.new{erb_text}.render
とすれば
"&"
と表示される。確かにエスケープはされていない。
(require "erb"
はあらわに書かなくても require してくれるようだ)
コードの意味を少し説明する。
Tilt::ERBTemplate
というのは,Tilt で定義された ERB ベースのテンプレート処理クラス2。
eRuby スクリプトを引数ではなくブロックで与えることに注意されたい。
一方,eRuby スクリプトそのものではなく,eRuby スクリプトのファイルパスを与える場合はブロックではなく引数として与える。
エスケープさせたいときは
エスケープさせたいときは,エスケープ用のメソッドを使う。
たとえば標準添付ライブラリーの cgi/escape
には escapeHTML
というメソッドが定義されている。
こんなふうに使える:
require "cgi/escape"
puts CGI.escapeHTML("&") # => "&"
しかし,ERB クラスには ERB::Util という小さなモジュールが定義されていて,そこに html_escape というメソッドと,そのエイリアスである h
メソッドが定義されているので,これを使えばよい。
このメソッドは
require "erb"
p ERB::Util.h("&") # => "&"
のように使える。
ERB::Util のメソッドはいわゆるモジュール関数として定義されているので,(ちょうど Math モジュールの関数たちと同じように)モジュールの特異メソッドしてだけでなく,
require "erb"
include ERB::Util
p h("&") # => "&"
のような使い方も可能だ。
このモジュールを使えば,"&"
をエスケープして挿入する eRuby スクリプトは
<%= h("&") %>
のように書くことができるはずだ。
あるいはもっとシンプルに
<%=h "&" %>
と書いてもいい。h
の前にスペースを書かなかったのは,まあなんというか,癖というか好みのようなもの。
では eRuby スクリプト中で h
メソッドが使えるようにするにはどうすればよいのか?
それには,h
メソッドを持つオブジェクトを render
メソッドに与えればよい。このようなオブジェクトを「スコープ」と呼ぶらしい。
具体的には,以下のようにする。
require "tilt"
require "erb"
erb = "<%=h '&' %>"
scope = Object.new
scope.extend ERB::Util
p Tilt::ERBTemplate.new{erb}.render(scope) # => "&"
オブジェクトを一つ作って,ERB::Util を extend
し,それを render
に引数として与えればよいのだ。
scope
を作るところは一行で
scope = Object.new.extend(ERB::Util)
と書いてもいい。
余談だが,eRuby スクリプトに埋め込まれた Ruby コード部分では,render
メソッドにスコープとして渡されたオブジェクトが self
になる。
したがって,もし上記のコードで scope
がインスタンス変数 @foo
を持っていたとしたら,eRuby スクリプト中でそれを直に @foo
として参照することができる。
Erubis と Erubi
Erubis と Erubi では,式の値(を文字列化したもの)を挿入する手段として <%= %>
のほかに <%== %>
がある。
両者の違いはエスケープされるかどうかだけ。
デフォルトでは
-
<%= %>
エスケープされない -
<%== %>
エスケープされる
なのだが,オプションにより逆にすることができる。
レンダリングの仕方
Erubis と Erubi は同じ使い方なので,Erubi のコードで示す。
require "tilt"
erb_text = "<%= '&' %> <%== '&' %>"
p Tilt::ErubiTemplate.new{erb_text}.render
# => "& &"
Erubis の場合は Tilt::ErubiTemplate
を Tilt::ErubisTemplate
に変えるだけ。
結果を見ると,確かに
-
<%= %>
エスケープ無し -
<%== %>
エスケープ有り
となっている。
エスケープの有無を逆転させるには
おそらく eRuby を書いたことのある人の多くは(デフォルトの逆の)
-
<%= %>
エスケープ有り -
<%== %>
エスケープ無し
に馴染んでいると思う。
Ruby on Rails がバージョン 3 以降そうなっているからだ。
もっとも,Rails の場合,ActiveSupport::SafeBuffer という String を継承したクラスを導入して,エスケープすべきかどうかを自動判定してよきに計らってくれるようになっているので,<%== %>
はあまり出番が無いかもしれない。
Rails については本記事のテーマを外れるので,これ以上触れない。
エスケープの有無を上記のように逆転させるには,escape
というオプションをキーワード引数として与え,
require "tilt"
erb_text = "<%= '&' %> <%== '&' %>"
p Tilt::ErubisTemplate.new(escape: true){erb_text}.render
# => "& &"
とすればよい。
結果を見れば,確かにデフォルトとはエスケープの有無が逆になっている。
オプションで逆ってややこしい?
オプション如何で <%= %>
と <%== %>
の意味が全く逆になり,しかも世間ではデフォルトと逆の設定が普通である,という状況はややこしくないだろうか?
eRuby スクリプトだけを見たのでは <%= %>
の意味がどちらであるか分からないのだ。
ここからは推測だが,こうなっているのは歴史的な事情ではないかと思う。
既に見たように ERB ではエスケープするために式の中でエスケープ用メソッドを使う必要があった。
Erubis は <%== %>
という形式を新たに付け加えることで,自動エスケープ機能を実現した。
ここでこういう考え方が起こる(完全に憶測というか妄想)。HTML 生成において,エスケープはセキュリティーに関わる重大事である。「うっかり h
を書き忘れた」は許されない3。自動エスケープこそが基本であるべきではないか。
なので,最もシンプルな <%= %>
をエスケープ有りとし,複雑な <%== %>
をエスケープ無しとする設定を用意したところ,多くのユーザーに支持された。そういうことではないのか。
こういう話は,Erubis のドキュメントか,Rails 3 のドキュメントに書いてあるかもしれないが,もう(私には)見つけられない。
私もこちら(<%= %>
がエスケープ)を断然支持する4。経験上,うっかり <%= %>
とすることはあっても,うっかり <%== %>
とすることはまず無いからだ。それにイコールを二つ重ねたほうがいかにも「そ・の・ま・ん・ま」感があるし。
最初からそうなってればよかったのに,と思わなくもないが,ERB との互換性を考慮してこういう仕様になっているのかもしれない。
余談だが,eRuby とは全く違うテンプレートシステム Slim でも,=
で「エスケープ有りの挿入」,==
で「エスケープ無しの挿入」となっている。