PHP+MySQL5.5.24 utf8mb4奮闘記。

PHPからUTF8の4バイト文字を入れてたらなんかおかしな文字コードで入ってたのに気がついて四苦八苦したメモ。

環境は
CentOS6.2、
PHP5.3.3(インストールはyum)
MySQL5.5.24(インストールはyum、remiリポジトリから)、
MySQLサーバの文字コードはutf8mb4。

そのままINSERT。

$db = new mysqli($host, $user, $password, $db_name, $port);
$db->query("INSERT INTO mojitest VALUES ('a')");
$db->query("INSERT INTO mojitest VALUES ('あ')");
$db->query("INSERT INTO mojitest VALUES ('𠀋')");
$db->close();

※3つ目のクエリは実際には「𠀋」が直接書かれている。
PHP文字コードはUTF8。

MySQLクライアントで文字コードを表示してみる。クライアントの文字コードもutf8mb4にしてある。

mysql> select hex(text), char_length(text), length(text) from mojitest;
+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| C3A3C281E2809A       |                 3 |            7 |
| C3B0C2A0E282ACE280B9 |                 4 |           10 |
+----------------------+-------------------+--------------+

何この文字コード。見たことない。「あ」は「E3 81 82」だし、「𠀋」は「F0 A0 80 8B」のはず。
てか1文字表現するのに7バイトとか10バイトになってるよ!
3バイト文字すら謎の文字コードに置き換わってるってなんだ?
でもPHPでこのデータを拾ってきて表示しても、ちゃんと表示される。

SET NAMESを使う。

調べてみると、SET NAMESを使うサンプルがいっぱいあったのでやってみた。

$db = new mysqli($host, $user, $password, $db_name, $port);
$db->query('SET NAMES utf8mb4');
// 以下略
+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| E38182               |                 1 |            3 |
| F0A0808B             |                 1 |            4 |
+----------------------+-------------------+--------------+

正しい!
でもSET NAMESはセキュリティ的にダメなんだとか。
http://rhiz.jp/id/171.html
SET NAMESは禁止 – yohgaki's blog

SET NAMESによって文字エンコーディングを変更するとC言語などで書かれたエスケープAPI (libmysql, libpqなど)が想定しているエンコーディングと実際のエンコーディングが異なる状況が発生します。この違いにより、環境によっては文字エンコーディングを利用したSQLインジェクション攻撃が可能になります。

つまり、APIによって文字エンコーディング情報を変更しないと接続情報が更新されず、エスケープAPIを利用していても正しくエスケープできない場合発生する、ということです。

SET NAMES, SET CLIENT ENCODINGを利用しないで、mysql_set_charset, pg_set_client_encodingを利用すれば、このような不整合が発生しないので問題も発生しなくなります。

なるほど。

mysqli::set_charset関数を使ってみる。

SET NAMESはセキュリティ的にダメとの事なので、mysqli::set_charset

$db = new mysqli($host, $user, $password, $db_name, $port);
$db->set_charset('utf8mb4');
// 以下略
+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| C3A3C281E2809A       |                 3 |            7 |
| C3B0C2A0E282ACE280B9 |                 4 |           10 |
+----------------------+-------------------+--------------+

ダメだ。試しにutf8にしてみた。

$db = new mysqli($host, $user, $password, $db_name, $port);
$db->set_charset('utf8');
// 以下略
+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| E38182               |                 1 |            3 |
| 3F3F3F3F             |                 4 |            4 |
+----------------------+-------------------+--------------+

あれ?変わった…。4バイト文字はもちろんダメだけど。

結論。skip-character-set-client-handshakeを設定する。

http://blog.cheki.net/archives/349

PHPはmy.cnfで[mysql]、[client]を設定しようがクライアントの文字コードはビルド時に指定されたキャラクタセット(通常latin1)。
〜中略〜
MySQLの4.1.15以降、5.0.13以降で「skip-character-set-client-handshake」というオプションが追加された。
クライアントからリクエストがあった場合、クライアントの文字コードをサーバの文字コードと同じものをセットする。

確かにcharacter_set_name関数の戻り値を表示すると「latin1」になる。
早速設定。

[mysqld]
...
character-set-server=utf8mb4
skip-character-set-client-handshake
mysqldを再起動して、イチバン最初のコードをそのまま実行してみる。

+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| E38182               |                 1 |            3 |
| F0A0808B             |                 1 |            4 |
+----------------------+-------------------+--------------+

キター!!
長かった。

でもあのC3で始まる謎の文字コードはなんだったんだろうか。