ディレクトリ作成を許可してファイルをアップロード

# **前書き**

まだ **[「ファイルアップロードの例外処理はこれぐらいしないと気が済まない」](http://qiita.com/mpyw/items/939964377766a54d4682)** をご覧になっていない方は先にそちらからどうぞ。

タイトルの通り、ディレクトリ作成を許可してユーザーにファイルをアップロードさせたいときのためのスクリプトです。今回は以下の条件のもと行ってみます。

- ファイル名はそのまま使用する
- 許可するファイル形式を `JPEG` `PNG` `GIF` のみに制限する
- ファイル名またはディレクトリ名に使われる文字を正規表現 `[\w.-]` にマッチするもののみに制限する
- スクリプトとして実行可能な `*.php` `*.cgi` `*.rb` `*.py` にマッチするファイル名のものは禁止する **(トロイの木馬対策)**
- ディレクトリを10階層までに制限する
- `.` で始まるまたは終わる「ファイル」「2文字以上のディレクトリ」は許可しない **(ディレクトリトラバーサル対策)**
- 絶対パスによるディレクトリ指定は許可しない

# **ソースコード**

コピペで動くと思います。サーバーに他の実行可能な拡張子が存在する場合はそれも禁止対象に追加してください。

```html+php
<?php

/* HTML特殊文字をエスケープする関数 */
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

// 全てのパラメータを正しい構造で受け取った時のみ実行
if (
    isset($_POST['path'], $_FILES['upfile']['error']) &&
    is_int($_FILES['upfile']['error']) &&
    is_string($_POST['path'])
) {
    
    try {
        
        /* ファイルのチェック */
        switch ($_FILES['upfile']['error']) {
            case UPLOAD_ERR_OK:
                // エラー無し
                break;
            case UPLOAD_ERR_NO_FILE:
                // ファイル未選択
                throw new RuntimeException('File is not selected');
            case UPLOAD_ERR_INI_SIZE:
            case UPLOAD_ERR_FORM_SIZE:
                // 許可サイズを超過
                throw new RuntimeException('File is too large');
            default:
                throw new RuntimeException('Unknown error');
        }
        if (!in_array(
            @exif_imagetype($_FILES['upfile']['tmp_name']),
            array(
                IMAGETYPE_GIF,
                IMAGETYPE_JPEG,
                IMAGETYPE_PNG,
            ),
            true
        )) {
            // JPEG, PNG, GIF ではない
            throw new RuntimeException('Unsupported image format');
        }
        if (!preg_match('/\A(?!\.)[\w.-]++(?<!\.)\z/', $_FILES['upfile']['name'])) {
            // 無効なファイル名
            throw new RuntimeException('Invalid filename: ' . $_FILES['upfile']['name']);
        }
        if (!preg_match('/(?<!\.php)(?<!\.cgi)(?<!\.py)(?<!\.rb)\z/i', $_FILES['upfile']['name'])) {
            // トロイの木馬を弾くため、実行可能な拡張子は禁止する
            throw new RuntimeException(
                'This extension is forbidden for security reason: ' .
                $_FILES['upfile']['name']
            );
        }
        
        /* ディレクトリの生成 */
        $deep = 0;
        foreach (explode('/', $_POST['path']) as $i => $dir) {
            if ($deep > 10) {
                // 10階層を超過
                throw new RuntimeException('Hierarchy is too deep');
            }
            if ($dir === '') {
                if ($_POST['path'] !== '' && !$i) {
                    // 絶対パスを検知
                    throw new RuntimeException('Absolute path is not allowed');
                }
                // 空文字列はスキップ
                continue;
            }
            if ($dir === '.') {
                // 「.」はスキップする
                continue;
            }
            if (!preg_match('/\A(?!\.)[\w.-]++(?<!\.)\z/', $dir)) {
                // 無効なディレクトリ名
                throw new RuntimeException('Invalid directory name: ' . $dir);
            }
            if (!is_dir($dir)) {
                // ディレクトリが存在していなければ生成を試みる
                if (!mkdir($dir)) {
                    // ディレクトリ生成に失敗
                    throw new RuntimeException('Failed to create directory: ' . $dir);
                }
                // パーミッションを0777に設定
                chmod($dir, 0777);
                $msgs[] = array('blue', 'Created directory "' . $dir . '"');
            }
            // カレントディレクトリを移動
            chdir($dir);
            ++$deep;
        }
        
        /* ファイルの移動 */
        if (!move_uploaded_file($_FILES['upfile']['tmp_name'], $_FILES['upfile']['name'])) {
            // ファイル移動に失敗
            throw new RuntimeException('Failed to save uploaded file');
        }
        // ファイルのパーミッションを確実に0644に設定する
        chmod($_FILES['upfile']['name'], 0644);
        
        /* メッセージをセット */
        $msgs[] = array(
            'green',
            'Uploaded successfully: ' .
                ($_POST['path'] === '' ? '.' : $_POST['path']) .
                '/' .
                $_FILES['upfile']['name']
        );

    } catch (RuntimeException $e) {

        $msgs[] = array('red', $e->getMessage());

    }

}

// XHTMLとしてブラウザに認識させる
// (IE8以下はサポート対象外ｗ)
header('Content-Type: application/xhtml+xml; charset=utf-8');

?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Hierarchical Image Uploading</title>
</head>
<body>
<?php if (isset($msgs)): ?>
  <fieldset>
    <legend>Result</legend>
    <ul>
<?php foreach ($msgs as $msg): ?> 
      <li style="color:<?=h($msg[0])?>;"><?=h($msg[1])?></li>
<?php endforeach; ?>
    </ul>
  </fieldset>
<?php endif; ?>
  <form enctype="multipart/form-data" method="post" action="">
    <fieldset>
      <legend>Select file (Directory name and filename must match <strong>'/\A(?!\.)[\w.-]++(?&lt;!\.)\z/'</strong>)</legend>
      Directory path: <input type="text" name="path" value="" /><br />
      Filename(GIF, JPEG, PNG): <input type="file" name="upfile" /><br />
      <input type="submit" value="Upload" />
    </fieldset>
  </form>
</body>
</html>
```