0
0

【PHP, JavaScript】画像を並び替えてアップロードする【後編】

Last updated at Posted at 2023-12-02

前回の記事の続きです。

前回は、

  • アップロードする画像を選択したら、その場でプレビュー
  • ドラッグ&ドロップで画像の位置を入れ替える
  • 最終的な画像の位置情報と、画像を一緒に送信

までやりました。全てJavaScriptでの処理でしたが、今回はPHPのみの処理です。送信された位置情報を使って、アップロードされた画像をプレビューと同じ並びで表示します。

HTMLの構造が同じなので、前回作成したCSSファイルphotos_view.cssを読み込みます(前回の、「作成するファイル」のリンクからご覧ください)。

作成するファイル

今回やること

(1)位置情報photoOrderをJSON形式からデコード&画像の枚数を取得
(2)エラーチェック
(3)エラーが無ければ並び替えて画像を表示

upload_process.php
<?php
//*********************************(1)*******************************
$photo_order = json_decode($_POST['photo-order'], true);
$count_files = count($_FILES['photo']['name']);
//*********************************************************************
?>
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="./css/photos_view.css"/>
  </head>
  <body>
    <div id="photos-view">
      <div id="all-photos" class="layout-<?=$count_files;?>">
        <?php
        //*******************************(2)***************************************
          for($i = 0; $i < $count_files; $i++){
            if($_FILES['photo']['error'][$i] !== UPLOAD_ERR_OK){
              $msg = [
                UPLOAD_ERR_INI_SIZE => 'php.iniのupload_max_filesizeを超えています。',
                UPLOAD_ERR_FORM_SIZE => 'HTMLのmax_file_sizeを超えています。',
                UPLOAD_ERR_PARTIAL => 'ファイルが一部分しかアップロードされていません。',
                UPLOAD_ERR_NO_FILE => 'ファイルが存在しません。',
                UPLOAD_ERR_NO_TMP_DIR => '一時保存のためのフォルダが存在しません。',
                UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました。',
                UPLOAD_ERR_EXTENSION => '拡張モジュールによってアップロードが中断されました。',
              ];
              $error_msg = $msg[$_FILES['photo']['error'][$i]];
              die('<div>'.$error_msg.'</div>');
            }else if(!in_array(strtolower(pathinfo($_FILES['photo']['name'][$i])['extension']), ['gif', 'jpg', 'jpeg', 'png'])){
              $error_msg = '拡張子が画像ではありません。';
              die('<div>'.$error_msg.'</div>');
            }else if(!in_array(finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['photo']['tmp_name'][$i]),['image/gif', 'image/jpg', 'image/jpeg', 'image/png'])){
              $error_msg = 'コンテンツタイプが画像ではありません。';
              die('<div>'.$error_msg.'</div>');
            }else{
            //*******************************(3)**********************************
              $fp = fopen($_FILES['photo']['tmp_name'][$photo_order[$i]], 'rb');
              $get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
              fclose($fp);
              $encode_photo = base64_encode($get_photo);
              $photo_info = getimagesize('data:application/octet-stream;base64,' . $encode_photo);
              print '<div><img src="data:' . $photo_info['mime'] .';base64,' . $encode_photo . '"></div>';
            }
          }
        ?>
      </div>
    </div>
  </body>
</html>

アップロードされたファイルはどこにある?

  • アップロードされたファイルの情報は$_FILES['photo']にある。
  • ['photo']とは、<input type="file">に指定したname属性の値。
  • 各項目ごとに「ファイル名の昇順」で格納される(ブラウザによって違う?)。
スクリーンショット 2023-11-29 161938.png
  • $_FILES['photo']['type']
    ・アップロードしたファイルのコンテンツタイプ
  • $_FILES['photo']['tmp_name']
    ・アップロードしたファイルは、「サーバ上の一時的なファイル」に「仮保存」される。その一時ファイル名。つまり、アップロードしたファイルを取得するにはこの一時ファイルにアクセスする。例えば、
  1. move_uploaded_file関数を使って、一時ファイルから本来の保存先へファイルを移動。
    保存先はドキュメントルートの外にする。不正なファイルがドキュメントルート下にアップロードされるとそのまま実行されてしまうから。
  2. 一時ファイルをfopen関数で開いて、アップロードしたファイルの内容をfread関数で読み取る。【今回はこれによって画像を表示します】。
  • 一時ファイルは、move_uploaded_file関数で処理されなかった場合、スクリプトの処理後に自動的に破棄される。

(1)位置情報photoOrderをJSON形式からデコード&画像の枚数を取得

upload_process.php
<?php
$photo_order = json_decode($_POST['photo-order'], true);
$count_files = count($_FILES['photo']['name']);
?>

(2)エラーチェック

画像を1枚ずつ、3つのエラーチェックを行います。
①アップロードに関するエラーチェック
②拡張子をチェック
③コンテンツタイプをチェック(②より信頼性あり)

①アップロードに関するエラーチェック

アップロードされたファイルの情報は$_FILES['photo']にあります。各項目ごとに「ファイル名の昇順」で格納されています(ブラウザによって違う?)。

スクリーンショット 2023-11-29 161938.png

つまり、for文を使って「画像を1枚ずつチェック」することができます。例えば$_FILES['photo']['name'][$i]とすると、ファイル名を1つずつ取り出します。「$iは各ファイル」です。

エラーチェックがしたいので、$_FILES['photo']['error']を見ます。

 [error] => Array ( [0] => 0 [1] => 0 [2] => 0 [3] => 0 ) 

$_FILES['photo']['error']には、0~8(5は無い)の数字が格納されます。PHPに元から定義されている定数の値と一致します。これを使ってアップロード時のエラーチェックができます。

定数 概要
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 拡張モジュールによってアップロードが中断された
upload_process.php
          for($i = 0; $i < $count_files; $i++){
            if($_FILES['photo']['error'][$i] !== UPLOAD_ERR_OK){
              $msg = [
                UPLOAD_ERR_INI_SIZE => 'php.iniのupload_max_filesizeを超えています。',
                UPLOAD_ERR_FORM_SIZE => 'HTMLのmax_file_sizeを超えています。',
                UPLOAD_ERR_PARTIAL => 'ファイルが一部分しかアップロードされていません。',
                UPLOAD_ERR_NO_FILE => 'ファイルが存在しません。',
                UPLOAD_ERR_NO_TMP_DIR => '一時保存のためのフォルダが存在しません。',
                UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました。',
                UPLOAD_ERR_EXTENSION => '拡張モジュールによってアップロードが中断されました。',
              ];
              $error_msg = $msg[$_FILES['photo']['error'][$i]];
              die('<div>'.$error_msg.'</div>');
            }
         }

②拡張子をチェック

  • 配列に、許可する拡張子のリストを用意する。['gif', 'jpg', 'jpeg', 'png']
  • pathinfo関数は、ファイルパスに関する様々な情報を連想配列で取得する。その中にある拡張子['extension']を取りだす。取り出した拡張子をstrtolower関数で小文字に変換。
  • 拡張子が配列のリストの中になければエラー。
upload_process.php
  ...}else if(!in_array(strtolower(pathinfo($_FILES['photo']['name'][$i])['extension']), ['gif', 'jpg', 'jpeg', 'png'])){
              $error_msg = '拡張子が画像ではありません。';
              die('<div>'.$error_msg.'</div>');
     }

③コンテンツタイプをチェック(②より信頼性あり)

  • 配列に、許可するコンテンツタイプのリストを用意する。['image/gif', 'image/jpg', 'image/jpeg', 'image/png']
  • finfo_file関数は、ファイルの内容を解析し、コンテンツタイプを取り出す。引数に「FileInfoインスタンス(finfo_open関数で生成)」と「ファイル名」を渡す。
  • コンテンツタイプが配列のリストの中になければエラー。
upload_process.php
else if(!in_array(finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['photo']['tmp_name'][$i]),['image/gif', 'image/jpg', 'image/jpeg', 'image/png'])){
    $error_msg = 'コンテンツタイプが画像ではありません。';
    die('<div>'.$error_msg.'</div>');
}
  • FILEINFO_MIME_TYPEはmimeタイプを返す。finfo_open(FILEINFO_MIME_TYPE)はコンテンツタイプを取得することを意味する。
  • $_FILES['photo']['tmp_name']は、アップロードされたファイルが仮保存されている一時ファイル名。

$_FILES['photo']['type']でもアップロードしたファイルのコンテンツタイプを取得できる。しかし、悪意のあるユーザーにとって、typeキーの内容を偽装するのは簡単。予備的なものにとどめるべき。

(3)エラーが無ければ並び替えて画像を表示

プレビューと同じ並びで表示します。プレビュー時の画像の位置情報である、配列photoOrderの順番に表示処理をします。今まで$iを使って処理していましたが、photoOrder[$i]にします。

スクリーンショット 2023-11-30 124315.png
upload_process.php
}else{
 $fp = fopen($_FILES['photo']['tmp_name'][$photo_order[$i]], 'rb');
 $get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
 fclose($fp);
 
 $encode_photo = base64_encode($get_photo);
 $photo_info = getimagesize('data:application/octet-stream;base64,' . $encode_photo);
 print '<div><img src="data:' . $photo_info['mime'] . ';base64,' . $encode_photo . '"></div>';
}

2つに分けて説明します。

upload_process.php
 $fp = fopen($_FILES['photo']['tmp_name'][$photo_order[$i]], 'rb');
 $get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
 fclose($fp);
  • fopen関数・・・ノートの表紙を開くイメージ。
    ・ファイルやURLを開き、「ファイルポインタリソース(ファイルハンドル)」を返す。
    ・ファイルハンドルとは、「ファイルを操作するためのキーとなる情報」。ファイルに対する読み書きはファイルハンドルに対して行う。
    ・引数に「開きたいファイル名」と「開くモード」を指定。
    ・このコードでは、$_FILES['photo']['tmp_name'](アップロードされたファイルが仮保存されている一時ファイル)を、rbモードで開く。
  • rbモード
    rは読み込み専用。他にも種類あり。モードの後ろにt(テキストモード)かb(バイナリモード)を指定する。省略するとbになる。tはファイル内の改行を、OSに合ったものに変換する。bは何もしない。tに頼らず正しい改行コードを書き、bを使うのが強く推奨。
  • fread関数
    ・「ファイルポインタリソース(ファイルハンドル)」から最高○バイトを読み込み、読み込んだ文字列を返す。引数に「ファイルポインタリソース」と「最大バイト」を指定。最大バイトは、filesize関数で取得したファイルサイズ。つまり、ファイル全てを読み込む。
  • fclose関数・・・開いていたノートを閉じるイメージ
    ・オープンしたファイルポインタをクローズする
upload_process.php
$get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
//........省略
$encode_photo = base64_encode($get_photo);
$photo_info = getimagesize('data:application/octet-stream;base64,' . $encode_photo);
print '<div><img src="data:' . $photo_info['mime'] . ';base64,' . $encode_photo . '"></div>';
  • base64_encode関数・・・指定した文字列をbase64でエンコードして返す。
  • getimagesize関数・・・画像の大きさを取得
    ・引数に「情報を取得したいファイル(名)」を指定。ここでは「データURL」で指定してる。
getimagesize関数の結果(例)
Array ( [0] => 1200                           ...幅
        [1] => 800                            ...高さ
        [2] => 2                              ...画像の形式。PHPに定義されている定数の値
        [3] => width="1200" height="800",     ...<img>で使える
        [bits] => 8                           ...色のbit
        [channels] => 3                       ...RGB画像は3、CMYK画像は4 
        [mime] => image/jpeg )                ...(★)画像のMIMEタイプ

・<img>のsrc属性の値も「データURL」。データURLを表現するのに(★)画像のMIMEタイプを使う。

  • データURL'data:application/octet-stream;base64, . $encode_photo'
    ・「data:」がついたURL。小さなファイルをインラインで文書に埋め込むことができる。「data:MIMEタイプ;base64,データ本体」で表す。
  • MIMEタイプ
    「タイプ/サブタイプ」。text/plaintext/htmlなど。
    タイプ
    text・・・文字データ
    image・・・画像
    video・・・動画
    audio・・・音声
    application・・・上記以外。(例)圧縮ファイルは「application/zip」
    サブタイプ
    applicationのサブタイプで、「IANAメディアタイプ」に登録されていない形式のものはoctet-streamとする。IANAメディアタイプとは、MIMEタイプの標準規格。
    ・つまりapplication/octet-streamは、任意のバイナリファイルや、ファイル形式が不明な時に使う。
  • ;base64
    ・テキストデータ以外の時に書く。続く「,」下のデータ本体には、base64エンコードしたバイナリデータを指定する。

upload_process.php【完成】

upload_process.php
<?php
$photo_order = json_decode($_POST['photo-order'], true);
$count_files = count($_FILES['photo']['name']);
?>
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="./css/photos_view.css"/>
  </head>
  <body>
    <div id="photos-view">
      <div id="all-photos" class="layout-<?=$count_files;?>">
        <?php
          for($i = 0; $i < $count_files; $i++){
            if($_FILES['photo']['error'][$i] !== UPLOAD_ERR_OK){
              $msg = [
                UPLOAD_ERR_INI_SIZE => 'php.iniのupload_max_filesizeを超えています。',
                UPLOAD_ERR_FORM_SIZE => 'HTMLのmax_file_sizeを超えています。',
                UPLOAD_ERR_PARTIAL => 'ファイルが一部分しかアップロードされていません。',
                UPLOAD_ERR_NO_FILE => 'ファイルが存在しません。',
                UPLOAD_ERR_NO_TMP_DIR => '一時保存のためのフォルダが存在しません。',
                UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました。',
                UPLOAD_ERR_EXTENSION => '拡張モジュールによってアップロードが中断されました。',
              ];
              $error_msg = $msg[$_FILES['photo']['error'][$i]];
              die('<div>'.$error_msg.'</div>');
            }else if(!in_array(strtolower(pathinfo($_FILES['photo']['name'][$i])['extension']), ['gif', 'jpg', 'jpeg', 'png'])){
              $error_msg = '拡張子が画像ではありません。';
              die('<div>'.$error_msg.'</div>');
            }else if(!in_array(finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['photo']['tmp_name'][$i]),['image/gif', 'image/jpg', 'image/jpeg', 'image/png'])){
              $error_msg = 'コンテンツタイプが画像ではありません。';
              die('<div>'.$error_msg.'</div>');
            }else{
              $fp = fopen($_FILES['photo']['tmp_name'][$photo_order[$i]], 'rb');
              $get_photo = fread($fp, filesize($_FILES['photo']['tmp_name'][$photo_order[$i]]));
              fclose($fp);
              $encode_photo = base64_encode($get_photo);
              $photo_info = getimagesize('data:application/octet-stream;base64,' . $encode_photo);
              print '<div><img src="data:' . $photo_info['mime'] .';base64,' . $encode_photo . '"></div>';
            }
          }
        ?>
      </div>
    </div>
  </body>
</html>

【参考】
・『独習PHP 第4版』 山田祥寛著
PHPでの画像の保存・表示方法まとめ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0