🤐

Laravel で作ったシステムに Excel をアップロードしたら拡張子が zip になった

2020/07/14に公開

環境

環境は ローカルのHomestead本番のVPS の2つで、どちらでも発生。

  • Laravel Framework 5.8.24
  • PHP 7.3.4 (PHP 7.2.16)
  • Ubuntu 18.04.2 LTS (CentOS Linux release 7.6.1810)
  • Homestead (ConoHa VPS Laravelイメージ)

何がどうなった?

エクセルをアップロードし、 $file->guessExtension() で拡張子を取得したところ zip となってしまった。
さらに $file->getMimeType() をみたところ application/zip となっていた。

クライアントの情報を取得する関数を使うと、それぞれ $file->guessClientExtension()
xlsx となり、 $file->getClientMimeType()application/vnd.openxmlformats-officedocument.spreadsheetml.sheet となった。
表にまとめると以下。

extension mime_type
larval zip application/zip
client xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

エクセルの mime_type なっが。

どうして?

finfo がうまく判定できないらしい。
ライブラリをガンガン追って行ったところ、Laravel の問題というよりは、内部で使用している Symfony のクラスの内部で使用している finfo に行きついた。
実際に finfo で mime_type を判定させてみると、 application/zip となってしまった。
application/zip から拡張子を推測すると zip になるという流れである。
[StackOverflow の関連記事] (https://stackoverflow.com/questions/6595183/docx-file-type-in-php-finfo-file-is-application-zip)
上記記事を読むと、ほかの Microsoft の Office 系ファイルもダメらしい。

どうする?

解決策はいくつかあると思う。

  • Symfony の該当クラスは複数の guesser を扱えるので、Laravel で頑張って拡張する
  • finfo を修正する
  • 素直にクライアントの拡張子を利用する

などなど。

自分は 上記のどれも採用せず、拡張子を推測するための関数をつくった(理由は後述)。
作ったと言っても、Laravel/Symfonyが用意したものと、先人の知恵を組み合わせただけだが。

基本方針

  • 拡張子に関してはクライアント(ファイルアップロード者)を信じるという選択は取りたくない
  • Laravel を拡張するのはしんどそう
  • finfo を修正するのは移植性がない
  • 不具合報告があったのはとりあえず excel だけ

参考ソース

public function guessExtension(UploadedFile $file): string
{
    $extension = $file->guessExtension();
    if ($extension !== 'zip') {
        return $extension;
    }

    $guessedMimeType = $file->getMimeType();
    $clientMimeType = $file->getClientMimeType();
    if ($guessedMimeType === $clientMimeType) {
        return $extension;
    }

    $map = [
        'application/vnd.ms-word.document.macroEnabled.12' => 'docm',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',

        'application/vnd.ms-powerpoint.template.macroEnabled.12' => 'potm',
        'application/vnd.ms-powerpoint.addin.macroEnabled.12' => 'ppam',
        'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' => 'ppsm',
        'application/vnd.ms-powerpoint.presentation.macroEnabled.12' => 'pptm',
        'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx',
        'application/vnd.openxmlformats-officedocument.presentationml.template' => 'potx',
        'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',

        'application/vnd.ms-excel.template.macroEnabled.12' => 'xltm',
        'application/vnd.ms-excel.addin.macroEnabled.12' => 'xlam',
        'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => 'xlsb',
        'application/vnd.ms-excel.sheet.macroEnabled.12' => 'xlsm',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => 'xltx',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
    ];

    return $map[$clientMimeType] ?? $extension;
}

作った後に「 Symfony の同様の力技を行使する guesser を使う手もあったな」などと思ったけど、いろんな mime_type を検討するのはしんどいので、とりあえずこれで。

Discussion