cakePHPのHtmlHelper::inputメソッドのダブルエスケープ問題

cakePHPでダブルクオートとかのhtmlspecialcharsの対象になる文字をテキストフィールドに入れると、エスケープが一回分多くて <input value="&amp;quot;" /> になって、ダブルクオートが一回エスケープされた状態でブラウザに出ちゃう問題で悩む。ようするにどこかでエスケープされたやつがさらにエスケープされてる。バージョンは cake_1.1.14.4797 です。

libs/view/helpers/html.php をみると

/**
 * @deprecated Name changed to 'input'. Version 0.9.2.
 * @see HtmlHelper::input()
 */
    function inputTag($tagName, $size = 20, $htmlOptions = null) {

って書いてあるのでinputTagではなくてinputを使う。

で、そのinputがくせもの。

    function input($fieldName, $htmlAttributes = array(), $return = false) {
        $this->setFormTag($fieldName);
        if (!isset($htmlAttributes['value'])) {
            $htmlAttributes['value'] = $this->tagValue($fieldName);
        }

っていうところでタグのvalueの値をとってきてます。tagValueの中はこう。

    function tagValue($fieldName) {
        $this->setFormTag($fieldName);
        if (isset($this->params['data'][$this->model][$this->field])) {
            return h($this->params['data'][$this->model][$this->field]);
        } elseif(isset($this->data[$this->model][$this->field])) {
            return h($this->data[$this->model][$this->field]);
        }
        return false;
    }

意味分かんないですが、なににしてもhtmlspecialcharsを意味するhを通して返します。ここで一度エスケープされてるわけです。

個人的には一時的な変数ならともかく、変数名に絶対dataとかつけません。変数にはなにかしらのデータが入ってんだからdataじゃ名前つけてる意味がねーんだよ!でもcakePHPでは $this->data はコントローラがフォームから受け取った値が入ることになってます。それも古いバージョンでは $this->params['data']に入っていたのを新しいバージョンでは $this->data に入れるように仕様を変更してこうなっています。そのときついでに名前も意味のあるものに変えてほしかった….

inputをさらに追って行くと最後のところで

return $this->output(sprintf($this->tags['input'], $this->model, $this->field, $this->_parseAttributes($htmlAttributes, null, ' ', ' ')), $return);

こうなってて、この_parseAttributesがタグのattributeの値をタグとしてシリアライズしています。中をみると

    function _parseAttributes($options, $exclude = null, $insertBefore = ' ', $insertAfter = null)
 {
        if (is_array($options)) {
            if (!is_array($exclude)) {
                $exclude = array();
            }
            $keys = array_diff(array_keys($options), $exclude);
            $values = array_intersect_key(array_values($options), $keys);
            $out = implode(' ', array_map(array(&$this, '__formatAttribute'), $keys, $values));
        } else {
            $out = $options;
        }
    trace($out);
        return $out ? $insertBefore . $out . $insertAfter : '';
    }

なにやらarray_mapでattribute全部に__formatAttributeを適用しています。その中身は

    function __formatAttribute($key, $value) {
        $attribute = '';
        $attributeFormat = '%s="%s"';
        $minimizedAttributes = array('compact', 'checked', 'declare', 'readonly', 'disabled', 'selected', 'defer', 'ismap', 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize');

        if (in_array($key, $minimizedAttributes)) {
            if ($value === 1 || $value === true || $value === 'true' || $value == $key) {
                $attribute = sprintf($attributeFormat, $key, $key);
            }
        } else {
            $attribute = sprintf($attributeFormat, $key, htmlspecialchars($value));
        }
        return $attribute;
    }

おーいやっぱり2回エスケープしてるよー。どーすんだよこれ。不用意にエスケープするのを減らすのは怖いから外側で1回アンエスケープするのが無難か。探したかんじみんな困ってなさそうなのでinputTagつかえば解決なんでしょうか。

解決でした。

function inputTag($tagName, $size = 20, $htmlOptions = null) {

っていうsizeだけ特別扱いのへんなメソッドなのでinputからfall backするときは

$size = 20;
if ( array_key_exists( 'size', $attributes ) ) {
    $size = $attributes['size'];
    unset($attributes['size']);
}
$this->html->inputTag($id, $size, $attributes);

こうなります。cakePHPはこんなのばっかりでもうやいだ。こういうのがなくなるのをあと一年くらい待っといた方がいいと思います。

追記

shin1×1さんにコメントいただいたとおり、新しいバージョンのcakePHPでは修正されているそうです。


About this entry