Smartyのバグ ~default_modifiers~

わかりやすいネタのほうがソーシャルブックマーク数が稼げるからこういうネタを掲載するわけじゃないです。
そう、ぼくはただこの土日を使って社内のイケメンリストを作りたかったんです。
硬派なぼくはerror_reportingは常時E_ALLだし、クロスサイト・スクリプティング(以下XSS)の脆弱性を産むようなミスは犯したりしないゼ。
へなちょこでもいい。たくましいプログラミングをしたいんだ。

しかし悲劇は起こりました。

1 イケメン一覧スクリプトを作成する

まず、Asial社内のイケメンリストを配列に持ち、Smartyのassignメソッドで配列をテンプレート変数に割り当て。
また、XSSの脆弱性を未然に防ぐため、Smartyのdefault_modifiersにescapeをセットし、自動的にhtmlspecialchars関数がかかるように設定します。

ikemen_list.php


<?php
// イケメンリストの配列を作成
$ikemen_list[0]["name"]       = "海原";
$ikemen_list[0]["nickname"]   = "saity";
$ikemen_list[0]["honey_flg"]  = "0";
$ikemen_list[1]["name"]       = "田中";
$ikemen_list[1]["nickname"]   = "massie";
$ikemen_list[1]["honey_flg"]  = "1";
$ikemen_list[2]["name"]       = "森川";
$ikemen_list[2]["nickname"]   = "joe";
$ikemen_list[2]["honey_flg"]  = "1";
$ikemen_list[3]["name"]       = "松田";
$ikemen_list[3]["nickname"]   = "mattsun";
$ikemen_list[3]["honey_flg"]  = "0";
require_once("../libs/Smarty.class.php");
$smarty = new Smarty();
$smarty->template_dir = "/home/saito/templates";
$smarty->compile_dir  = "/home/saito/templates_c";
$smarty->default_modifiers = array('escape');
$smarty->assign("ikemen_list",$ikemen_list);
$smarty->display("ikemen_list.html");

次に、テンプレート側のスクリプトを組みました。
ちゃんと値がassignされたかを確認するために、{debug}関数を挟みます。

ikemen_list.html


<html>
<head>
<title>社内モテリスト</title>
<body>
{debug}
<h1>アシアルのイケメン一覧表</h1>
{foreach from=$ikemen_list item=ikemen name="motefoobar"}
  {$smarty.foreach.motefoobar.iteration}:
  {$ikemen.name}:
  {$ikemen.nickname}:
  {if isset($ikemen.honey_flg)  & & $ikemen.honey_flg}彼女有{else}彼女無{/if}
  <br>
{/foreach}

そして実行。

2 悲劇の始まり ~Smartyのバグ~


ナニィィィ!!?syntax error(構文エラー)だとォ?
どこを何度見ても構文を間違えている様子はないですよね。

仕方ないので
50^50A^50AAF14B%%ikemen_list.html.php
このコンパイル後の24行目で、何が起こっているのかを確かめてみることにします。
問題となっているのがこのくだり。↓


<?php if (isset ( ((is_array($_tmp=$this->_tpl_vars['ikemen']['honey_flg'])) ? $this->_run_mod_handler('escape', true, $_tmp) : smarty_modifier_escape($_tmp)) | )  & & ((is_array($_tmp=$this->_tpl_vars['ikemen']['honey_flg'])) ? $this->_run_mod_handler('escape', true, $_tmp) : smarty_modifier_escape($_tmp))): ?>彼女有<?php else: ?>彼女無<?php endif; ?>

変数の中身を判定して、修飾子の処理を噛まそうとしてます。
このif文が構文エラーとなっているわけですが、要約すると


if (isset((is_array(($a) ? $b : $c)))) {

さらにもうちょっと解りやすく噛み砕くと、


if (isset(($a = $b))){

コレが構文エラーの問題です。
phpマニュアルでisset関数を調べてみると、『Warning: isset() は何らかの値が渡された変数の場合のみ動作します。そうでない場合、パースエラーとなります。』とありました。isset関数で、式を評価してはいけませんね!

対策として、修飾子を実行させないよう


{if isset($ikemen.honey_flg|smarty:nodefaults)  & & $ikemen.honey_flg}彼女有{else}彼女無{/if}

として回避しました。

そして再実行

3 default_modifiersを設定していると{debug}もダメ


またsyntax error!?
どうせ先ほどと同じ問題だと思ったので、中身を見てみると案の定。
しかし、この問題を回避するのは難しそうです。
default_modifiersをセットすると、{debug}関数が使えないことが判明しました。
解決方法があったら、どなたか教えてください。

気を取り直し、{debug}を取り除いて再実行

4 {foreach}する配列もdefault_modifiersの対象となる


どうやら今度は、escape修飾子関数内で問題が起こっているようでした。
プラグインの中の
modifier.escape.php
を調べてみることにしました。


function smarty_modifier_escape($string, $esc_type = 'html', $char_set = 'ISO-8859-1')
{
    switch ($esc_type) {
        case 'html':
            return htmlspecialchars($string, ENT_QUOTES, $char_set);
以下略

この第1引数に配列が渡ってきちゃうと、htmlspecialchars関数に配列を渡すことになってしまいます。
詳しくコンパイルファイルを調べてみると、やはりdefault_modifiersが悪さをしていました。
ループしたい変数が配列の場合、_run_mod_handlerというメソッド内で配列の中身をもすべて分解してdefault_modifire処理をかけようとします。
しかし_run_mod_handlerメソッド内では、1次元配列までにしか対応していませんでした。だから、今回のように2次元配列である$ikemen_listをループする場合、1次元配列にescape修飾子処理をかけようとしてWarningが出ちゃうんです。

かなり付け焼刃的な措置ではありますが、配列が渡ってきた場合は無視するよう追記します。


function smarty_modifier_escape($string, $esc_type = 'html', $char_set = 'ISO-8859-1')
{
    // 配列が渡ってきたら何もせずにreturn
    if (is_array($string)) return $string;
    switch ($esc_type) {
        case 'html':
            return htmlspecialchars($string, ENT_QUOTES, $char_set);
以下略

本当なら、多次元配列に対応できるように修正すべきなんだけど、めんどうなので「あとでやる」。
(ここはむしろ新ジャンル「だれかやって」)

ということで、改めて実行

ぃよっしゃー!

5 まとめ

・$default_modifiersを設定すると、{debug}が使えない
 対策→わからないのでだれか教えてください

・$default_modifiersを設定すると、2次元配列を{foreach}できない
 対策→plugin/modifier.escape.phpを配列が渡ってきても大丈夫なように改造する

・$default_modifiersを設定すると、{if isset($variable)}のように、if文内でisset関数が使えない
 対策→{if isset($variable|smarty:nodefaults)}としてdefault_modifiersを無効にする