topic: file-io (read / write / CSV / paths / upload概念) / ch06

ch06 — ファイルアップロードの概念

学習目標

  • HTML フォームのアップロード送信に必要な enctype="multipart/form-data" を言える
  • PHP 側に届く $_FILES の構造 (name / tmp_name / size / type / error) を読める
  • move_uploaded_file($tmp, $dest) で 一時ファイルを 保存先に移す 流れが分かる
  • アップロード処理での セキュリティ最低限 (拡張子ホワイトリスト / サイズ上限 / 保存ディレクトリの分離) を言える

所要時間

スライドのみ = 7〜8 分

ドリル

このチャプターにドリルはありません。

理由: ファイルアップロードは HTML フォーム → HTTP multipart/form-data → PHP の $_FILES という Web ランタイム前提の機能で、本コースの CLI 採点ランナーでは再現できない$_FILES を CLI で擬似的に埋めるスタブは書けるが、move_uploaded_file「本物のアップロード由来のファイルしか動かさない」 ように保護されているため、意味のあるドリルにならない。

代わりに 「概念と落とし穴」 をスライドで頭に入れ、実機 (php -S + ブラウザ) で 任意で試す案内とする。

本物の体験 (任意)

php -S でローカルサーバーを立ち上げてブラウザから試すと、5 分で全体像が掴める。スライド末尾の最小サンプル (form.html + upload.php) を写経して、ブラウザで localhost:8000/form.html を開いて画像を 1 枚アップしてみると分かりやすい。

次のステップ

ファイルアップロードを本格的に扱うのは Laravel など Web フレームワーク (バリデーション / Storage ドライバ / 画像変換ライブラリ) を学ぶ時。本コースは 「生 PHP で何が起きているか」を理解する ところまでに留める。

解説スライド

教材スライド(Marp)を1枚ずつ表示します。矢印キー(←→)・スワイプ・ナビボタンで移動できます。

  1. <!-- LLM_CONTEXT: Lesson 14 / Chapter 6 目的: ファイルアップロードの全体像 (multipart form / $_FILES / move_uploaded_file) と最低限のセキュリティを言葉にする 扱わない: 画像加工 / Storage ドライバ / バリデーションフレームワーク (Web フレームワークで覚える領域) 読み上げ時間目安: 7〜8 分 -->

    ファイルアップロードの概念

    Lesson 14 / Chapter 6

  2. なぜこのチャプターは「概念だけ」か

    • アップロードは ブラウザ → HTTP multipart/form-data → PHP の $_FILES という Web 前提の機能
    • 本コースの CLI 採点ランナーでは再現できない (move_uploaded_file が「本物のアップロード由来か」を内部チェックして弾く)
    • そこで本チャプターは 「全体像と落とし穴」 を言葉と絵で覚え、実機で試したい人だけ php -S で体感してもらう構成

    → ドリルはなし。スライドを読み終わったら L14 完了。

  3. HTML フォーム側に必要なもの

    <!DOCTYPE html>
    <form action="/upload.php" method="post" enctype="multipart/form-data">
      <input type="file" name="avatar">
      <button type="submit">アップロード</button>
    </form>
    要素 何のため
    method="post" ファイルは大きい / URL に乗せられないので 必ず POST
    enctype="multipart/form-data" バイナリも送れる multipart 形式 を指示 (これを忘れると $_FILES が空になる No.1 原因)
    <input type="file" name="avatar"> ファイル選択 UI。name 属性が PHP 側の $_FILES['avatar'] になる

    → 「enctype 付け忘れ」が初学者の事故 No.1。テンプレ化して覚える。

  4. PHP 側に届く $_FILES の構造

    <?php
    // /upload.php
    print_r($_FILES);
    
    // 出力 (avatar に hello.png をアップロードした場合):
    // Array (
    //   [avatar] => Array (
    //     [name]     => hello.png            ← 元のファイル名
    //     [type]     => image/png            ← MIME タイプ (ブラウザ申告: 信用しない)
    //     [tmp_name] => /tmp/phpAbCdEf       ← サーバー側で一時保存されたパス
    //     [error]    => 0                    ← UPLOAD_ERR_OK = 0
    //     [size]     => 12345                ← バイト数
    //   )
    // )
    ▶ 3v4l で実行
    • $_FILES['<name>']連想配列 (name / type / tmp_name / error / size)
    • tmp_name = PHP が 自動で一時ディレクトリに保存したパス。リクエスト終了時に消える
    • 受け取った側の役目は tmp_name を恒久保存先に移す こと
  5. move_uploaded_file で恒久保存先に移す

    <?php
    // /upload.php
    
    if (($_FILES['avatar']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        http_response_code(400);
        exit("アップロード失敗");
    }
    
    $tmp  = $_FILES['avatar']['tmp_name'];
    $dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']);
    
    if (move_uploaded_file($tmp, $dest)) {
        echo "OK: $dest に保存";
    } else {
        http_response_code(500);
        exit("保存失敗");
    }
    ▶ 3v4l で実行
    • move_uploaded_file($tmp, $dest) = アップロード由来の一時ファイルを 保存先に移す
    • rename ではなく move_uploaded_file を使う のが鉄則 (アップロード由来かを内部検証してくれる = 任意ファイル移動の悪用を弾く)
    • error が 0 (= UPLOAD_ERR_OK) かを 先に確認
  6. セキュリティ最低限 4 つ

    守ること 何をする
    拡張子ホワイトリスト pathinfo($name, PATHINFO_EXTENSION) を取り、許可リスト (['jpg','png','gif'] 等) に 入っていれば通す
    サイズ上限 $_FILES[..]['size'] を見て、上限超過なら 413 / 400 で拒否
    保存ディレクトリの分離 公開ディレクトリ (public/) に PHP 実行可能な状態で置かないuploads/.htaccess などで PHP 実行禁止に
    ファイル名のサニタイズ ユーザー名そのままを使わない。uniqid() などで サーバー側生成 にする (../../etc/passwd 攻撃の防止)

    「ブラックリスト方式」(.php を弾く) は破られやすい「ホワイトリスト方式」(.jpg だけ通す) を defult に。

  7. $_FILES['x']['error'] のエラーコード

    定数 意味
    UPLOAD_ERR_OK 0 成功
    UPLOAD_ERR_INI_SIZE 1 php.iniupload_max_filesize 超過
    UPLOAD_ERR_FORM_SIZE 2 HTML form の MAX_FILE_SIZE 超過
    UPLOAD_ERR_PARTIAL 3 一部しか届かなかった
    UPLOAD_ERR_NO_FILE 4 ファイル未選択
    UPLOAD_ERR_NO_TMP_DIR 6 一時ディレクトリ無し
    UPLOAD_ERR_CANT_WRITE 7 ディスクに書けない
    UPLOAD_ERR_EXTENSION 8 拡張モジュールが拒否

    「0 かどうか」だけで判断しない。ユーザーに 「何が原因か」 を 1 行返せると親切。

  8. 本物の動作確認 (任意)

    mkdir -p /tmp/upload-demo && cd /tmp/upload-demo
    mkdir uploads

    form.html:

    <form action="/upload.php" method="post" enctype="multipart/form-data">
      <input type="file" name="avatar">
      <button type="submit">送る</button>
    </form>

    upload.php:

    <?php
    if (($_FILES['avatar']['error'] ?? 4) !== 0) exit("err: ".$_FILES['avatar']['error']);
    $dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']);
    move_uploaded_file($_FILES['avatar']['tmp_name'], $dest);
    echo "saved to: $dest";
    ▶ 3v4l で実行
    php -S localhost:8000
    # ブラウザで http://localhost:8000/form.html を開いて 画像を 1 枚送信
  9. このチャプターでできるようになること

    ✅ HTML form で enctype="multipart/form-data" が必要だと言える ✅ $_FILES['<name>'] の構造 (name / tmp_name / size / type / error) を読める ✅ move_uploaded_file で 一時ファイルを保存先に移す流れを 図で説明できる ✅ セキュリティの最低限 (拡張子ホワイトリスト / サイズ上限 / 保存ディレクトリ分離 / ファイル名サニタイズ) を言える ✅ error コードを見て 「何が原因か」 をユーザーに返す重要性を理解した

    L14 完了。お疲れさまでした。

1 / 9
スライドを全部一気に読む(縦表示)

<!-- LLM_CONTEXT: Lesson 14 / Chapter 6 目的: ファイルアップロードの全体像 (multipart form / $_FILES / move_uploaded_file) と最低限のセキュリティを言葉にする 扱わない: 画像加工 / Storage ドライバ / バリデーションフレームワーク (Web フレームワークで覚える領域) 読み上げ時間目安: 7〜8 分 -->

ファイルアップロードの概念

Lesson 14 / Chapter 6


なぜこのチャプターは「概念だけ」か

  • アップロードは ブラウザ → HTTP multipart/form-data → PHP の $_FILES という Web 前提の機能
  • 本コースの CLI 採点ランナーでは再現できない (move_uploaded_file が「本物のアップロード由来か」を内部チェックして弾く)
  • そこで本チャプターは 「全体像と落とし穴」 を言葉と絵で覚え、実機で試したい人だけ php -S で体感してもらう構成

→ ドリルはなし。スライドを読み終わったら L14 完了。


HTML フォーム側に必要なもの

<!DOCTYPE html>
<form action="/upload.php" method="post" enctype="multipart/form-data">
  <input type="file" name="avatar">
  <button type="submit">アップロード</button>
</form>
要素 何のため
method="post" ファイルは大きい / URL に乗せられないので 必ず POST
enctype="multipart/form-data" バイナリも送れる multipart 形式 を指示 (これを忘れると $_FILES が空になる No.1 原因)
<input type="file" name="avatar"> ファイル選択 UI。name 属性が PHP 側の $_FILES['avatar'] になる

→ 「enctype 付け忘れ」が初学者の事故 No.1。テンプレ化して覚える。


PHP 側に届く $_FILES の構造

<?php
// /upload.php
print_r($_FILES);

// 出力 (avatar に hello.png をアップロードした場合):
// Array (
//   [avatar] => Array (
//     [name]     => hello.png            ← 元のファイル名
//     [type]     => image/png            ← MIME タイプ (ブラウザ申告: 信用しない)
//     [tmp_name] => /tmp/phpAbCdEf       ← サーバー側で一時保存されたパス
//     [error]    => 0                    ← UPLOAD_ERR_OK = 0
//     [size]     => 12345                ← バイト数
//   )
// )
▶ 3v4l で実行
  • $_FILES['<name>']連想配列 (name / type / tmp_name / error / size)
  • tmp_name = PHP が 自動で一時ディレクトリに保存したパス。リクエスト終了時に消える
  • 受け取った側の役目は tmp_name を恒久保存先に移す こと

move_uploaded_file で恒久保存先に移す

<?php
// /upload.php

if (($_FILES['avatar']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
    http_response_code(400);
    exit("アップロード失敗");
}

$tmp  = $_FILES['avatar']['tmp_name'];
$dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']);

if (move_uploaded_file($tmp, $dest)) {
    echo "OK: $dest に保存";
} else {
    http_response_code(500);
    exit("保存失敗");
}
▶ 3v4l で実行
  • move_uploaded_file($tmp, $dest) = アップロード由来の一時ファイルを 保存先に移す
  • rename ではなく move_uploaded_file を使う のが鉄則 (アップロード由来かを内部検証してくれる = 任意ファイル移動の悪用を弾く)
  • error が 0 (= UPLOAD_ERR_OK) かを 先に確認

セキュリティ最低限 4 つ

守ること 何をする
拡張子ホワイトリスト pathinfo($name, PATHINFO_EXTENSION) を取り、許可リスト (['jpg','png','gif'] 等) に 入っていれば通す
サイズ上限 $_FILES[..]['size'] を見て、上限超過なら 413 / 400 で拒否
保存ディレクトリの分離 公開ディレクトリ (public/) に PHP 実行可能な状態で置かないuploads/.htaccess などで PHP 実行禁止に
ファイル名のサニタイズ ユーザー名そのままを使わない。uniqid() などで サーバー側生成 にする (../../etc/passwd 攻撃の防止)

「ブラックリスト方式」(.php を弾く) は破られやすい「ホワイトリスト方式」(.jpg だけ通す) を defult に。


$_FILES['x']['error'] のエラーコード

定数 意味
UPLOAD_ERR_OK 0 成功
UPLOAD_ERR_INI_SIZE 1 php.iniupload_max_filesize 超過
UPLOAD_ERR_FORM_SIZE 2 HTML form の MAX_FILE_SIZE 超過
UPLOAD_ERR_PARTIAL 3 一部しか届かなかった
UPLOAD_ERR_NO_FILE 4 ファイル未選択
UPLOAD_ERR_NO_TMP_DIR 6 一時ディレクトリ無し
UPLOAD_ERR_CANT_WRITE 7 ディスクに書けない
UPLOAD_ERR_EXTENSION 8 拡張モジュールが拒否

「0 かどうか」だけで判断しない。ユーザーに 「何が原因か」 を 1 行返せると親切。


本物の動作確認 (任意)

mkdir -p /tmp/upload-demo && cd /tmp/upload-demo
mkdir uploads

form.html:

<form action="/upload.php" method="post" enctype="multipart/form-data">
  <input type="file" name="avatar">
  <button type="submit">送る</button>
</form>

upload.php:

<?php
if (($_FILES['avatar']['error'] ?? 4) !== 0) exit("err: ".$_FILES['avatar']['error']);
$dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']);
move_uploaded_file($_FILES['avatar']['tmp_name'], $dest);
echo "saved to: $dest";
▶ 3v4l で実行
php -S localhost:8000
# ブラウザで http://localhost:8000/form.html を開いて 画像を 1 枚送信

このチャプターでできるようになること

✅ HTML form で enctype="multipart/form-data" が必要だと言える ✅ $_FILES['<name>'] の構造 (name / tmp_name / size / type / error) を読める ✅ move_uploaded_file で 一時ファイルを保存先に移す流れを 図で説明できる ✅ セキュリティの最低限 (拡張子ホワイトリスト / サイズ上限 / 保存ディレクトリ分離 / ファイル名サニタイズ) を言える ✅ error コードを見て 「何が原因か」 をユーザーに返す重要性を理解した

L14 完了。お疲れさまでした。