ます’s Blog - どうでもいい記事100選

どうでもいい記事100選

file upload problem in mbstring(enable encoding_translation)

ちょっとした事でハマって色々と調べていたら、mbstringの微妙な挙動に出くわした。これは既知の問題なのだろうか。
草陰に隠れているヘビをつついてたら大蛇が出てきた。。。みたいな。忙しい時に限って悪条件が重なる。_| ̄|○
まず、自分がハマった件についての調査結果。
例えば「c:\test\hoge\huga.txt」というファイルをアップロードしたとします。
PHP的に色々と処理を行った結果、最終的には「huga.txt」と、ファイル名だけになってプログラム側に制御が戻ります。
具体的な実装は「/php-src/main/rfc1867.c」を参照して頂ければ、と。あんまり余裕が無いので端折っちゃってスミマセン。
どうでもいい補則ですが、IEでファイルをアップロードすると「c:\test\hoge\huga.txt」と、フルパスで情報が来るのですが、Firefoxでは「huga.txt」と、ファイル名だけで情報が来るようです(Content-Disposition経由の情報)。こういうのはブラウザ間で統一してよ。。。とか思ってみたり。
で、本題ですが、IEで特定の文字(主にマルトバイト)がファイルパスに入っていると正しくファイル名が取得できないようです(ファイル名が「hoge\huga.txt」となる)。
Firefoxでは特に問題は無かったのですが、ファイル名に特定の文字が入っていると同じような事になるのかもしれません(未調査)。
色々と調べた結果、mbstringの設定状況によっても正しく取得できたり正しく取得できなかったりする事が分かりました。
はじめからmbstringが無効になっていた場合、mbstring関連の設定が全て無効であった場合、前提条件に合うようにmbstring関連の設定を正しく設定していた場合等は正しく取得できるようです。
ぱっと見た感じ、mbstring.encoding_translationディレクティブが「On」になっているだけで該当処理が走るっぽいのですが、該当処理はmbstring.http_inputディレクティブが「pass」以外(入力の自動変換が有効になっている事、各種情報が内部コードに正しく変換されている事)が前提になってるっぽい。
うーん。うーん。うーん。苦しい。や、単に自分が間違っているだけかもしれない(とか言ってみる)。

とりあえず、mbstring.encoding_translationディレクティブが「On」で、かつmbstring.http_inputディレクティブが「pass」以外の場合に該当処理が走る(passは除外する)ように変更してみたら期待する結果になった(ファイル名が「hoge\huga.txt」ではなくて「huga.txt」となる)。わーい。
本来であればphp_mb_encoding_translation関数を修正すれば良いのですが、他で使ってるかもしれないので新たに関数を用意しました(関数名が安直スギですが)。
php_mb_encoding_translation関数を修正してもいいんだったら、mbstring.cの修正だけで済みます。後者を期待したい。。。
こちらは4.4.8用。

--- php-4.4.8,orig/main/rfc1867.c	2007-12-31 16:22:55.000000000 +0900
+++ php-4.4.8/main/rfc1867.c	2008-03-04 12:09:31.000000000 +0900
@@ -55,7 +55,7 @@
 void php_mb_flush_gpc_variables(int num_vars, char **val_list, int *len_list, zval *array_ptr  TSRMLS_DC)
 {
 	int i;
-	if (php_mb_encoding_translation(TSRMLS_C)) {
+	if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 		if (num_vars > 0 &&
 			php_mb_gpc_encoding_detector(val_list, len_list, num_vars, NULL TSRMLS_CC) == SUCCESS) {
 			php_mb_gpc_encoding_converter(val_list, len_list, num_vars, NULL, NULL TSRMLS_CC);
@@ -590,7 +590,7 @@
 			*resp++ = start[++i];
 		} else {
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-			if (php_mb_encoding_translation(TSRMLS_C)) {
+			if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 				size_t j = php_mb_gpc_mbchar_bytes(start+i TSRMLS_CC);
 				while (j-- > 0 && i < len) {
 					*resp++ = start[i++];
@@ -615,7 +615,7 @@
 	char *str = *line, *strend, *res, quote;
 
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-	if (php_mb_encoding_translation(TSRMLS_C)) {
+	if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 		int len=strlen(str);
 		php_mb_gpc_encoding_detector(&str, &len, 1, NULL TSRMLS_CC);
 	}
@@ -838,7 +838,7 @@
 	PG(http_globals)[TRACK_VARS_FILES] = http_post_files;
 
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-	if (php_mb_encoding_translation(TSRMLS_C)) {
+	if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 		val_list = (char **)ecalloc(num_vars_max+2, sizeof(char *));
 		len_list = (int *)ecalloc(num_vars_max+2, sizeof(int));
 	}
@@ -904,7 +904,7 @@
 				}
 
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-				if (php_mb_encoding_translation(TSRMLS_C)) {
+				if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 					php_mb_gpc_stack_variable(param, value, &val_list, &len_list, 
 											  &num_vars, &num_vars_max TSRMLS_CC);
 				} else {
@@ -1067,7 +1067,7 @@
 			}
 
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-			if (php_mb_encoding_translation(TSRMLS_C)) {
+			if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 				if (num_vars>=num_vars_max){	
 					php_mb_gpc_realloc_buffer(&val_list, &len_list, &num_vars_max, 
 											  1 TSRMLS_CC);
--- php-4.4.8,orig/ext/mbstring/mbstring.c	2007-04-05 00:28:18.000000000 +0900
+++ php-4.4.8/ext/mbstring/mbstring.c	2008-03-04 12:09:31.000000000 +0900
@@ -3903,6 +3903,17 @@
 }
 /* }}} */
 
+/* {{{ MBSTRING_API int php_mb_encoding_translation4upload() */
+MBSTRING_API int php_mb_encoding_translation4upload(TSRMLS_D) 
+{
+	if( MBSTRG(encoding_translation) && MBSTRG(http_input_list_size) > 0 && MBSTRG(http_input_list)[0] != mbfl_no_encoding_pass ) {
+		return 1;
+	} else {
+		return 0;
+	}
+}
+/* }}} */
+
 /* {{{ MBSTRING_API size_t php_mb_mbchar_bytes_ex() */
 MBSTRING_API size_t php_mb_mbchar_bytes_ex(const char *s, const mbfl_encoding *enc)
 {
--- php-4.4.8,orig/ext/mbstring/mbstring.h	2006-04-03 22:04:13.000000000 +0900
+++ php-4.4.8/ext/mbstring/mbstring.h	2008-03-04 12:09:31.000000000 +0900
@@ -121,6 +121,8 @@
 
 MBSTRING_API int php_mb_encoding_translation(TSRMLS_D);
 
+MBSTRING_API int php_mb_encoding_translation4upload(TSRMLS_D);
+
 MBSTRING_API char *php_mb_safe_strrchr_ex(const char *s, unsigned int c,
                                     size_t nbytes, const mbfl_encoding *enc);
 MBSTRING_API char *php_mb_safe_strrchr(const char *s, unsigned int c,

こちらは5.2.5用(4.4.8と内容は同じです)。

--- php-5.2.5,orig/main/rfc1867.c	2007-07-18 08:46:40.000000000 +0900
+++ php-5.2.5/main/rfc1867.c	2008-03-04 12:09:31.000000000 +0900
@@ -58,7 +58,7 @@
 void php_mb_flush_gpc_variables(int num_vars, char **val_list, int *len_list, zval *array_ptr  TSRMLS_DC)
 {
 	int i;
-	if (php_mb_encoding_translation(TSRMLS_C)) {
+	if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 		if (num_vars > 0 &&
 			php_mb_gpc_encoding_detector(val_list, len_list, num_vars, NULL TSRMLS_CC) == SUCCESS) {
 			php_mb_gpc_encoding_converter(val_list, len_list, num_vars, NULL, NULL TSRMLS_CC);
@@ -595,7 +595,7 @@
 			*resp++ = start[++i];
 		} else {
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-			if (php_mb_encoding_translation(TSRMLS_C)) {
+			if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 				size_t j = php_mb_gpc_mbchar_bytes(start+i TSRMLS_CC);
 				while (j-- > 0 && i < len) {
 					*resp++ = start[i++];
@@ -620,7 +620,7 @@
 	char *str = *line, *strend, *res, quote;
 
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-	if (php_mb_encoding_translation(TSRMLS_C)) {
+	if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 		int len=strlen(str);
 		php_mb_gpc_encoding_detector(&str, &len, 1, NULL TSRMLS_CC);
 	}
@@ -846,7 +846,7 @@
 	PG(http_globals)[TRACK_VARS_FILES] = http_post_files;
 
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-	if (php_mb_encoding_translation(TSRMLS_C)) {
+	if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 		val_list = (char **)ecalloc(num_vars_max+2, sizeof(char *));
 		len_list = (int *)ecalloc(num_vars_max+2, sizeof(int));
 	}
@@ -941,7 +941,7 @@
 					}
 
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-					if (php_mb_encoding_translation(TSRMLS_C)) {
+					if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 						php_mb_gpc_stack_variable(param, value, &val_list, &len_list, 
 												  &num_vars, &num_vars_max TSRMLS_CC);
 					} else {
@@ -1172,7 +1172,7 @@
 			}
 
 #if HAVE_MBSTRING && !defined(COMPILE_DL_MBSTRING)
-			if (php_mb_encoding_translation(TSRMLS_C)) {
+			if (php_mb_encoding_translation4upload(TSRMLS_C)) {
 				if (num_vars>=num_vars_max){	
 					php_mb_gpc_realloc_buffer(&val_list, &len_list, &num_vars_max, 
 											  1 TSRMLS_CC);
--- php-5.2.5,orig/ext/mbstring/mbstring.c	2007-09-24 20:51:36.000000000 +0900
+++ php-5.2.5/ext/mbstring/mbstring.c	2008-03-04 12:09:31.000000000 +0900
@@ -4099,6 +4099,17 @@
 }
 /* }}} */
 
+/* {{{ MBSTRING_API int php_mb_encoding_translation4upload() */
+MBSTRING_API int php_mb_encoding_translation4upload(TSRMLS_D) 
+{
+	if( MBSTRG(encoding_translation) && MBSTRG(http_input_list_size) > 0 && MBSTRG(http_input_list)[0] != mbfl_no_encoding_pass ) {
+		return 1;
+	} else {
+		return 0;
+	}
+}
+/* }}} */
+
 /* {{{ MBSTRING_API size_t php_mb_mbchar_bytes_ex() */
 MBSTRING_API size_t php_mb_mbchar_bytes_ex(const char *s, const mbfl_encoding *enc)
 {
--- php-5.2.5,orig/ext/mbstring/mbstring.h	2007-01-01 18:36:02.000000000 +0900
+++ php-5.2.5/ext/mbstring/mbstring.h	2008-03-04 12:09:31.000000000 +0900
@@ -132,6 +132,8 @@
 
 MBSTRING_API int php_mb_encoding_translation(TSRMLS_D);
 
+MBSTRING_API int php_mb_encoding_translation4upload(TSRMLS_D);
+
 MBSTRING_API char *php_mb_safe_strrchr_ex(const char *s, unsigned int c,
                                     size_t nbytes, const mbfl_encoding *enc);
 MBSTRING_API char *php_mb_safe_strrchr(const char *s, unsigned int c,

mbstring.http_inputディレクティブを「pass」にして運用しているので、本来であればmbstring.encoding_translationディレクティブは「Off」にするべきなのですが。。。個人的には(コンパイル・オプション「--enable-zend-multibyte」を有効にした上で)mbstring.script_encodingディレクティブを設定して使っているので、微妙な設定状態になっていたと。で、ハマる、と。_| ̄|○
ややこしい事に、mbstring.script_encodingディレクティブはmbstring.encoding_translationディレクティブを「On」にしないと機能しないんですよね。使う方も使われる方も微妙なので、判定はドロー!
ただ、PHPを再ビルドするのも面倒だったので、最終的には(パッチを適用せずに)以下のようなJavaScriptを仕込んで逃げ。ヘタレっぷりは健在です(ぉ

<?php

  header( "Content-Type: text/html; charset=Shift_JIS" );

  mb_http_output( "SJIS" );
  ob_start( "mb_output_handler" );

?>
<html>
<head>
<title>ファイルアップロードのテスト</title>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<script language="JavaScript">
<!--
function GoSubmit( )
{
 document.UpLoad.original_file_name.value = document.UpLoad.userfile.value;
 document.UpLoad.submit( );
}
-->
</script>
</head>
<body>
<form name="UpLoad" enctype="multipart/form-data" action="upload.php" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="1000000">
    このファイルをアップロード: <input name="userfile" type="file"><br>
    このファイルをアップロード: <input name="upFile[]" type="file"><br>
    このファイルをアップロード: <input name="upFile[]" type="file"><br>
    <input type="hidden" name="original_file_name" value="">
    <input type="button" value="ファイルを送信" onClick="JavaScript:GoSubmit( );">
</form>
</body>
</html>


ここからは別の話。
PHP的に色々と処理を行った結果、最終的には「huga.txt」と、ファイル名だけになってプログラム側に制御が戻ります。
これはこれで構わないと思いますが、恒例の「余計なお節介」に感じる時もあります。
プログラム側で対処したいので、そのままの状態でプログラム側に制御を戻してほしい時があります。なので、こういうパッチを用意。
こちらは4.4.8用。

--- php-4.4.8,orig/main/rfc1867.c	2007-12-31 16:22:55.000000000 +0900
+++ php-4.4.8/main/rfc1867.c	2008-03-04 12:09:31.000000000 +0900
@@ -1121,6 +1121,25 @@
 			} else {
 				register_http_post_files_variable(lbuf, filename, http_post_files, 0 TSRMLS_CC);
 			}
+
+			/* Add $foo_orig_name */
+			if (is_arr_upload) {
+				sprintf(lbuf, "%s_orig_name[%s]", abuf, array_index);
+			} else {
+				sprintf(lbuf, "%s_orig_name", param);
+			}
+
+			safe_php_register_variable(lbuf, filename, strlen(filename), NULL, 0 TSRMLS_CC);
+
+			/* Add $foo[orig_name] */
+			if (is_arr_upload) {
+				sprintf(lbuf, "%s[orig_name][%s]", abuf, array_index);
+			} else {
+				sprintf(lbuf, "%s[orig_name]", param);
+			}
+
+			register_http_post_files_variable(lbuf, filename, http_post_files, 0 TSRMLS_CC);
+
 			efree(filename);
 			s = NULL;
 	

こちらは5.2.5用(4.4.8と内容は同じです)。

--- php-5.2.5,orig/main/rfc1867.c	2007-07-18 08:46:40.000000000 +0900
+++ php-5.2.5/main/rfc1867.c	2008-03-04 12:09:31.000000000 +0900
@@ -1235,6 +1235,25 @@
 			} else {
 				register_http_post_files_variable(lbuf, filename, http_post_files, 0 TSRMLS_CC);
 			}
+
+			/* Add $foo_orig_name */
+			if (is_arr_upload) {
+				sprintf(lbuf, "%s_orig_name[%s]", abuf, array_index);
+			} else {
+				sprintf(lbuf, "%s_orig_name", param);
+			}
+
+			safe_php_register_variable(lbuf, filename, strlen(filename), NULL, 0 TSRMLS_CC);
+
+			/* Add $foo[orig_name] */
+			if (is_arr_upload) {
+				sprintf(lbuf, "%s[orig_name][%s]", abuf, array_index);
+			} else {
+				sprintf(lbuf, "%s[orig_name]", param);
+			}
+
+			register_http_post_files_variable(lbuf, filename, http_post_files, 0 TSRMLS_CC);
+
 			efree(filename);
 			s = NULL;
 	

このパッチを実行すると以下のような結果になります(新たにorig_nameを用意し、Content-Disposition経由の情報を加工せずに定義する)。「※※」の部分に注目です。

Array
(
    [userfile] => Array
        (
            [name] => huga.txt
            [orig_name] => c:\test\hoge\huga.txt  ※※
            [type] => text/plain
            [tmp_name] => /tmp/phpyFyG3Y
            [error] => 0
            [size] => 0
        )

    [upFile] => Array
        (
            [name] => Array
                (
                    [0] => huga.txt
                    [1] => huga.txt
                )

            [orig_name] => Array          ※※
                (                  ※※
                    [0] => c:\test\hoge\huga.txt  ※※
                    [1] => c:\test\hoge\huga.txt  ※※
                )                  ※※

            [type] => Array
                (
                    [0] => text/plain
                    [1] => text/plain
                )

            [tmp_name] => Array
                (
                    [0] => /tmp/php92pGGH
                    [1] => /tmp/phpS2NGjq
                )

            [error] => Array
                (
                    [0] => 0
                    [1] => 0
                )

            [size] => Array
                (
                    [0] => 0
                    [1] => 0
                )

        )

)


ここからは更に別の話(最初に言っていたmbstringの微妙な挙動について)。
PHPマニュル的には、以下のように掲載されています。


注意: PHP 4.3.3 以降、HTML フォームの enctype が multipart/form-data に設定され、かつ、 php.ini において mbstring.encoding_translation に On が指定されている場合、 POST データの変数とアップロードされたファイルの名前の文字エンコーディングは、 内部文字エンコーディングに変換されます。 ただし、クエリキーに関しては、変換されません。

5.2.5ではPOSTデータ* のみ *変換されないんですよね。4.4.8ではPOSTデータも正しく変換されています。
以下のように設定関連を固定にしても、POSTデータは変換されませんでした。ただ、$_GETや$_FILES等の情報は5.2.5でも正しく変換されています。

[mbstring]
mbstring.language             = Japanese
mbstring.internal_encoding    = EUC-JP
mbstring.http_input           = SJIS
mbstring.http_output          = pass
mbstring.encoding_translation = On
mbstring.detect_order         = SJIS
mbstring.substitute_character = 0x3013;
mbstring.strict_detection     = On(Offでも同じ結果)
mbstring.script_encoding      = SJIS

この辺りについては(まだ)深く追ってないので何ともいえませんが、微妙な挙動です。
間違っている箇所があれば、是非とも指摘して頂きたく。。。って、入力の自動変換なんか使わない人だったので結構な落とし穴がありそうな予感がする。