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

Source: https://php-school.pages.dev/topics/file-io/ch06-upload-concept/
Site: AIと学ぶPHPの学校 (https://php-school.pages.dev)
Lesson: ファイル入出力
Level: L4

Reference (official): https://www.php.net/manual/ja/book.filesystem.php

## 本文

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

## 概要

ファイルアップロードは「Web で唯一、バイナリを Body に乗せる仕組み」。普通のフォーム送信 (`application/x-www-form-urlencoded`) では Body に `key=value` の文字列しか乗らないが、画像など **ファイルを送るときは違うエンコーディング** が要る。それが `<form enctype="multipart/form-data">` で指定する **`multipart/form-data`**。Body を境界線で区切り、各セクションにヘッダ + 中身 (バイナリも OK) を並べる HTTP の特別な書式。

PHP 側にはアップロード情報が **`$_FILES` 連想配列** で届く。`$_FILES['avatar']['name']` (元のファイル名)、`['tmp_name']` (PHP が一時保存した場所)、`['size']`、`['type']` (ブラウザ自己申告 MIME)、`['error']` という構造。永続化は **`move_uploaded_file($tmp, $dest)`** を使うのが原則 (アップロード由来かを検証する保護付き)。

絶対に手を抜けないのが **セキュリティ**。「拡張子 / MIME / サイズ」をサーバー側で必ず再検証する。`$_FILES['type']` はブラウザの自己申告で信用できないので、`finfo_file()` で実体から MIME を判定するのが正解。`.php` など実行可能な拡張子をアップロードさせるのは致命的で、**保存ディレクトリは public 領域から分離** し、ホワイトリストで拡張子を絞る。CLI 採点では再現できないため、本章は **概念とドリル無しの座学** に振り切る。

## 学習目標

- 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: true
theme: dojo
paginate: true
size: 16: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` で体感してもらう構成

→ ドリルはなし。スライドを読み終わったら file-io トピック完了。

---

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

```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
<?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                ← バイト数
//   )
// )
```

- **`$_FILES['<name>']`** は **連想配列** (`name` / `type` / `tmp_name` / `error` / `size`)
- **`tmp_name`** = PHP が **自動で一時ディレクトリに保存したパス**。リクエスト終了時に消える
- 受け取った側の役目は **`tmp_name` を恒久保存先に移す** こと

---

## `move_uploaded_file` で恒久保存先に移す

```php
<?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("保存失敗");
}
```

- **`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.ini` の `upload_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 行返せると親切。

---

## 本物の動作確認 (任意)

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

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

`upload.php`:
```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";
```

```bash
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` コードを見て **「何が原因か」** をユーザーに返す重要性を理解した

→ **file-io トピック完了**。お疲れさまでした。
