以前、JavaScriptで15パズルをつくりましたが、今回はユーザーが好きな画像をアップロードできるように15パズルを改良します。
Contents
画像をアップロードできるようにする
まず画像をアップロードできるようにしなければなりません。
HTML部分を示します。といってもここではアップロード用のフォームを表示させているだけです。
upload.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>画像をアップロードする</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <form action="upload.php" enctype="multipart/form-data" method="post"> <input name="file_upload" type="file"> <input type="submit" value="アップロード"> </form> </body> </html> |
アップロードボタンがクリックされたらupload.phpで処理がおこなわれます。
upload.php
1 2 3 4 5 6 7 8 9 |
<?php $upload = './'.$_FILES['file_upload']['name']; //アップロードが正しく完了したかチェック if(move_uploaded_file($_FILES['file_upload']['tmp_name'], $upload)) echo 'アップロード完了'; else echo 'アップロード失敗'; ?> |
PHPで画像ファイルかどうかを調べるには?
一応、これでアップロードはできるのですが、これだと問題があります。
それは画像以外のファイルでもアップロードできてしまうということです。画像ファイル以外のものをアップロードした場合(とくに有害スクリプト)は削除してしまいたいです。
exif_imagetype関数は危険かも?
exif_imagetype関数を使う方法がよく紹介されています。ただこの方法はあまりよくありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php $upload = './'.$_FILES['file_upload']['name']; if(move_uploaded_file($_FILES['file_upload']['tmp_name'], $upload)) { if(!exif_imagetype($upload)){ echo 'これは画像ファイルではありません'; if (unlink($upload)) echo $upload.'の削除に成功しました。'; else echo $upload.'の削除に失敗しました。'; } } else echo 'アップロード失敗'; ?> |
なぜexif_imagetype関数を使う方法がよくないかというと、この関数は画像の先頭バイトを読んで そのサインを調べているだけだからです。
たとえばテキストファイルをつくって以下を書き込みます。そしてファイル名をGIF画像っぽい名前にしてアップロードしてみると画像ファイルではないのに削除されません。GIFファイルの先頭部分は「GIF89a」なのでここだけ見てGIF画像であると判断しているようです。もしここに有害な作用をするスクリプトが仕込まれていたらゾッとします。
dangerous.gif(悪意があるファイル)
1 2 |
GIF89a <?php echo time(); |
imagecreatefromstring関数で解決
ではどうすればいいのか? imagecreatefromstring関数を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php $upload = './'.$_FILES['file_upload']['name']; //アップロードが正しく完了したかチェック if(move_uploaded_file($_FILES['file_upload']['tmp_name'], $upload)) { // PHPエラーを非表示にする。画像ファイルではないものをアップロードしてしまうと警告が表示されてしまう error_reporting(0); if (imagecreatefromstring(file_get_contents($upload)) !== false) echo 'アップロード完了'; else { // 悪意があるユーザーにエラーメッセージを見せる必要はあるのか?? echo 'これは画像ファイルではありません'; if (unlink($upload)) echo $upload.'の削除に成功しました。'; else echo $upload.'の削除に失敗しました。'; } } else echo 'アップロード失敗'; ?> |
これだと上記の偽装したGIFファイルや壊れた画像ファイルをアップロードすると削除されます。
ユーザーが画像をアップロードできる15パズルをつくる
ではユーザーが画像をアップできるようにしてみましょう。
同じ名前でファイルをアップロードすると上書きされてしまうので、元ファイル名の先頭にアップロード日時を加えることで名前がバッティングすることを防ぎします。また15パズルの本体があるディレクトリに大量の画像ファイルを置きたくないのでimagesというディレクトリ内に配置します。
upload.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php // 元ファイル名の先頭にアップロード日時を加える // 事前にimagesディレクトリを作成しておかないとエラーになるので注意 $upload = './images/'. date("Y-md-His")."-".$_FILES['file_upload']['name']; if(move_uploaded_file($_FILES['file_upload']['tmp_name'], $upload)) { // PHPエラーを非表示 if (imagecreatefromstring(file_get_contents($upload)) !== false) echo "アップロード完了"; else unlink($upload); } else echo "アップロード失敗"; ?> |
JavaScript側でアップロードされているファイルを取得できるようにします。そのためにクライアントサイドからディレクトリ内のファイルパスを取得できるように以下のようなphpファイルを用意します。
get-files.php
1 2 3 4 5 6 7 |
<?php $result = glob('./images/*'); foreach($result as $value){ echo $value . "\n"; } /* phpの閉じタグがあると余計な改行が入るのでないほうがいいとかしれない */ |
JavaScript部分の変更箇所
15パズル本体の処理が書かれているindex.js(JavaScriptで15パズルをつくるを参照)の以下の部分を変更します。
ページが読み込まれたときにおこなう処理をまるっと変更します。
get-files.phpにアクセスするとファイルパスが改行区切りで出力されるので、これを改行で分解してファイルのパスを取得します。そしてグローバル変数に格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let files = []; window.onload = async() => { let response = await fetch('./get-files.php'); let text = await response.text(); let arr = text.split('\n'); for(let i=0; i<arr.length; i++){ if(arr[i] == '' || arr[i] == '\r') continue; files.push(arr[i]); } gameStart() setVolume(0.05); } |
ゲームを開始する処理でランダムで画像を取得してpiecesの配列をクリアします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
async function gameStart(){ let index = Math.floor(Math.random() * files.length); let image = new Image(); let sourceImage = await new Promise(resolve => { image.width = colMax * pieceSize; image.height = rowMax * pieceSize; image.src = files[index]; image.onload =() => { resolve(image); } }); can.width = colMax * pieceSize + 4; can.height = rowMax * pieceSize + 4; // 元になる画像がその都度変更されるのでピースもその都度生成し直す pieces = []; for(let row = 0; row < rowMax; row++){ for(let col = 0; col < colMax; col++){ let image = await createImage(sourceImage, row, col, false); let outline = await createImage(sourceImage, row, col, true); pieces.push(new Piece(image, outline, col, row)); } } // ピースをシャッフルする。これ以降の処理は前回のものと同じ while(true){ if(shuffle() % 2 == 0) break; } isGameCleared = false; drawAll(); time = 0; $time.innerHTML = `${time} 秒`; clearInterval(timer); timer = setInterval(() => { time++; $time.style.color = '#fff'; $time.innerHTML = `${time} 秒`; }, 1000); } |