PEAR::MDB2のプリペアドステートメント

はまちちゃんも「SQLインジェクションに対してはプリペアドステートメントが有効」って言ってた(5分でできるPHPセキュリティ対策 - ぼくはまちちゃん!(Hatena))ので、やってみることにした。
だけどエラーに悩まされたので、そのことを書いておく。

やったことを時系列に

  1. MDB2を導入
  2. 自分が作ってるアプリで使用(下記のような感じで)
    1. アップロードされたCSVを解析
    2. 各フィールドの型判定
    3. CREATE TABLE実行
    4. INSERT INTO実行
  3. INSERT INTOでエラー
    • エラーメッセージは「MDB2 Error: not supported」
  4. エラー箇所特定の方法を模索
    • エラーのハンドラで例外をthrowしてcatchしない
  5. エラー箇所特定、修正→INSERT成功
  6. 上記をブログにまとめ始める
    • 分かりやすくするためにテーブルをシンプルに
  7. intをintegerに、doubleをfloatに書き換えたけどエラー
    • エラーメッセージは「MDB2 Error: unknown error」
  8. unknown errorに関する情報収集
  9. まさかとは思いつつMDB2をstableを消してbetaへ変更
  10. 同じPHPスクリプトを実行、エラー無し
  11. (゚д゚)ウマー

僕のPHPの環境

以下の環境のデフォルトのまま

  • Pleiades 3.7(2011年09月24日版)
  • Pleiadesに同梱されているXAMPP version 1.7.4

エラーが起こった箇所の特定に関して

自分の書いたコードに絶対間違いがあるはず。だけど間違いが全然分からない。そうとなれば、ライブラリ内も含めてデバッグしたい。それができれば、自分が与えた引数のうちどれが間違ってるか調べられるだろうなー、なんて。
PEARには、引数で与えられたオブジェクトがエラーを表すオブジェクトかどうか確かめるPEAR::isErrorってのがある。プリペアドステートメントをexecuteした後、エラーが発生していれば戻り値としてこのエラーが返ってくることもある。でも、これだと素通りしてしまう。エラーメッセージは「MDB2 Error: not supported」で、何も有益な情報を与えてくれない。
いろいろ調べていると、エラー処理の方法にPEAR::isError以外の方法があることが分かった。それは、PEAR::setErrorHandlingというメソッド(関数?)を使って、エラーのハンドラをコールバックとして登録することだった。
ということで、これを利用して、ハンドラの中で例外をthrowしてcatchしなければ、Fatal errorで止まってくれて、XAMPPが表示してくれるコールスタックを手掛かりに位置が特定できるだろうと考えた。
やってみると、確かにできたが、XAMPPが表示するコールスタックにはディレクトリ無しのファイル名しかなく*1、ファイル名で検索すると同名のファイルがいくつも見つかってしまい、エラーの位置の特定ができない(やりたくない)感じだった。どうにかやってフルパスを知る方法は無いかと考えてたら、Exceptionクラスはtraceフィールドにフルパスで持ってたので、これを頼りにエラー箇所を特定できた。

MDB2 Error: not supportedの原因

直接の原因は、MDB2_Driver_Datatype_mysqliというクラスが_quoteintというメソッドを持っていない事だと分かった。クラス調べてみると確かに無い。でも親クラスのMDB2_Driver_Datatype_Commonを見てみると、_quoteIntegerっていうメソッドがあった。
実際に実行していたスクリプトはこんな感じ

<?php
// $sql と $types は動的に作った
$sql = 'INSERT INTO test VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$types = array('int','int','int','int','double','double','int','int','int','int','int');

// 用意
$stmt = $mdb2->prepare($sql, $types, MDB2_PREPARE_MANIP);

// 実行
for (レコードの個数回)
{
  $rec = レコードを配列に詰める();
  $stmt->execute($rec);
}
?>

ここで、MDB2::prepareの第2引数に指定している 'int' がダメなのかなと思い、'Integer' にしたら、今度は_quotedoubleが無い、と言われたので 'Float' にしてみたらできた*2
INSERT INTOする前にCREATE TABLEしたとき、アップロードされたCSVファイル(健康診断のデータ)を解析して生成したSQLは以下の通り。

CREATE TABLE test (
  `年齢` int,
  `性別` int,
  `飲酒度` int,
  `喫煙度` int,
  `BMI` double,
  `HbA1c` double,
  `GPT` int,
  `GGT` int,
  `TG` int,
  `TCho` int,
  `HDL-C` int
);

だから何の疑いも持たずに 'int' と 'double' って書いた。だけど、PEAR::MDB2DBMSの違いを吸収するためにあるので、改めて考えてみれば特定のDBMSでの型名がそのまま使えるとは限らないのは当然なんだよなぁ。実際MDB2は独自の型システムを持っていて、各DBMSの型へのマッピングを自動的にやってくれるみたいだ。
参考:http://pear.php.net/manual/ja/package.database.mdb2.datatypes.php
あと、フィールド名を日本語にするのは多分良くないんだろうなぁ。

まだだ、まだ終わらんよ

せっかくだから、上記でやったことをブログにまとめとこうと思い、書き始めた。状況をシンプルにするために、テーブルは

CREATE TABLE hoge (
  foo int,
  bar double
);

と決め打ちで作成。
まずはエラーになるはずのケース。次のスクリプトを実行。

<?php
// ... 略 ...
$sql = 'INSERT INTO hoge VALUES (?, ?);';
$sth = $mdb2->prepare($sql, array('int', 'double'), MDB2_PREPARE_MANIP);
$sth->execute(array(1, 3.14));
// ... 略 ...

確かにnot supportedのエラーになった。
つぎに、成功する気満々で以下のスクリプトを実行。'int' を 'integer' に、'double' を 'float' にしただけ。

<?php
// ... 略 ...
$sql = 'INSERT INTO hoge VALUES (?, ?);';
$sth = $mdb2->prepare($sql, array('integer', 'float'), MDB2_PREPARE_MANIP);
$sth->execute(array(1, 3.14));
// ... 略 ...

なんと失敗。本当に意味不明。いらいら。エラーメッセージを見ると「MDB2 Error: unknown error」だと。もっと有益なことを言えよこの が!!!not supportedの時と同じく、コールスタックを頼りに場所は分かったけど、エラーの理由は全く見当つかない。

Google先生

若干14歳のgoogle先生にすがると、こんなの教えてくれた。とある書籍のサポートページのようで、「INSERT文でunknown errorになる」との読者からの問合せ。返答として「MDB2のバージョンが2.5.0b3かどうか確認しろ」とのこと。

MDB2の2.5.0b3導入成功

まさかとは思いつつベータ版をインストール。実はインストールでもつまずいたけど、だんだん書くの疲れてきました。

INSERT成功!

(゚д゚)ポカーン

所感

これで丸一日費やした。疲れるなぁ...。

not supportedの件

思い込みはダメ。ちゃんと疑問を持ってドキュメントを参照しましょう。あとprepareの第2引数以降は省略可能ですよ...orz

unknown errorの件

公式マニュアルのサンプル(「http://pear.php.net/manual/ja/package.database.mdb2.intro-execute.php」の「実行 (Execute)」の最初のスクリプトとか)はstableで動かなくね?勘弁してくださいよ...。

*1:後で気付いたけど、HTMLのtd要素のtitle属性でフルパスを保持しているようだ

*2:頭文字は大文字にする必要はなく、integerとfloatでいいみたい