PHPのバージョンでZend_Jsonの動作が違っていた件2008年04月06日 04時05分27秒

Zend_Json::decode()って便利!と思っていたが

Zend_Json::decode()が割と使える。UNICODEエスケープされた文字(\uXXXX形式ね)をデコードできるからだ。

<?php
require_once 'Zend/Json.php';

$s = '"\u65e5\u672c\u8a9e"';
echo Zend_Json::decode( $s );

とかってすると、「日本語」と出力が得られる(内部エンコードがutf-8じゃないとあかんみたいだが)。

だもんで、クライアント側でJSでescape()したマルチバイト文字を、preg_replace_callbackを絡めて'%uXXXX'を'\uXXXX'に変換した上でダブルクォートで囲ってZend_Json::decode()に渡すようにして復元したりしていた。

が、Zend_Jsonの動きをたいして気にしていなかったため、ちみっとハマった。

PHPのバージョンの違いで、なんかヘン。

ある環境では上記のようなデコード処理がまったく問題なく動いていたのだが、他の環境で動かしたとたんに'\uなんて不正なエスケープだ!'とエラーがでるようになった。Zend Frameworkのバージョンはどちらも「1.0.0」を使っているのに。

文字コードの関連も、実行環境に依存しないように必ずdefault_charsetとmbstring.internal_encoding、mbstring.http_outputをコード中で指定してutf-8にあわせてあるし、違いといえばPHPのバージョン。

ためしに、

<?php
require_once 'Zend/Json.php';

$s = '日本語';
echo Zend_Json::encode( $s );

なんてのをやってみたところ、正常に動作する環境は
"\u65e5\u672c\u8a9e"
とUNICODEエスケープで出力されたが、うまく動かない環境のほうでは
"日本語"
と、まんまで出力されている。はて。

ソースを覗いてみたら

エンコード部分で動作に違いがでたので、Zend/Json/Encoder.php(Zend_Json_Encoder)のソースを見てみた。該当するのは _encodeString プロテクトメソッドか。

/**
 * JSON encode a string value by escaping characters as necessary
 *
 * @param $value string
 * @return string
 */
protected function _encodeString(&$string)
{
    // Escape these characters with a backslash:
    // " \ / \n \r \t \b \f
    $search  = array('\\', "\n", "\t", "\r", "\b", "\f", '"');
    $replace = array('\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', '\"');
    $string  = str_replace($search, $replace, $string);

    // Escape certain ASCII characters:
    // 0x08 => \b
    // 0x0c => \f
    $string = str_replace(array(chr(0x08), chr(0x0C)), array('\b', '\f'), $string);

    return '"' . $string . '"';
}

(Zend/Json/Encoder.php より抜粋)
はて、マルチバイト文字の扱いとか特にやってる風ではないな。どないなっとんねん。

じゃあ、実際にコードから叩いているZend_Jsonのほうを見てみるか。

/**
 * Encode the mixed $valueToEncode into the JSON format
 *
 * Encodes using ext/json's json_encode() if available.
 *
 * NOTE: Object should not contain cycles; the JSON format
 * does not allow object reference.
 *
 * NOTE: Only public variables will be encoded
 *
 * @param mixed $valueToEncode
 * @param boolean $cycleCheck Optional; whether or not to check for object recursion; off by default
 * @return string JSON encoded object
 */
public static function encode($valueToEncode, $cycleCheck = false)
{
    if (function_exists('json_encode') && self::$useBuiltinEncoderDecoder !== true) {
        return json_encode($valueToEncode);
    }

    require_once 'Zend/Json/Encoder.php';
    return Zend_Json_Encoder::encode($valueToEncode, $cycleCheck);
}

(Zend/Json.php より抜粋)
ほあ!?json_encode()??

はぁ、PHP5.2.0からだったのねん

同様にZend_Json::decode()部分もjson_decode()が存在していたらそっちを利用するようになっていた。調べてみるとこの2つの関数は、JSON関数として、PHP5.2.0からは標準でインストールされるようになったPECL拡張モジュールで提供されている関数だったと。

先ほどの2つの環境、うまく動かないほうは5.1.6、正常なほうは5.2.5だもんで、なるほどこの通りになるのか。

先ほどのjson拡張モジュール自体はPHP4.3.0以降に適合するので、それをインストールすれば同様の動作になるけど、Zend_Json関連を使うときは一応PHPのバージョンを気にしておいたほうがよいかも。

オマケ

前半部分でescape()した文字のデコード目的で使用、ってな話を書いたけど、「escape()って必ずUNICODEエスケープなのか?」ってのに自信がなくなったので調べてみたら、こんな一覧表が見つかった。

なるほど現在普通に使われるようなブラウザならたいていUNICODEエスケープとみて間違いないかな(MacユーザでiCab使ってる人いたらごめんなさい)。

SafariがWin2Kで安定動作するみたいっす!!2008年04月11日 03時47分27秒

素晴らしいトラックバックをいただきました!

どうやらWin2Kのadvapi32.dllに実装されていないAPIをコールすることが原因のようで、トラバ元の黒翼猫さんはこれを補完するラッパーDLLを開発され、配布されています。

ただ、OSのDLLを置き換えるため、

操作手順を間違えるとWindowsが起動しなくなってしまいます。
というリスクはあるようですが、同梱のマニュアルに詳細な手順や万が一の時の復旧方法が記載されているので、試してみる価値は大いにあるでしょう。(とかいいながらdara-jもまだ試していないので、近いうちに試して記事にしよう)

黒翼猫さん、素晴らしい情報をありがとうございました!

コメントとトラバの上限が設定されるらしい。2008年04月23日 03時14分43秒

昨今増えているスパム(迷惑)コメント、スパムトラックバック対策のため、近日中に1記事あたりのコメントとトラックバック受付数上限を設けることにいたしました。
ありがたい。めっさありがたい。1日あたり30も40もクスリのトラバがやってくる現状からすると、ほんとうにありがたい。

が、「上限50」とかだったら俺的に実質意味ないし。それはそれで笑えてよいかも。

古いspamコメント/トラバの削除を支援するGMスクリプト2008年04月23日 03時51分27秒

しつこくアサブロ管理画面向けのGMスクリプトを載せてみたり。

コメント一覧/トラックバック一覧で状態が「spam」の項目に自動的にチェックをつけます。以下のスクリプトを「http://www.asablo.jp/app*」で動作するようにしてください。

※:なんかうまく動かなくなったので、修正入れました。詳しくはこちら。(09.09.04)

// 指定要素を含むtrを取得する
function findRow(el) {
  if( el == document.body ) return null;
  
  var row = el.parentNode;
  if( /^tr$/i.test( row.tagName ) ) {
    // 「状態」を取得するメソッドを追加
    row.getStatus = function() {
      var sel = this.getElementsByTagName("select")[0];
      if( sel ) {
        return sel.options[ sel.selectedIndex ].innerHTML;
      }
      return null;
    };
    // 日付を取得するメソッドを追加
    row.getDate = function() {
       var cells = this.getElementsByTagName("td"), i = 0;
       while( i < cells.length ) {
         var cell = cells[i++];
         if( /\d{4}((\-\d{2}){2})\s\d{2}((:\d{2}){2})/.test( cell.innerHTML ) ) {
           return new Date(
             cell.innerHTML.replace(/\-/g, "/").replace( /\s\d{2}(:\d{2}){2}/, "" )
           );
         }
       }
       return null;
    };
    return row;
  }
  
  // elの親がtrじゃないのでさらに上位に遡る
  return arguments.callee( row );
}

// [状態の変更/~の削除]ボタンを検出する
function getSubmit() {
  var list = window.document.getElementsByTagName("input")
  for(var i = 0, l = list.length; i < l; i++) {
    if( list[i].type == "submit" && /状態の変更/.test( list[i].value ) ) {
      return list[i];
    }
  }
  return null;
}

// チェックボックスのリスト → リストの取得方法変更(09.09.04)
//var list = document.getElementsByTagName("input");
var list = document.getElementsByName("delmsg");
// チェックボックスをチェックした数
var checked = 0;
// 現在日よりこの日数以上古いデータは削除対象にする
var days = 5;

// 削除対象のコメント/トラバをチェック
for(var i = 0, l = list.length; i < l; i++) {
  var input = list[i++];
  if( input.type != "checkbox" || input.name != "delmsg" ) continue;
  
  new function() {
    var row = findRow(input);
    var d = row.getDate();
    var limit = new Date( new Date() - ( 86400000 * days ) );
    if( row.getStatus() == "spam" && d <= limit ) {
      input.checked = true;
      checked++;
    }
  }();
}

// ボタンをハイライト
var btn = getSubmit();
if( checked && btn ) {
  btn.style.color = "red";
  btn.style.fontWeight = "bold";
  btn.focus();
  document.documentElement.scrollTop = document.documentElement.scrollHeight;
}

ノードの抽出だとかDOM操作はちょっとグダグダしてるが勘弁。

コード中の下線部の「var days = 5」の数字は、削除対象にする日数で、例えば本日(2008年4月23日)だと、4月19日よりも前のspamトラバ/コメントにチェックがつきます。「5日も待てねぇ。さっさと削除してぇ。」とかいった場合はここを変更すると。

また、チェックをつけた項目があったページでは[状態の変更/~の削除]ボタンを強調表示し、そこまで画面をスクロールさせてます。

あくまでチェックをつけるだけなので、実際の削除は[状態の変更/~の削除]を自分でクリックしてください。

うーん、またあんまり需要がないスクリプトを書いてしまった。