MW WP Formで、「画像添付を必須にしているのに、画像が届かないまま送信が完了してしまう」という報告を受けた。調べてみると原因はフォームの設定ミスでもバグでもなく、iPhone特有のファイルの渡され方にあった。同じ落とし穴にハマる人がいるかもしれないので、最終的な対策をまとめておく。


    症状:必須は効いているのに、中身が空

    問題のフォームは MW WP Form で作ったフォームで、身分証明書の画像添付を必須にしていた。クライアントからの報告はこうだった。

    画像添付しないと先に進めないはずなのに、あるお客様の申込みで身分証明書が添付されていなかった。

    こちらでも試すと、確かに「ファイルを未選択のままでは確認画面に進めない」。必須バリデーションは正しく効いている。にもかかわらず、実際の申込データには画像が入っていなかった。

    この時点で分かるのは、「確認画面まではファイルが存在する扱いになったが、最終的に中身が届いていない」ということ。ファイルの有無だけを見る必須チェックはすり抜けている。


    外れた仮説を先に潰す

    いくつか思いついた原因を、順に検証して消していった。

    「拡張子を非表示にしていたから」ではない

    まず疑ったのが、利用者がOS側でファイルの拡張子を非表示にしていて、拡張子なしで送られたのでは、という説。これは違った。確認画面ではファイル名に拡張子が表示されていた。OSの「拡張子を非表示」はあくまで画面表示の設定で、実際にアップロードされるファイル名からは拡張子は消えない。

    「変な形式のファイルを添付したから」でもない

    次に、mp4やtxt、あるいは拡張子のないファイルを添付したのでは、という説。これも症状と噛み合わない。当時はファイル形式の制限をかけていなかったので、仮にそうした非画像ファイルを添付したなら、中身のあるファイルがそのまま届くはず。開けるかどうかは別として、「空」にはならない。実際の症状は「中身が空」なので、これも原因ではない。

    「サイズ超過で無言落ち」も可能性は低い

    大きな写真がPHPのpost_max_sizeを超えて$_FILESが空になった、という線も考えた。ただ、サーバー(ヘテムル)のアップロード上限は100MBあり、身分証明書の写真程度で超えることはまずない。加えて、超過していれば確認画面に進めず必須エラーで止まるはずで、完了までいっている事実と合わない。


    真因:iCloud未取得画像の「名前だけ」通過

    残ったのが、次の可能性だった。

    iPhoneで、iCloud上にあってまだ端末に取り込まれていない写真を選択した結果、実体が0バイト(または不完全)のまま、ファイル名だけがフォームを通過した。

    MW WP Form の必須チェックは「ファイル名(=選択の有無)」で判定するため、中身が0バイトでも通ってしまう。これが症状にきれいに一致する。

    再現も取れた。0バイトのtest.jpgを作って添付すると、入力画面は通過し、確認画面ではファイルが表示されない。まさに報告どおりの挙動だった。


    対策の試行錯誤

    原因が見えたので、0バイト・不完全なファイルを確実に弾く方法を探した。ここが一番手こずったので、うまくいかなかった案も含めて残しておく。

    案1:拡張子制限(効果は限定的)

    MW WP Form のバリデーションルールには「拡張子制限」がある。まず画像形式に絞った。

    • 拡張子制限に jpg,jpeg,png,gif を指定

    注意点として、この欄はカンマの後にスペースを入れないこと。jpg, jpeg のように書くと、環境によっては先頭スペースごと比較されて正しい画像まで弾かれることがある。

    これで「非画像・拡張子なし」は弾けるようになった。ただし、今回の本丸である「0バイトの.jpg」は拡張子が正しいので通ってしまう。これだけでは足りない。

    案2:$_FILESを見る自作バリデーション(効かなかった)

    $_FILESのサイズとエラーコードを見て0バイトを弾く独自バリデーションルールを書いてみた。ところがこれが効かない。

    理由は MW WP Form の処理順にあった。MW WP Form はバリデーションが走る前に、アップロードファイルを一時ディレクトリへ移動して$_FILESを消費してしまう。そのため、バリデーション時点では$_FILES['certify']はすでに空で、$_FILESを直接参照するアプローチ自体が成立しなかった。ここは MW WP Form の実装をちゃんと追わずに書いてしまった反省点。

    案3:最小画像サイズ(画像だけなら有効)

    画像に限れば、バリデーションルールの「最小画像サイズ」が効いた。

    • 最小画像サイズに 100 × 100 を指定

    これが有効なのは、MW WP Form が内部でgetimagesize()を使い、アップロードされた実ファイルの中身から縦横のピクセル数を判定するため。0バイトや壊れたファイル、非画像はgetimagesize()が失敗して寸法0扱いになり、確実に弾かれる。$_FILESに依存しないので、案2の問題も回避できる。本物の身分証明書写真は必ず数百〜数千pxあるので、100×100で誤ってブロックする心配はない。

    0バイトのtest.jpgで再テストすると、今度はきちんと止まった。


    PDFも受け付けたい、という追加要件

    ここで追加要望が来た。

    パソコンから申し込む人はPDFで送ってくることもあるので、PDFも受け付けたい。

    ところが、PDFだと「最小画像サイズ」が壁になる。getimagesize()PDFを画像として読めないため、最小画像サイズを有効にしているとPDFは寸法0扱いで弾かれてしまう。画像の0バイト対策とPDF受け入れが、この設定では両立しない。

    最終方針:最小画像サイズを外し、0バイト対策はJSへ

    PDFを通すには最小画像サイズを外すしかない。ただし外すと0バイト対策も一緒に消える。そこで、0バイト検出を画像・PDF共通で効くクライアント側チェックに置き換えた。

    案2でサーバー側の$_FILES検証が効かなかったのに対し、選択した瞬間にJavaScriptでファイルサイズを見る方式なら、画像でもPDFでも同じように0バイトを止められる。しかもその場でユーザーに知らせられるので、iCloud未取得のケースにはむしろこちらの方が親切だ。

    // 身分証アップロード:0バイト・空ファイルを選択時点で弾く(画像・PDF共通)
    $('input[type="file"][name="certify"]').on('change', function () {
      var file = this.files && this.files[0];
      if (!file) return;
    
      // 100バイト未満は中身が無い/壊れていると判断(正規の写真・PDFは必ずこれ以上)
      if (file.size < 100) {
        alert('ファイルの中身が読み取れませんでした。\niPhoneの場合、iCloud上の写真が端末にダウンロードされていない可能性があります。\n写真アプリで一度その画像を開いてから、もう一度選択してください。');
        $(this).val('');
        return;
      }
    });

    閾値は100バイト。正規の身分証写真やPDFがこれを下回ることはないので誤ブロックはなく、0バイトだけでなく途中で切れた不完全ファイルも捕まえられる。

    PDFは問題なく通過し、データにもcertify-〜.pdfとして保存され、中身も開ける。

    最終的な設定

    MW WP Form のバリデーションルール(身分証フィールドcertify)は次のとおり。

    • 拡張子制限:jpg,jpeg,png,gif,pdf(スペースなし)
    • サイズ制限:31457280(30MB)
    • 最小画像サイズ:空(未設定) ← PDFを通すため
    • 加えて、上記のJSで0バイト・不完全ファイルを選択時に弾く

    この構成で「画像もPDFも受け付ける/0バイト・不完全ファイルは弾く/非対応形式は拡張子で弾く/巨大ファイルはサイズで弾く」が揃う。案3で得られたgetimagesize()による実体チェックは失うが、PDFを受ける以上そこは割り切り、0バイト対策はJSでカバーする形にした。


    まとめ

    今回の学びを整理すると、こうなる。

    MW WP Form の必須チェックは「ファイル名の有無」しか見ないため、0バイトや不完全なファイルはすり抜ける。iPhoneではiCloud未取得の写真を選ぶとこれが起きうる。バリデーション前にファイルが一時ディレクトリへ移されるため、$_FILESを見る自作検証は効かない。画像だけならgetimagesize()ベースの最小画像サイズが確実だが、PDFも受けるならこれは使えず、0バイト対策はJSに寄せる――この流れが、実際に手を動かして分かったことだ。

    「必須にしたから大丈夫」と思っていたフォームほど、実体が空のファイルには無防備だったりする。ファイルアップロードを扱うなら、有無だけでなく中身まで見ておきたい。