User-Agent分岐無しに日本語ファイル名でファイルをダウンロードさせる (理論編)

ファイル名を日本語でダウンロードさせるためには、User-Agentを見て`Content-Disposition`ヘッダの`filename`の書式や文字コードを書き換える方法[^1]がよく知られていますが、今回はUser-Agentに頼らず日本語ファイル名ダウンロードをクロスブラウザ対応する方法を紹介したいと思います。

## 概要

* User-Agentで分岐して`Content-Disposition`ヘッダを出すことはしない
* ブラウザがURLからファイル名を決める仕様を活用する
* ダウンロードURLのPATH_INFOに日本語ファイル名を入れる

## ブラウザはファイルを保存するとき、何を参考にしているか?

ブラウザがウェブ上のリソースをローカルに保存するとき、必ずファイル名をつけるわけですが、そもそもブラウザは何を参考にして、ファイル名を決めているのでしょうか?

第一に考えられるのが、HTMLなら`<title>`タグの文字列 + .htmlです。

<img width="434" alt="別名で保存_and_ウェブブラウザ_-_Wikipedia.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/7a1bf184-8554-2b2d-d074-68af0c6f343c.png">

第二に、HTMLでない場合、たとえば画像ファイルはURLに含まれるファイル名です。

<img width="676" alt="別名で保存_and_Mozilla_Firefox.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/80f9efc7-6cf3-2032-e8e0-aa62a58e84bb.png">

最後に、`Content-Disposition`ヘッダの`filename`の値です。

他にもあるかもしれませんが、大まかにこの3つのどれかになるわけです。

ここで着目したいのは第二のファイル名決定の仕様です。上の例では、英語のファイル名 Web-browser_usage_on_Wikimedia.svg でしたが、URLにはURLエンコードさえすれば日本語を含めることができるので、日本語ファイル名をURLのパスに含めてしまうという発想です。

## $_SERVER['PATH_INFO']の利用

PHPでは、PHPファイル名の後ろにスラッシュをつけるとそれ以降からクエリストリングの手前までのパスを、`$_SERVER['PATH_INFO']`に格納してくれるという仕様があります。

たとえば、`http://example.com/index.php/foo/bar/buz?hoge=123`にアクセスした場合、index.phpでは`$_SERVER['PATH_INFO'] = '/foo/bar/buz'`がセットされます。

この仕様を活用して、`PATH_INFO`にダウンロード用ファイル名をセットすればいいわけです。たとえば、`index.php/請求書-2016年02月-1432.pdf?invoice_id=1432`といったURLでPDFを生成してあげれば、ブラウザはそのPDFを`請求書-2016年02月-1432.pdf`として保存してくれます。

サーバ側のindex.phpは`PATH_INFO`の値は無視してもいいですが、URLはユーザが決められるので、もし勝手にファイル名をつけてほしくない場合は、`$_GET['invoice_id']`をキーにとってきた請求データと比較して、開発者が意図したファイル名と同じか、`PATH_INFO`をチェックしてもいいです。

## サンプルコード: 日本語ファイル名で保存する例

次のサンプルコードは、ウェブ上のリソースを任意のファイル名で保存する例です。ご覧いただけるとわかりますが、User-Agentによる分岐はありません。

```php:download.php
$filename = isset($_SERVER['PATH_INFO']) ? trim($_SERVER['PATH_INFO'], '/') : '';
$file = isset($_GET['file']) ? $_GET['file'] : '';
if (empty($filename) or empty($file)) {
    header('HTTP', null, 400);
    header('Content-Type: text/plain');
    die('Invalid request');
}

// ファイルの内容をとってくる処理
$contents = @file_get_contents("http://$file"); // ここはもう少し安全にする必要があるがサンプルなので割愛
if ($contents === false) {
    header('HTTP', null, 500);
    header('Content-Type: text/plain');
    die(error_get_last()['message']);
}

// ファイルをダウンロードさせる処理
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment');
header('Content-Length: ' . strlen($contents));
echo $contents;
```

このサンプルの使い方ですが、`http://example.com/download.php/[ファイル名]?file=[リソース名]`といったURLにユーザをアクセスさせて使います。ファイル名はダウンロード時のファイル名を指定し、リソース名にはURLからプロトコルを除いた部分を指定します。[^2]

たとえば、ユーザにQiitaの[Markdown記法 チートシート](http://qiita.com/Qiita/items/c686397e4a0f4f11683d)のMarkdownファイルをダウンロードさせたいなら、下記のようなURLにアクセスさせます。

```
http://example.com/download.php/Markdown%E8%A8%98%E6%B3%95%20%E3%83%81%E3%83%BC%E3%83%88%E3%82%B7%E3%83%BC%E3%83%88.md?file=qiita.com/Qiita/items/c686397e4a0f4f11683d.md
                                ^ この部分がダウンロード向けファイル名
```

このURLにアクセスした、ブラウザの挙動は次のようになります。

Google Chromeは問題なく意図したファイル名で保存されました。

<img width="570" alt="Downloads.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/5ade26a2-1474-1def-dd31-db6471e68d20.png" class="img-thumbnail">

Firefoxも問題なさそうです。

<img width="442" alt="Markdown記法_チートシート_md_を開く.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/3d311159-227f-5252-3272-825cc274bf00.png">

Safariも大丈夫です。

<img width="344" alt="Screen Shot 2016-02-10 at 9.22.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/88f819ae-05b4-ff26-c48a-a33421cb7c31.png">

懸念のIE11も問題なさそうですね。

<img width="814" alt="IE11_-_Win8_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/60e7956a-eb48-5255-f6fa-582870ceeb4e.png">

## URLエンコードの注意点

注意点として、日本語ファイル名をURLに含める以上、URLエンコードする必要があるわけですが、エンコードする関数は`rawurlencode`を使うという点です。`urlencode`関数を使ってしまうと半角スペースが`+`にエンコードされますが、ファイル名にそのまま`+`が現れてしまうからです。

## ルーターを使えばURLはもっとすっきりする

今回 紹介したサンプルコードではクエリストリングからファイルのリソースを探す実装になっていましたが、MVCフレームワークのルータを使えば、URLがもっとすっきりしたものになるかと思います。

たとえば、Silexで次のようなルーティングを設定しておけば、`/invoices/1432/請求書-2016年02月-1432.pdf`といったURLでファイルをダウンロードさせることができます。

```php
$app = new Silex\Application(); 

$app->get('/invoices/{invoiceId}/{filename}', function($invoiceId, $filename) use($app) { 
    // ここで $invoiceId から請求データを取得し、PDFを出力する
})->assert('filename', '.+\.pdf');

$app->run(); 
```

## おわり

今回は、User-Agentで分岐しない日本語ファイル名ダウンロードの方法を紹介しました。検証したブラウザは多くないので、うまく動作しないブラウザはご報告いただけると助かります。

続き: [User-Agent分岐無しに日本語ファイル名でファイルをダウンロードさせる (完全版)](http://qiita.com/mpyw/items/202bef4349bfdc7e5c13)

[^1]: [[日本語ファイル名対応] 簡単にファイルをダウンロードさせられる関数 - Qiita](http://qiita.com/mpyw/items/3838819d4af75c84b564)
[^2]: このコードはあくまで日本語URLを簡単に試せるよう、「ファイルの内容をとってくる処理」の部分はどんなURLからでも取得してくるようにしていますが、実際はもっと制約的な実装の方が安全です。実践的な実装は@mpywさんの[User-Agent分岐無しに日本語ファイル名でファイルをダウンロードさせる (完全版)](http://qiita.com/mpyw/items/202bef4349bfdc7e5c13)をご覧ください。