モダンPerlの世界へようこそ

第41回 HTML::Template::Pro:テンプレートに極力コードを書かせたくないときは

この記事を読むのに必要な時間:およそ 5 分

何でも埋め込めるのは楽ですが

前回紹介したHTML::MasonText::MicroTemplateのように生のPerlコードを埋め込めるテンプレートエンジンは,Perlをよく知っている人が画面の設計からウェブアプリケーションのコーディングまでひとり(ないし,よく統制のとれた少人数のチーム)で行うときには非常に手軽で便利なものです。

ただし,なんでも書けるからといって,たとえばテンプレートの中でO/Rマッパのメソッドを直接呼び出すコードを書いてしまうと,そのテンプレートは(利用するO/Rマッパの性質にもよりますが)おそらく実際に動作するデータベースやそれに付随するテストデータを用意しないと,途中で「Can't call method "..." on an undefined value」などのエラーが発生してレンダリングできなくなってしまいます。また,アプリケーションの設定にあわせてURLを生成するコードが埋まっていたり,コンテキストオブジェクト中のセッションオブジェクトの中のパラメータの値によって条件分岐するコードが埋まっている場合もアプリケーション本体やコンテキストオブジェクトが必要になるでしょうし,このようなオブジェクトは概して複雑なものになりがちですから,初期化に時間がかかったり,レンダリングには不要な大量のパラメータを渡したりしなければならなくなります。

そのこと自体は,モデル・ビュー・コントローラのような大きな枠組みの中ではある程度仕方のないことですが,このままでは,たとえばクロス・サイト・スクリプティング(XSS)対策が正しくできているか確認するテストを書きたくなっても,テンプレート単体でのテストはできず,テストのたびにデータベースなりアプリケーションなりを用意する作業が必要になってしまいます。これではテストも書きづらいですし,テンプレートを書き換えるたびにテストを走らせようという気にもなれません※1)⁠

※1

テンプレートのテストというとピンと来ない人もいるかもしれませんが,XSS対策などは,基本的にはテンプレートとテストデータがあればテストできるものです。危険な文字はテンプレートエンジンのほうでかならず実体参照に変換するようになっているのでテストは省略しても大丈夫と判断できる場合もありますが,自動でエスケープできるテンプレートエンジンでもファイルインクルードなどに対応するためにエスケープなしでデータを埋め込めるような抜け道は用意されているものですし,ユーザが任意のHTMLやCSSを埋め込めるようになっているような場合は,実体参照への変換だけでは十分な対策とは言えません。最終的にはブラウザの実装の違いや併用しているJavaScriptの問題なども考慮する必要があるのでPerlレベルのテストだけでは完璧とは言えませんが,セキュリティまわりのバグは周囲に与える影響も大きいので,これで大丈夫なはずと過信するよりは,可能な範囲でテストを書き,またテストを書けるようにしておいたほうが後々のためです。

テンプレートからコードを追い出す

この問題を解決する方法はいくつか考えられますが,おそらくもっとも根本的な解決策は,テンプレートや,テンプレートをレンダリングするビューのクラスを整理して,外部依存しがちなコードをできるだけ外に出してしまうことでしょう。

いわゆるモデル層については,連載第8回でもReactionの例を紹介したように,データベースなどの外部ツールをラップする層と,O/Rマッパより上位のビジネスロジックを格納する層に分割するやり方などが知られるようになってきましたが,ビュー層も同じように,テンプレートに(オブジェクトではない)静的なデータを埋め込むエンジン部分と,各種のオブジェクトからテンプレートに埋め込むデータを取り出すロジック部分を分けてやれば,少なくともXSS対策などのテストは簡単になります。

もう少し具体的にいうと,たとえばO/Rマッパのイテレータなどは,そのままテンプレートに埋め込むのではなく,テンプレートの外であらかじめイテレータを回して必要なデータを配列やハッシュに格納してから,その配列やハッシュをテンプレートに渡すようにすればデータベースへの依存を取り除けるでしょうし,テンプレートに複雑な条件分岐がある場合は,その条件もあらかじめ簡単な変数にまとめておくと,わざわざレンダリングしなくてもその条件分岐の妥当性をテストできます。

このような処理は,メモリの使用量や処理時間の点ではかならずしも効率がよいとは限りませんし,アプリケーションやフレームワークの仕様が複雑になりすぎると結局どこにコードを記述すればよいかわからなくなって,見た目にもわかりやすいテンプレートにコードを埋め込んでお茶を濁すようなことにもなりがちですから,何が何でも関心の分離を追求すればよいというものでもないのですが,そのような制約を承知のうえで安全面を優先するなら,作業する人次第でどうにでもなってしまう約束事に頼るよりは,最初からテンプレートの中では任意のメソッドを呼び出せないテンプレートエンジンを使うほうが確実です。そのようなコードとデザインの分離に重点をおいたエンジンの代表例が,今回取り上げるHTML::Templateとその仲間です。

HTML::Template

ドットコムバブル真っ盛りの1999年6月に生まれたHTML::Templateは,既存のHTML::MasonやHTML::Embperlを使ったアプリケーションではないがしろにされがちだったコードとテンプレートの分離を強制するために,簡単な条件文やループのほかは,変数の値を埋め込むことしかできないようになっているのが最大の特徴といえます。

一例として,CPANの更新情報データベースから最新10件のデータを取り出して一覧にするHTMLページのテンプレートを考えてみましょう。ここでは連載第39回で紹介したORDB::CPANUploadsを利用してデータを取得することにします。第39回の記事ではORDB::CPANUploads::Uploadsのiterateメソッドを使って一行ごとに処理していきましたが,今回はselectメソッドを使って該当行のオブジェクトを一気に取り出してみました。生のPerlを扱えるエンジンであればselectメソッドの結果をそのままテンプレートに渡せばよいのですが,HTML::Templateの場合,テンプレートの中ではオブジェクトのメソッドを呼び出せないので,あらかじめ配列の中のオブジェクトの値を取り出しておく必要があるのがポイントです※2)⁠

use strict;
use warnings;
use HTML::Template;
use ORDB::CPANUploads;
use Time::Piece;

my $users = ORDB::CPANUploads::Uploads->select("order by released desc limit 10");

my $template = <<'TMPL';
<html>
<body>
<TMPL_IF NAME="users">
<ul>
<TMPL_LOOP NAME="users">
<li><TMPL_VAR NAME="dist"> (<TMPL_VAR NAME="author">; <TMPL_VAR NAME="released">)</li>
</TMPL_LOOP>
</ul>
<TMPL_ELSE>
<p>Not found</p>
</TMPL_IF>
</body>
</html>
TMPL

my %params = (users => [
  map { +{
    dist     => $_->dist,
    author   => $_->author,
    released => Time::Piece->new($_->released)->ymd,
  }} @$users
]);

my $ht = HTML::Template->new(
  scalarref      => \$template,
  default_escape => 'HTML',
);

$ht->param(%params);
$ht->output(print_to => \*STDOUT);

なお,HTML::Templateでは埋め込む値をparamメソッドで渡すのが原則ですが,CGI.pmApache::RequestなどCGI.pm互換のparamメソッドを持つオブジェクトの値をそのまま埋め込みたい場合は,associateオプションを利用することでparamの値をコピーする作業を省略できます。


use strict;
use warnings;
use HTML::Template;
use CGI;

my $cgi = CGI->new;
my $ht = HTML::Template->new(
  associate      => $cgi,
  default_escape => 'HTML',
);

# $ht->param($_ => $cgi->param($_)) for $cgi->param; は不要
※2

ここでは値を取り出す部分をスクリプトの中にベタ書きしていますが,実際のアプリケーションではORDB::CPANUploadsをラップして,返り値にオブジェクトを含まないようにしたほうがよいでしょう。あるいは,O/Rマッパを使わず素のDBI(と,必要ならSQL::AbstractSQL::MakerのようなSQLを構築するためのモジュール)を使うことにすればfetchrow_arrayrefなどで取り出した値をそのまま利用できます。

著者プロフィール

石垣憲一(いしがきけんいち)

あるときは翻訳家。あるときはPerlプログラマ。先日『カクテルホントのうんちく話』(柴田書店)を上梓。最新刊は『ガリア戦記』(平凡社ライブラリー)。

URLhttp://d.hatena.ne.jp/charsbar/

コメント

コメントの記入