動作検証環境
- CentOS7
- Apache/2.4.6
- PHP 5.4
起きた問題
ファイルダウンロード時に500エラーが起きる。
名前をつけて保存用に、ヘッダーのfilenameにユーザーファイルの名前ほぼそのままの値を入れている。
header('Content-Disposition: attachment; filename="' . $filename . '";'
Apacheのエラーログには
[http:error] [pid 29947] [client 192.168.1.1:57556] AH02430: Response header 'Content-Disposition' value of 'attachment; filename="~\x1d~' contains invalid characters, aborting request
と出ている。
その他手動デコードなどの結果、\x1d
が制御文字であり、これがエラーの原因と見た。
ひとまず安直に解決方法
$filename = preg_replace('/\p{Cc}/u', '', $filename);
filenameをBase64エンコード・URLエンコードしていないことが諸悪の根源なのですが、filename*への移行期間中なので…filenameくんにはあまり大胆な変更をせずに役割を終えてもらいたいです。
U+001Dとはなにか 制御文字だよ
ざっくりと
[制御文字 - Wikipedia]
のGroup Separator(^]
)です。
なんじゃそりゃ。どうやって入ったんでしょうね。
wikiを少し縮めると、おそらく
ASCIIの制御文字 = 0から21および127(C0制御コード)
拡張ASCIIの制御文字 = ASCIIの制御文字 + 128から159(C1制御コード)
Unicodeの制御文字 = 拡張ASCIIの制御文字(Cc)
という区分になる。
が、一般?の方なら遭遇頻度的にも、制御コードと言えばC0制御コード(C0集合)かな。
127のDeleteについては、C0の中でも飛んでいることもあり、端折られることもあるように見受けられた。
ぼけーと気をつけることと言えば、制御文字にはCRLFでおなじみな
\r\n\t
が含まれているので、テキスト系に使うときはレイアウト崩れてから思い出そう。
除去方法
幾つか見つかった。
[制御文字を取り除く方法(改行コードは保持) - Qiita]
最初はこれだけど、改行コード保持のためちょっと長くなっている。
[preg_replaceでutf8文字列からコントロール文字を削除する。 - gounx2の日記]
もっと\s
のようなメタ文字でばばっと消したい要望に応えられた。
[PHP: Unicode 文字プロパティ - Manual]
UTF-8モードなんて使ったこと無いっす。
見た目とググりやすそうな使い方(+ファイル名に改行は無い)から解決方法ではこれを採用している。
[制御文字がDBに入らないようにする - Qiita#コメント]
から[PHP: Filter 関数 - Manual]の[PHP: 除去フィルタ - Manual]を使えばいいよとの啓示を受けるが、フラグの説明が無く、どれを使えば過不足無いかがよくわからなかった。
[filter_var, filter_input でよく使うもの - Qiita#除去フィルター]
を見るに、FILTER_FLAG_STRIP_LOW
がキモそうともう少しググって
[PHPのFilter関数の除去フィルタにある「FILTER_SANITIZE_ENCODED」の意味が分か... - Yahoo!知恵袋]
から、(知恵袋ですがご安心ください)
$var = filter_var($var, FILTER_SANITIZE_ENCODED, FILTER_FLAG_STRIP_LOW); これは以下と等価です。文字コード0x0~0x1fまでの制御文字を取り除きます。 $var = urlencode(preg_replace('/[\x0-\x1f]/', '', $var));
FILTER_FLAG_STRIP_LOW
が(\x7F
を除く)C0制御コードを除去するものだと認識しました。
提示されているようにurlencode
しちゃえばいいですが、制御コード除去のみしたければ、
filter_var($var, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW);
でしょうか。
わざわざ正規表現せずとも~というのはごもっともですが、
除去フィルタのフラグの仕様が公式にささっと検索できない限りはまだ、正規表現の方が他人に伝わりやすいかなあと思いました。
その他 参考
- [User-Agent分岐無しに日本語ファイル名でファイルをダウンロードさせる (完全版) - Qiita]
URLにファイル名入れられるならこれが良さそうなのですが。ちょっとまだ良くわかってない。
- [日本語ファイル名対応 簡単にファイルをダウンロードさせられる関数 - Qiita]
User-Agent分岐版。filenameにエンコードした文字列を入れられることを知らなかった。
- [Content-Disposition: attachment; filenameのrfc 6266形式 - Qiita]
現状、正解かわかりませんがUser-Agentを見ずに併記して移行期間としてますが、filename*では正常に処理されててもfilenameが文字化け以前にヘッダーとして不正ならどうしようもないねという問題でした。
参考になりそうな有名なサイトからちっちゃいサイト含め、日本語ファイルをダウンロードさせるサイトが全然見つからなくて、他所はどうしているかが観測しづらいです。
[Content-Disposition: attachment; filenameのrfc 6266形式 - Qiita]:https://qiita.com/khsk/items/d541b8dc40bd2c6128d2
[日本語ファイル名対応 簡単にファイルをダウンロードさせられる関数 - Qiita]:https://qiita.com/mpyw/items/3838819d4af75c84b564
[User-Agent分岐無しに日本語ファイル名でファイルをダウンロードさせる (完全版) - Qiita]:https://qiita.com/mpyw/items/202bef4349bfdc7e5c13
[PHPのFilter関数の除去フィルタにある「FILTER_SANITIZE_ENCODED」の意味が分か... - Yahoo!知恵袋]:https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q14131407269
[filter_var, filter_input でよく使うもの - Qiita#除去フィルター]:https://qiita.com/Ayutanalects/items/0749d7c684f94f98b224#除去フィルター
[PHP: Filter 関数 - Manual]:https://secure.php.net/manual/ja/ref.filter.php
[PHP: 除去フィルタ - Manual]:https://secure.php.net/manual/ja/filter.filters.sanitize.php
[制御文字がDBに入らないようにする - Qiita#コメント]:https://qiita.com/rentondesu/items/ea359abd14d88032475b#comment-783c6dac2d0c4f7bee84
[制御文字を取り除く方法(改行コードは保持) - Qiita]:https://qiita.com/suin/items/fd4c0fc808316793f9cc
[preg_replaceでutf8文字列からコントロール文字を削除する。 - gounx2の日記]:http://d.hatena.ne.jp/gounx2/20091127/1259291487
[制御文字 - Wikipedia]:https://ja.wikipedia.org/wiki/%E5%88%B6%E5%BE%A1%E6%96%87%E5%AD%97
[PHP: Unicode 文字プロパティ - Manual]:http://jp2.php.net/manual/ja/regexp.reference.unicode.php