PHPの大御所「Do You PHP?」の中の人が困っていた問題に、同僚がずっぽりハマってしまった。

「multipart/form-data使ってアップロード」で助けて~ – Do You PHP はてな
http://d.hatena.ne.jp/shimooka/20080526/1211792488

PHP5.2.6で「multipart/form-data使ってアップロード」の続き – Do You PHP はてな
http://d.hatena.ne.jp/shimooka/20080527/1211872306

ファイルをアップロードするための「enctype=“multipart/form-data”」なフォームからPOSTされた内容が、PHPの文字コード変換(mbstring.http_input -> mbstring.internal_encoding)を通らない、という不具合だ。

検証ページを書いてみる。

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
    <body>
<table border="1" cellspacing="0">
<?php
// マルチバイト系の設定を表示
$mb_param = array(
    'output_handler',
    'mbstring.language',
    'mbstring.internal_encoding',
    'mbstring.http_input',
    'mbstring.http_output',
    'mbstring.encoding_translation',
    'mbstring.detect_order',
    'mbstring.func_overload',
    'default_charset',
) ;
foreach ($mb_param as $param)
{
    echo "<tr>\n" ;
    echo "<th align=\"left\">{$param}</th>" ;
    echo "<td>" . ini_get($param) . "</td>\n" ;
    echo "</tr>\n" ;
}
?>
<hr>
<table border="1" cellspacing="0">
    <tr>
        <td>
            <form action="" method="POST">
            <input type="text" name="text" value="フォームから送信される文字列" size="40">
            <input type="submit" name="post_form" value="POSTフォームから送信">
            </form>
        </td>
        <td>
        <?php
        // [A] 通常のPOSTフォームから送信された文字列とエンコーディングを表示
        if (isset($_POST['post_form']))
        {
            echo "<td>{$_POST['text']}</td>" ;
            echo "<td>". mb_detect_encoding($_POST['text']) . "</td>" ;
        }
        ?>
        </td>
    </tr>
    <tr>
        <td>
            <form action="" method="POST" enctype="multipart/form-data">
            <input type="text" name="text" value="フォームから送信される文字列" size="40">
            <input type="submit" name="multi_form" value="multipartフォームから送信">
            </form>
        </td>
        <td>
        <?php
        // [B] マルチパートフォームから送信された文字列とエンコーディングを表示
        if (isset($_POST['multi_form']))
        {
            echo "<td>{$_POST['text']}</td>" ;
            echo "<td>". mb_detect_encoding($_POST['text']) . "</td>" ;
        }
        ?>
        </td>
    </tr>
</table>
</body>
</html>

設定はこんな感じ。

<td>
  mb_output_handler
</td>
<td>
  Japanese
</td>
<td>
  eucJP-win
</td>
<td>
  SJIS-win
</td>
<td>
  SJIS-win
</td>
<td>
  1
</td>
<td>
</td>
<td>
</td>
<td>
  eucJP-win
</td>
output_handler
mbstring.language
mbstring.internal_encoding
mbstring.http_input
mbstring.http_output
mbstring.encoding_translation
mbstring.detect_order
mbstring.func_overload
default_charset

「POSTフォームから送信」すると、[A]に以下が出力される。

フォームから送信される文字列 | EUC-JP

変換された。

「multipartフォームから送信」すると、[B]に以下が出力される。

?t?H?[???????M????????/td> | SJIS

変換されてない!

ということで、mbstring.encoding_translation=onにしたい場合(すべてのエンコーディングを統一できないなど)は、

  • PHP4.4.8ではmbstring拡張を組み込みでbuildする
  • PHP5.2.xではパッチを当ててmbstring拡張を組み込みでbuildする

とする必要がありそうです。

PHP5.2.6で「multipart/form-data使ってアップロード」の続き – Do You PHP はてな
http://d.hatena.ne.jp/shimooka/20080527/1211872306

というまとめがあるにあるのだが、本番環境で動いているサーバのPHPをリビルドするのはなかなか難しい場合もあるだろう。同僚もそんな状況だったので、手作りのフィルタで自前変換することをアドバイスした。

●auto_prepend_fileを指定する。

.htaccess

php_value auto_prepend_file "encoding_filter.php"

可能ならhttpd.confで書くほうがいろいろと望ましい。

●フィルタの作成

encoding_filter.php

<?php
$apache_headers = apache_request_headers() ;
if (strpos($apache_headers['Content-Type'] , 'multipart/form-data') !== false)
{
    $srcEnc = ini_get("mbstring.http_input") ;
    $dstEnc = ini_get("mbstring.internal_encoding") ;
    foreach ($_POST as &$param)
    {
        $param = mb_convert_encoding($param, $dstEnc, $srcEnc) ;
    }
}
unset($apache_headers) ;
?>

ハマった事例ではとりあえず困ってない(ファイルネームは動的に生成している)のでファイルネームの処理はしていないが、$_FILES[‘userfile‘][‘name’](アップロードもとのファイル名)を使用する場合は同様にフィルタでエンコーディングをコンバートしないと、ファイル名にマルチバイト文字を使われた場合困ったことになる。

PHP: apache_request_headers – Manual
http://www.php.net/manual/ja/function.apache-request-headers.php

PHP: ファイルアップロードの処理 – Manual
http://www.php.net/manual/ja/features.file-upload.php

ずいぶん前からあるバグっぽいのだが、いまだにこんなのが残ってるんだなぁ、と妙に感心してしまった。http_inputを固定してencoding_transrationを使うのは携帯っぽいが、携帯はmultipart/form-dataを使わない場合が大半だから、というのが気づかれにくい理由だろうか。