脱初心者備忘録

PHPのPOSTでファイルをアップロードする

サイトを制作していると、かなりの頻度で必要になるのが、ファイルのアップロード。
画像のアップロードや、最近多いのが動画のアップロード。
ユーザーが選んだファイルをそのままアップする場合と、アップした後、サーバー側でサイズ変換したり、型変換し整形する場合があります。
まずは、アップロードの基本から。

アップロードのフォームを作成

アップロードするには、基本的にフォームが必要です。
まず基本となるフォームを作成します。

//HTMLタグ
    <form action="post.php" method="post" enctype="multipart/form-data">
      <input type="hidden" name="action" value="upload">
      <input type="hidden" name="token" value="<?php echo getToken(); ?>">
      <div class="form">
        <div class="inputs">
          <label for="upfile">ファイル選択:</label>
          <input type="file" id="upfile" name="upfile" accept="image/*">
        </div>

        <button type="submit" class="upbtn">アップロードする</button>
      </div>
    </form>

【タグの説明】

<form action="post.php" method="post" enctype="multipart/form-data">

formタグで、post先・method方法・enctypeの指定をします。
ファイルをアップロードするフォームにはenctypeにmultipart/form-dataの指定が必要です。

<input type="file" id="upfile" name="upfile" accept="image/*">

アップロードしたいファイルを選択する部分は、inputタグのtypeでfileを指定し、許容できるファイルを画像系に指定します。

<input type="hidden" name="action" value="upload">
<input type="hidden" name="token" value="<?php echo getToken(); ?>">

inputのtype=hiddenで、フォームの目的とトークンの、2つの隠しデータを送信します。
トークンはcsrf対策として有効な方法で、フォーム送信の安全性には欠かせません。
ランダムで解読が難しい安全性の高い文字列をPHPで生成したものをトークンに使用します。

クロスサイトリクエストフォージェリ

クロスサイトリクエストフォージェリは、Webアプリケーションの脆弱性の一つ、もしくはそれを利用した攻撃。略称はCSRF。
攻撃者はブラウザなどのユーザ・クライアントを騙し、意図しないリクエスト(たとえばHTTPリクエスト)をWebサーバに送信させる。

ウィキペディア

フォームをPHPで実装する

フォームをindex.phpに実装します。
include~でfunctions.php を呼び出し、session関数を利用し、データをセッションに格納します。
$_SESSION['warning']では、アップロードの処理中にエラーが出た場合のエラーを格納しています。

//index.php

<?php
include_once(__DIR__ . '/functions.php');

session_start();

setToken();
?>
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>アップロード</title>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <h1>ファイルアップロード</h1>
  <?php
  if (isset($_SESSION['warning']) && !empty($_SESSION['warning'])) {
  ?>
    <div class="alert">
      <?php
      echo '<p>' . $_SESSION['warning'] . '</p>';
      ?>
    </div>
  <?php
    unset($_SESSION['warning']);
  }
  ?>
  <div class="form_area">
    <form action="post.php" method="post" enctype="multipart/form-data">
      <input type="hidden" name="action" value="upload">
      <input type="hidden" name="token" value="<?php echo getToken(); ?>">
      <div class="form">
        <div class="inputs">
          <label for="upfile">ファイル選択:</label>
          <input type="file" id="upfile" name="upfile" accept="image/*">
        </div>

        <button type="submit" class="upbtn">アップロードする</button>
      </div>
    </form>
  </div>
</body>

</html>

【コードの説明】

include_once(__DIR__ . '/functions.php');

include_once で呼び出した functions.php には、index.phpやpost.phpで使う関数を入れてあります。
index.phpの中に直接関数で書いても良いのですが、複数のPHPファイルを使用する際は、関数はまとめて記載して別ファイルにするほうが、修正も少なくて済むので便利です。

session_start();
setToken();

セッションを開始して、functions.php に書いたsetToken() でトークンを生成します。
setToken() は、セッションにcsrf_tokenの値が無ければ、暗号論的に安全な12バイトのランダム文字列を生成し、16進表現に変換したものを値に入れて返す関数です。

<?php
  if (isset($_SESSION['warning']) && !empty($_SESSION['warning'])) {
  ?>
    <div class="alert">
      <?php
      echo '<p>' . $_SESSION['warning'] . '</p>';
      ?>
    </div>
  <?php
    unset($_SESSION['warning']);
  }
  ?>

これはフォームをPOSTした後に、エラーでindex.phpに戻る際、そのエラー内容を表示するエリアです。入力間違いや送信エラーなど、何らかのエラーがあった場合、ただ画面を戻すだけでなく、ユーザーにエラーの表示することは大事なステップです。
セッションにwarningの値が入っていれば、alertクラスでwarningの値を表示します。

//functions.php
<?php

function setToken() {
  global $_SESSION;

  if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(12));
  }

  return;
}

function getToken() {
  global $_SESSION;
  $token = null;

  if (isset($_SESSION['csrf_token'])) {
    $token = $_SESSION['csrf_token'];
  }

  return $token;
}

//アップロードしたファイル名を時間.拡張子に書き換える
function changeName($filename) {
  $ext = substr($filename, strrpos($filename, '.') + 1);
  $today = microtime(true);
  $time = str_replace('.', '', $today);
  $name = $time . '.' . $ext;
  return $name;
}

changeName($filename) でファイル名の書き換えを行っています。
英語圏なら問題ないのですが、日本語の場合は、ひらがなや漢字のファイル名の場合もあるので、サーバーにアップした時、ファイル名が文字化けしたりします。
それを防ぐためにも、ファイル名を半角英数字に変換するのがベストだと思っています。
アップした日時をファイル名にすれば、基本的に重複は防げるので、アップロード時にchangeName()で名前を変換します。

POST先のPHPを作成する

同じindex.phpにPOSTしても良いのですが、わかりやすくするために今回は別ファイルのpost.php にPOSTします。

//post.php
<?php
include_once(__DIR__ . '/functions.php');

session_start();

$action = isset($_POST['action']) ? $_POST['action'] : null;
$token = getToken();
$extensions = array('jpeg', 'jpg', 'bmp', 'gif');
$_SESSION['warning'] = null;
$saveDir = __DIR__ . '/images/';
$filename = null;
$flag = 0;
define('MAX_FILE_SIZE', 10000000);

if (isset($action) && ($action === "upload") && isset($_POST['token']) && ($_POST['token'] === $token)) {


  if (isset($_FILES['upfile'])) {
    $file = $_FILES['upfile'];

    if (!empty($file['tmp_name']) && ($file['tmp_name'] != 'none') && is_uploaded_file($file['tmp_name'])) {

      if ($file['size'] > MAX_FILE_SIZE) {
        $_SESSION['warning'] = 'アップロードできるファイルサイズを超えています。';
      }

      if (!in_array(strtolower(substr($file['name'], strrpos($file['name'], '.') + 1)), $extensions)) {
        $_SESSION['warning'] = 'アップロードできるのは画像ファイルのみです。';
      }

      $filesize = $file['size'];
      $filename = changeName($file['name']);

    } else {
      $_SESSION['warning'] = '画像が選択されていません。';
    }

    if (!empty($_SESSION['warning'])) {
      header('Location: http://localhost/test/index.php');
    }

    if($file['error'] === 0) {
      if (move_uploaded_file($file['tmp_name'], $saveDir . $filename)) {
        chmod($saveDir . $filename, '0666');
        $flag = 1;

        $this_file = 'images/' . $filename;

        $fileKsize = $filesize / 1000;
        $size = $fileKsize . 'KB';
        if ($fileKsize > 1000) {
          $fileMsize = round($fileKsize / 1000, 1);
          $size = $fileMsize . 'MB';
        }

        $imagesize = getimagesize($this_file);
        $width = $imagesize[0];
        $height = $imagesize[1];
      }
    }


  } else {
    $_SESSION['warning'] = "アップロードするファイルを選択してください。";
    header('Location: http://localhost/test/index.php');
  }
} else {
  $_SESSION['warning'] = "不正なアクセスです。";
  header('Location: http://localhost/test/index.php');
}

if ($flag === 1) {
?>
  <!DOCTYPE html>
  <html lang="ja">

  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>アップロード完了</title>
    <link rel="stylesheet" href="style.css">
  </head>

  <body>
    <h1>アップロードが成功しました</h1>
    <div class="form_area">
      アップロードされたファイル
      <div><img src="<?php echo $this_file; ?>" alt=""><br /></div>
      <ul>
        <li>ファイル名:<?php echo $filename;?></li>
        <li>ファイルサイズ:<?php echo $size;?></li>
        <li>ファイルの横幅×高さ:<?php echo $width;?> x <?php echo $height;?></li>
      </ul>
    </div>
  </body>

  </html>
<?php
} else {
  echo 'アップロードエラー';
}

【コードの説明】

include_once(__DIR__ . '/functions.php');

ここでも 共通で使える関数を書いた functions.php を呼び出します。

session_start();

index.php と同じく、ここでもセッションを開始します。ページごとに開始します。

$action = isset($_POST['action']) ? $_POST['action'] : null;
$token = getToken();
$extensions = array('jpeg', 'jpg', 'bmp', 'gif');
$_SESSION['warning'] = null;
$saveDir = __DIR__ . '/images/';
$filename = null;
$flag = 0;
define('MAX_FILE_SIZE', 10000000);

このファイルで使う変数の宣言です。
$actionはフォームPOST時に隠しデータで送ったactionの値。
$tokenはセッションに格納しているトークンの値の取得。
$extensionsは、アップロードで許可する拡張子の配列。
セッションのwarningは、フォームのエラーを格納するもので、最初は初期化しています。
$saveDirは、アップロードした画像を保存するディレクトリ。
$filenameは、アップロードした画像の名前。
$flagは、画像アップロードの成功失敗。最初は初期化で0。
define('MAX_FILE_SIZE', 10000000); 定数指定で、一度にアップロードできるファイルサイズの最大値を指定しています。現在は、10MBまでのファイルならOKの意味です。

if (isset($action) && ($action === "upload") && isset($_POST['token']) && ($_POST['token'] === $token)) {

} else {
  $_SESSION['warning'] = "不正なアクセスです。";
  header('Location: http://localhost/test/index.php');
}

これは、もしフォームPOSTの際に、変数actionが存在して、かつ、その値がuploadで、かつ、POSTされた値にtokenが存在して、かつ、そのPOSTされたtokenの値が、変数token(セッションに格納されたトークン)の値と一致したなら真の if文です。
フォームのPOSTでhiddenに設定した値と比較しています。
もしこの if文が偽ならば、セッションのwarningにエラー文を格納し、headerでindex.phpにリダイレクトさせます。

if (isset($_FILES['upfile'])) {
    $file = $_FILES['upfile'];

アップロードされたupfileというnameのファイルがあるなら真、の if文です。
これはフォームのファイルを選んだ部分、inputのtype=fileのnameの値です。
$_FILES['upfile']を変数fileに代入します。

if (!empty($file['tmp_name']) && ($file['tmp_name'] != 'none') && is_uploaded_file($file['tmp_name'])) {

} else {
    $_SESSION['warning'] = '画像が選択されていません。';
}

いきなり、ややこしそうに見えますが、変数fileのtmp_nameが空でなくて、noneでも無くて、HTTP POST でアップロードされたファイルであるなら真という if文です。
偽で画像がアップロードされていないということになります。
ここで、変数file は配列であることが分かると思います。

//$file($_FILES['upfile'])の内容 
//※テストでimg001.jpgをアップロードした場合

array(6) { 
    ["name"]=> string(10) "img001.jpg" 
    ["full_path"]=> string(10) "img001.jpg" 
    ["type"]=> string(10) "image/jpeg" 
    ["tmp_name"]=> string(15) "tmp\php925C.tmp" 
    ["error"]=> int(0) 
    ["size"]=> int(1629679) 
}

name、full_pathでアップしたファイル名、typeでMIMEタイプ、tmp_nameで一時ファイル名、errorで数字、sizeでファイルサイズの値を持つ配列です。
この配列の内容を利用して、処理を行います。

if ($file['size'] > MAX_FILE_SIZE) {
    $_SESSION['warning'] = 'アップロードできるファイルサイズを超えています。';
}

if (!in_array(strtolower(substr($file['name'], strrpos($file['name'], '.') + 1)), $extensions)) {
    $_SESSION['warning'] = 'アップロードできるのは画像ファイルのみです。';
}

上で定数指定した最大ファイルサイズより大きなファイルであれば、エラーを格納します。
また、ファイルの拡張子が、上で指定した拡張子(extensions)でなければ、エラーを格納します。

if (!empty($_SESSION['warning'])) {
      header('Location: http://localhost/test/index.php');
}

セッションのwarningが空でなければ、index.phpにリダイレクトします。
※エラーがあるので、index.phpに戻します。

$filesize = $file['size'];
$filename = changeName($file['name']);

ここで、変数filesizeに、変数fileのsizeの値を代入します。
さらに変数filenameに、変数fileのnameを functions.phpで変換したものを代入します。(リネーム)

if($file['error'] === 0) {
    if (move_uploaded_file($file['tmp_name'], $saveDir . $filename)) {

    }
}

変数fileのerrorの値が0なら真、さらに、move_uploaded_file が真、のif文です。
値が0でなければ、なんらかのエラーが発生しているということです。
$_FILES[name]['error']は、常に数字でエラーを返します。

UPLOAD_ERR_OK
値: 0; エラーはなく、ファイルアップロードは成功しています。
UPLOAD_ERR_INI_SIZE
値: 1; アップロードされたファイルは、php.ini の upload_max_filesize ディレクティブの値を超えています。
UPLOAD_ERR_FORM_SIZE
値: 2;
アップロードされたファイルは、HTML フォームで指定された 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
; PHP の拡張モジュールがファイルのアップロードを中止しました。

PHPマニュアル

値の5がないのは書き忘れではありませんのであしからず。

move_uploaded_file(string $from, string $to);
$from:アップロードしたファイルのファイル名。
$to:移動したい場所のパス(ファイル名込み)

PHPマニュアル

move_uploaded_file() の戻り値は、移動に成功したらtrue、失敗したら処理は行わずfalseを返します。
post.php で、move_uploaded_file の to部分に入れているのは、変更したいファイル名です。
この部分でリネームも行っています。

chmod($saveDir . $filename, '0666');
$flag = 1;

ファイルの移動も成功したので、ここで、ファイルの属性を変更して保存します。
変数flagに1を代入して成功フラグを立てます。

$fileKsize = $filesize / 1000;
$size = $fileKsize . 'KB';
if ($fileKsize > 1000) {
  $fileMsize = round($fileKsize / 1000, 1);
  $size = $fileMsize . 'MB';
}

ここからは任意で、あると便利な機能。
ファイルサイズが現在バイトになっているので、KBやMBなど見やすい単位に変更しています。

$this_file = 'images/' . $filename;
$imagesize = getimagesize($this_file);
$width = $imagesize[0];
$height = $imagesize[1];

これは、getimagesize() という関数を使用して、画像の情報を取得し、サイズを出しています。
getimagesize() の戻り値は配列になっており、0番目は横幅、1番目は高さを表します。

ここまででPHPの説明は終わりです。
post.phpでは、変数flagが1であればHTML部分を表示する仕様にしています。
HTML部分では、アップロードした画像ファイルと、情報を表示しています。

ファイルのプレビュー

各画面でのスクリーンショットです。

①最初の画面 index.php

②画像を選択後

③アップロードボタンを押した後 post.php

④エラーがあった場合 index.phpに戻った画面