javastudyshtai
@javastudyshtai

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

PHP + jQuery : ファイルダウンロード機能の実装で文字化けしてしまう

解決したいこと

Webサイトから$.postを使ってファイルパスとファイル名をサーバーへ送信し、
サーバー側でダウンロードパスを生成後、readfile関数で標準出力し、
ブラウザ側でダウンロードしたファイルを開くとエラーが出て開けない。

開発環境

PHP v8.2.4
jQuery v3.7.1
XAMPP v3.3.0
Smarty v4.3.4
Visual Studio Code v1.84.2

OS: Windows 10 Pro
ブラウザ: Microsoft Edge v119.0.2151.97

ダウンロードするファイルのありか: サーバのc:ドライブに有

発生している問題・エラー

ダウンロードしたファイルを開くと、

「この文書を開くときにエラーが発生しました。ファイルが壊れています。修復できませんでした。」

と表示される。(PDFファイルの場合)
サクラエディタで確認してみると…

image.jpg

該当するソースコード

画面を写すphpファイル、tplファイルの記述は省略します。
表のある文字列をダブルクリックしたときに、その文字列にセットされている、
ファイルパスを$.postでサーバーへ送信しサーバーでダウンロードパスを生成します。
その後、サーバー側でreadfileで標準出力し、jsでその値をblobに入れて、
ブラウザでダウンロードさせています。
ファイルはpdfやzip、dwgなどのCADファイルなど、多岐にわたります。
まとめてダウンロードという機能の実装は必要なく、1アクション1ダウンロードです。

fileDownload.php (ajax用)
<?php
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') 
    {
        exit;
    }

    $dlPath = $_POST['filePath'];

    header('Content-Length: '. filesize($dlPath));
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename= "'. basename($dlPath). '"');

    readfile($dlPath);
    exit;
~省略~
    // Tabulator 表内をダブルクリックした時のアクション
    table.on("cellDblClick", function(e, cell){

    ~ 表内のどこをダブルクリックしたか特定する処理(省略) ~
    ~ ダウンロードリンクをダブルクリックしていたらダウンロード処理へ ~
    
    $.post('fileDownload.php?',{
        filePath: ファイルパス;
    })
    .done(function (result){
        const blob = new Blob([result], [ 'type: application/octet-stream' ]);
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a'); 
        document.body.appendChild(a);
        a.download = file;
        a.href = url;
        a.click();
        a.remove();
        URL.revokeObjectURL(url);
    });

自分で試したこと

・contents-typeをpdfを指定して実行(結果は同じ)
・file_get_contents関数を試してみる(結果は同じ)
・ブラウザでダウンロードさせるのではなく、サーバー側で
クライアントのファイルへコピーさせる処理を実装
(fileDownload.phpでcopy関数を使った。)(コピーできず)
・テキストファイルだと文字化けせず、成功した

0

2Answer

サクラエディタで確認してみると…

画像を見る限り pdf ファイルではないようですが? 違うファイルをダウンロードしているということはないですか?


下の質問者さんのコメントによると、上に書いたようなことはないそうなので・・・

【追記】

ダウンロードするファイルのありか: サーバのc:ドライブに有

そこにある pdf ファイルを php アプリでダウンロードするようにしているのですよね。その c ドライブの pdf ファイルを直接 Adobe Acrobat などで開いて正常に表示されますか?

そこは問題ないということなら、サーバーからブラウザに送信してクライアントの PC のフォルダに保存する過程でファイルが壊れるということは考えにくいので、その前の php によるプロセスに問題がありそうな気がします。

Fiddler などを使って、元のファイルとサーバーから送られてくるファイルを比較してみてはいかがですか?

自分の環境で(PHP ではありませんが)、サーバーにある以下のような pdf ファイルをダウンロードする API に、

pdf.jpg

以下のようなスクリプトで要求をかけて、要求・応答を Fiddler でキャプチャすると、

<script type="text/javascript">
    function download() {
        var url = "/Download/VirtualFileResult";
        var filename = "testajax";

        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.responseType = 'blob';
        xhr.onreadystatechange = function (e) {
            if (this.readyState == 4 && this.status == 200) {
                var blob = this.response;

                //IE, 旧 Edge とその他で処理の切り分け
                if (window.navigator.msSaveBlob) {
                    window.navigator.msSaveBlob(blob, filename + ".pdf");
                } else {
                    var a = document.createElement("a");
                    // IE11 は URL API をサポートしてない
                    var url = window.URL;
                    a.href = url.createObjectURL(new Blob([blob],
                        { type: blob.type }));
                    document.body.appendChild(a);
                    a.style = "display: none";
                    a.download = filename + ".pdf";
                    a.click();
                }
            }
        };
        xhr.send();
    }

</script>

以下のようになります。

fiddler2.jpg

サーバー側の処理の問題なければ、pdf ファイルのバイナリは元ファイルと Fiddler のキャプチャ結果で当然同じになります。ダウンロードした pdf ファイルはもちろん問題なく開けます。

そのあたりを確認されてはいかがですか?

1Like

Comments

  1. @javastudyshtai

    Questioner

    確認頂き、ありがとうございます。
    先ほどの画像の一番上の写真を張り付けます。
    image2.jpg

    中身は見せられないのでトリミングしていますが、先頭でpdfとなっています。
    パスもデバッグして、絶対パス指定し、ファイルに間違いがないことも確認しています。

  2. 先ほどの画像の一番上の写真を張り付けます

    元ファイルなのかダウンロードしたファイルなのか分かりませんが、どっちにせよ pdf ファイルはそういう内容になるんでしょうかね? 違うような気がするのですが(気がするだけで確証はありませんけど)・・・

    回答欄の【追記】に確認するための案を書きました。試してみてはいかがでしょう。

  3. @javastudyshtai

    Questioner

    そこにある pdf ファイルを php アプリでダウンロードするようにしているのですよね。その c ドライブの pdf ファイルを直接 Adobe Acrobat などで開いて正常に表示されますか?

    その通りです。
    C:配下にあるpdfファイルをphpのプログラムでパス指定をし、readfileしています。
    そして、cドライブをAdobe Acrobatで開くことは確認済です。

    他にもxlsxやdocxなどの様々なファイルもダウンロードする予定です。

    追記で頂いた案を教えていただき、ありがとうございます。
    さっそく実行して確認します。
    確認終わり次第、またコメントします!

  4. @javastudyshtai

    Questioner

    Fiddlerにアクセスしたのですが…接続することができませんでした。
    スマホからはアクセスできたのですが、社内のインターネットから接続
    できなかったので、設定で接続できないようになっている可能性があります。

    代替手段を考えてみたところ、まず比較というところで
    WinMergeにて、ファイルを比較してみました。
    image3.jpg

    左側がC:ドライブにある開けるPDF、
    右側がPHP経由でダウンロードしたPDFです。

    文字コードが違うようなので、明日、PHPの方で文字コード指定を行ってみます。
    ただ、下記サイトによるとreadfileでは文字コード指定はできないようなので、
    少しロジックに工夫が必要になりそうです。
    https://wake-mob.jp/2020/11/18/bom%e3%81%ae%e6%9c%89%e7%84%a1%e3%82%92%e5%88%a4%e5%88%a5%e3%81%97%e3%80%81utf%e3%82%92%e8%aa%ad%e3%81%bf%e5%88%86%e3%81%91%e3%82%8b-php7-4-%e3%81%ae%e3%82%b5%e3%83%b3%e3%83%97%e3%83%ab%e3%82%b3/

    また、HTTP通信をキャプチャするということについては、
    Wiresharkはアクセスできたので、これをインストールしてみようと思います。

    取り急ぎ、確認結果でした。
    ありがとうございます。

  5. Fiddlerにアクセスしたのですが…接続することができませんでした。スマホからはアクセスできたのですが、社内のインターネットから接続できなかったので、設定で接続できないようになっている可能性があります。

    Fiddler は基本プロキシで、起動すると Windows OS なら以下の画像のように自動的にプロキシの設定がされます (終了すればプロキシの設定は元に戻ります)。ブラウザがシステムのプロキシを使うように設定されていれば Fiddler で要求・応答はキャプチャできます・・・が、社内のネットワークということですので、プロキシの使用には制約がかかっているのかもしれませんね。

    proxy.png

    ブラウザのディベロッパーツールでも要求・応答はキャプチャできますのでやってみてはいかがですか。ただ、上の回答の画像のような形でバイト列まで見ることができるかは分かりませんが。

  6. @javastudyshtai

    Questioner

    通常はプロキシサーバを使うようになっており、
    Wi-Fiなどを利用する際はプロキシサーバ設定をOFFにして使っています。

    ブラウザはEdgeとChrome、両方あるのでどちらとも開発者ツールを使ってみます!

jQuery の $.post でバイナリデータは取得できません。データはテキスト扱いになり、それを Blob に渡すと UTF-8 として不正な文字が置換され、データが破損します。

Blob を取得するには以下のように fetch を使ってください。

fetch('fileDownload.php?', {
  method: 'POST',
  headers: { /* 任意 */ },
  body: new URLSearchParams({ filePath: 'ファイルパス' }),
}).then((resp) => {
  return resp.blob();
}).then((blob) => {
  const url = URL.createObjectURL(blob);
  // 略
});
1Like

Comments

  1. @javastudyshtai

    Questioner

    回答ありがとうございます。

    問題解決のために、fetchでもできないか試してみたいと思います。

  2. @SurfurOnWww ありがとうございます、勉強になります。 $.post のオプションに xhrFields: { responseType: 'blob' } を渡せばよさそうですね

  3. @javastudyshtai

    Questioner

    @uasi 様、 @SurferOnWww

    fetchにて、他のサイトを見ながら実装してみた所、問題が解決しました!!
    最終的なコードは下記の通りです。

    fileDownload.php (fetch用)
    <?php
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') 
        {
            exit;
        }
    
        $dlPath = $_POST['filePath'];
    
        readfile($dlPath);
        exit;
    
    ~省略~
        // Tabulator 表内をダブルクリックした時のアクション
        table.on("cellDblClick", function(e, cell){
    
        ~ 表内のどこをダブルクリックしたか特定する処理(省略) ~
        ~ ダウンロードリンクをダブルクリックしていたらダウンロード処理へ ~
        
        const postData = new FormData();
        postData.append('filePath', ファイルパス);
    
        fetch('fileDownload.php',{
            method: 'POST',
            body: postData,
        })
        .then((resp) => {
            return resp.blob();
        })
        .then((blob) => {
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a'); 
            document.body.appendChild(a);
            a.download = file;
            a.href = url;
            a.click();
            a.remove();
            URL.revokeObjectURL(url);
        });
    

    body: 部分ですが、

    const postData = {filePath : path}
    ~ 省略 ~
        body: JSON.stringfy( postData),
    

    でデータ送信しても、PHP側でデバッグしてPOST覗いても値がありませんでした。
    データ形式はform-dataも可能とのことだったので試しに空のFormDataを
    作り、appendで値を追加した後、content-typeを指定していたのですが削除した
    所、POSTで値を確認することができました!!

    その後、ダウンロードしてファイルを開くこともできました!!
    (xlsx、xls、pptx、docxなど全形式ok)

    いろいろ見てくださり、本当にありがとうございます。

Your answer might help someone💌