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]mysqldを再起動して、イチバン最初のコードをそのまま実行してみる。
...
character-set-server=utf8mb4
skip-character-set-client-handshake
+----------------------+-------------------+--------------+ | hex(text) | char_length(text) | length(text) | +----------------------+-------------------+--------------+ | 61 | 1 | 1 | | E38182 | 1 | 3 | | F0A0808B | 1 | 4 | +----------------------+-------------------+--------------+
キター!!
長かった。
でもあのC3で始まる謎の文字コードはなんだったんだろうか。