|
|||||||
href="..." のような属性の値を変更する方法を説明します。
コード:
require "amrita/template"
include Amrita
tmpl = TemplateText.new <<END
<table border="1">
<tr><th>name</th><th>author</th><th>webpage</tr>
<tr id=table1>
<td id="name"></td>
<td id="author"></td>
<td><a id="webpage"></a></td>
</tr>
</table>
END
data = {
:table1=>[
{
:name=>"Ruby",
:author=>"matz" ,
:webpage=> a(:href=>"http://www.ruby-lang.org/") { "Ruby Home Page" },
},
{
:name=>"perl",
:author=>"Larry Wall" ,
:webpage=> a(:href=>"http://www.perl.com/") { "Perl.com" },
},
{
:name=>"python",
:author=>"Guido van Rossum" ,
:webpage=> a(:href=>"http://www.python.org/") { "Python Language Website" },
},
]
}
tmpl.prettyprint = true
tmpl.use_compiler = true
tmpl.expand(STDOUT, data)
出力:
<table border="1">
<tr>
<th>name</th>
<th>author</th>
<th>webpage</th>
</tr>
<tr>
<td>Ruby</td>
<td>matz</td>
<td><a href="http://www.ruby-lang.org/">Ruby Home Page</a></td>
</tr>
<tr>
<td>perl</td>
<td>Larry Wall</td>
<td><a href="http://www.perl.com/">Perl.com</a></td>
</tr>
<tr>
<td>python</td>
<td>Guido van Rossum</td>
<td><a href="http://www.python.org/">Python Language Website</a></td>
</tr>
</table>
Amrita#a() というメソッドは Amrita::AttrArray という特別なオブジェクトを生成します。
a(:href=>"http://www.ruby-lang.org/") { "Ruby Home Page" },
このオブジェクトをモデルデータとして使用すると、HTML要素の属性が変更されます。 例えば、次のようなテンプレートにこのデータを与えたとすると
<td><a id="webpage"></a></td>
出力は次のようになります。
<td><a href="http://www.ruby-lang.org/">Ruby Home Page</a></td>
docs/XML_ja で説明している filelist.rb というサンプルもAttrArrayを使用しています。
なお、属性の展開は別の方法もあります。詳しくは docs/Tour2の expand_attr を参照してください。
モデルデータとid属性のマッチングというamrita独特の方法は、 シンプルで見通しのよいコードを可能にします。
しかし、そのような方法ではきれいに処理しきれないケースも稀には存在します。 Proc オブジェクトをモデルデータとして与えると、 手続き的にテンプレートを変更することができます。
コード:
require "amrita/template"
include Amrita
tmpl = TemplateText.new <<END
<ul>
<li id=list><font id=data></font>
</ul>
END
languages = %w(java Ruby perl python c++ c sml cobol fortran ada lisp)
i = 0
data = {
:list => languages.collect do |l|
{
:data => proc do |elem|
if l == "Ruby" # Ruby is special language to me!
# use Amrita::Element's methods to edit
elem[:color] = "red"
elem[:size] = "big"
elem.set_text("I love Ruby!")
# e() is Amrita's method that generates Element
e(:em) { elem }
else
i = i + 1 # i is shared by all procs
elem[:color] = i%2 == 0 ? "blue" : "black"
elem.set_text(l)
elem
end
end
}
end
}
tmpl.prettyprint = true
tmpl.expand(STDOUT, data)
出力:
<ul>
<li><font color="black">java</font> </li>
<li><em><font color="red" size="big">I love Ruby!</font></em> </li>
<li><font color="blue">perl</font> </li>
...
</ul>
モデルデータとして Proc オブジェクトが渡されると、amrita は、 テンプレート展開時に、そのProc を呼び出します。 その際、パラメータとして、対応する id のついたHTML要素を Amrita::Element オブジェクトとして 渡します。 そして、その Proc の結果とテンプレートを置き換えます。
この Proc の中で、次のようなメソッドを利用して自由にテンプレートを編集することができます。
属性値の設定
elem[:color] = "red"
要素にテキストを設定する
elem.set_text("I love Ruby!")
Amrita#e メソッドによって、新しいHTML要素を生成する
e(:em) { elem }
HashやArrayだけでなく、 既存のクラス(Rubyの標準クラスやユーザ作成のクラス)のオブジェクトを そのまま、amrita のモデルデータとして使用する例です。
コード:
require "amrita/template"
include Amrita
tmpl = TemplateText.new <<END
<span id="time">
<span id="year"></span>/<span id="month"></span>/<span id="day"></span>
</span>
END
t = Time.now
t.extend Amrita::ExpandByMember
data = { :time=>t }
tmpl.compact_space = true
tmpl.expand(STDOUT, data)
出力:
2002/7/17
もし、モデルデータが、Amrita::ExpandByMember というモジュールをincludeしていたら、 amritaは id 属性の値をメソッド名と見なして、そのメソッドを呼び出します。
このサンプルでは、+:time+に対応するデータは、Rubyの標準のTimeオブジェクトですが、 ExpandByMember モジュールを extend しています。 それで id属性の値である year をメソッド名とみなし、amritaは t に対してそのメソッドの呼出しを行います。
その結果 <span id="year"></span> という部分は、 t.yearの結果 "2002" と展開され、他の部分も同様に処理されて 次のように展開されます。
<span><span>2002</span>/<span>7</span>/<span>17</span></span>
amrita は 属性のない <span> 要素は削除しますので、最終的な出力は
2002/7/17
となります。
amrita は HTML テンプレートを Ruby のコードにコンパイルすることができます。
コード(table.rbにコンパイラを利用するために追加した分) :
tmpl = TemplateText.new(TEMPLATE) tmpl.use_compiler = true tmpl.set_hint_by_sample_data(data) # これを追加するとそのデータに最適化します tmpl.expand(STDOUT, data) # puts "----code generated by Amrita -----------" puts tmpl.src puts "----code generated by Amrita end -------"
出力はtable.rbと同じですが、 コンパイラの出力したコードとベンチマークが追加されています。
私のCrusoe TM5600マシン(NEC Lavie MX)での出力は次のようになります。
43.068354 seconds for 1000 times without compiling 5.078764 seconds for 1000 times with pre-compiled code
基本的には、次の処理を追加するだけでコンパイラを使用できます。
tmpl.use_compiler = true
これ以降、expand はコンパイルされたRubyコードで実行されます。 prettyprintの機能はサポートされませんが、それ以外は同じ結果になります。
サンプルデータを利用して、最適化を行なうには次の処理を追加します。
And optionally give a sample data to amrita.
tmpl.set_hint_by_sample_data(data)
amritaのコンパイラは、このデータを出力するRubyコードの最適化のために使用します。 従って、渡すモデルデータの構造が変化したら、再度、その新しいデータで set_hint_by_sample_data を呼ぶ必要があります。
amritaのコンパイラは、部分的にインタプリターモードを含めることができます。 部分的に構造が変化するデータに対して、コンパイラを利用する場合は、 サンプルデータの対応する部分(変化するデータの部分)に、nil を渡す必要があります。
コンパイラは、Element::expandを使用するようなコードを対応する個所に挿入します。
このようにして、スピードと柔軟性のトレードオフを自由に取ることができます。
amritaには、XSS対策として、Amrita::Sanitizer というモジュールが組込まれています。 Amrita::Formatter は自動的にこのモジュールを使用します。
I will provide interface to controle sanitizer through Amrita::Template in future release.
require "amrita/template" include Amrita tmpl = TemplateText.new %q[<p id=body>xxx</p>]
data = {
:body=>"I want to insert new line.<br>But I can't"
}
tmpl.expand(STDOUT, data) # <p>I want to insert new line.<br>But I can't</p>
puts
data = {
:body=>noescape { "I can insert new line <br>with escape { ... } <br>But it may be dangerous" }
}
tmpl.expand(STDOUT, data) # <p>I can insert new line <br>with escape { ... } <br>But it may be dangerous</p>
puts
data = {
# The attacker expected amrita to print <p yyy=""></p>XSS attack<p>But amrita sanitize it!</p>
:body=>a(:yyy=>%q["></p>XSS attack here<p]) { "But amrita sanitize it!" }
}
tmpl.expand(STDOUT, data) # <p yyy=""></p>XSS attack here<p">But amrita sanitize it!</p>
puts
tmpl = TemplateText.new %q[<a id=body>href is treated in a special way</a>]
data = {
:body=>a(:href=>%q[javascript:alert('hello')])
}
tmpl.expand(STDOUT, data) # <a href="">href is treated in a special way</a>
puts
xhtml/html 内のテキストとして危険な文字、(<>&) は自動的にエスケープされます。
"<abc>" => "<abc>"
属性値として危険な文字(<>&"')は自動的にエスケープされます。
<a>要素のhref属性のように、URLを値として持つ属性値は、特別扱いされます。
どの属性値を特別扱いするかの詳細については tag.rb を参照してください。
これらの属性値は、次のようにさらに厳しくチェックされます。
* これらの属性値はURLとして許されない文字を持つことはできません * これらの属性値は許されないスキーム(プロトコル指定)を持つことはできません
この条件に違反したら、属性値はnilで置きかえられて<a href="">....</a> のように表示されます。
次のようにsetup_taginfo メソッドを再定義することで、 どの属性をこのように扱うか(扱わないか)をカスタマイズすることができます。
t = TemplateFile.new ...
def t.setup_taginfo
ret = TagInfo.new
ret[:aaa].set_url_attr(:bbb)
ret
end
この場合は、 aaa要素のbbb属性は、URLとしてサニタイズされます。
Amrita::SanitizedString オブジェクトをモデルデータに含めることで、 この機能を無効にすることができます。
t = TemplateText.new '<p id="a">sample_text</p>'
t.expand(STDOUT, { :a=>"<xxx>" }) # => <p><xxx></p>
t.expand(result, { :a=>SanitizedString["<xxx>"] }) # => <p><xxx></p>
この機能は、XSSについて理解した上で、充分注意して利用してください。
なお、もうひとつ同様の効果を得る方法として、escape {...} で モデルデータを囲むという方法もあります。