【2021/10/15 追記】
この記事は更新が停止されています。現在では筆者の思想が変化している面もありますので,過去の記事として参考程度にご覧ください。
前書き
より一般化したものについては 「ファイルアップロードの例外処理はこれぐらいしないと気が済まない」 を参照。ここではそれを元に画像ファイルに限定して、いくつかのパターンで例を構成してみる。また、フォームの送信と受信を同一ファイルで行うとする。
サンプル集
1. exif_imagetype 関数を用いてチェックを行う
これは、画像に関するMIMEタイプを整数として返す関数である。finfoクラスが使えない環境においても統一的にこちらの関数は使えるはず。但し、画像の形式を判別するために必要なだけのバイト数を読み込めない場合にエラーを発生するので、 エラー制御演算子 @
を用いて抑制する必要がある。以前ここでは getimagesize 関数を用いる手法を掲載していたが、こちらもマニュアルには記載されていないだけでエラーを発生するリスクを抱えていたので、抑制がいずれ必要となるならば…ということで一番動作が速いこの関数を採用することにした。
なお、この定数から実際の文字列形式の MIMEタイプ や 拡張子 を得るための関数も用意されている。
<?php
if (isset($_FILES['upfile']['error']) && is_int($_FILES['upfile']['error'])) {
try {
// $_FILES['upfile']['error'] の値を確認
switch ($_FILES['upfile']['error']) {
case UPLOAD_ERR_OK: // OK
break;
case UPLOAD_ERR_NO_FILE: // ファイル未選択
throw new RuntimeException('ファイルが選択されていません');
case UPLOAD_ERR_INI_SIZE: // php.ini定義の最大サイズ超過
case UPLOAD_ERR_FORM_SIZE: // フォーム定義の最大サイズ超過
throw new RuntimeException('ファイルサイズが大きすぎます');
default:
throw new RuntimeException('その他のエラーが発生しました');
}
// $_FILES['upfile']['mime']の値はブラウザ側で偽装可能なので、MIMEタイプを自前でチェックする
$type = @exif_imagetype($_FILES['upfile']['tmp_name']);
if (!in_array($type, [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
throw new RuntimeException('画像形式が未対応です');
}
// ファイルデータからSHA-1ハッシュを取ってファイル名を決定し、ファイルを保存する
$path = sprintf('./uploads/%s%s', sha1_file($_FILES['upfile']['tmp_name']), image_type_to_extension($type));
if (!move_uploaded_file($_FILES['upfile']['tmp_name'], $path)) {
throw new RuntimeException('ファイル保存時にエラーが発生しました');
}
chmod($path, 0644);
$msg = ['green', 'ファイルは正常にアップロードされました'];
} catch (RuntimeException $e) {
$msg = ['red', $e->getMessage()];
}
}
// XHTMLとしてブラウザに認識させる
// (IE8以下はサポート対象外w)
header('Content-Type: application/xhtml+xml; charset=utf-8');
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>画像アップロード</title>
</head>
<body>
<?php if (isset($msg)): ?>
<fieldset>
<legend>結果</legend>
<span style="color:<?=$msg[0]?>;"><?=$msg[1]?></span>
</fieldset>
<?php endif; ?>
<form enctype="multipart/form-data" method="post" action="">
<fieldset>
<legend>画像ファイルを選択(GIF, JPEG, PNGのみ対応)</legend>
<input type="file" name="upfile" /><br />
<input type="submit" value="送信" />
</fieldset>
</form>
</body>
</html>
もしファイルではなく $data
に文字列として保持しているデータをチェックしたい場合、以下のようにする。
@exif_imagetype(
'data://application/octet-stream;base64,' .
base64_encode($data)
)
2. imagecreatefromstring 関数から全てPNGとして保存する
環境によって差があるが、PHPのGDライブラリは GIF
JPEG
PNG
に加えて BMP
XPM
に対応している場合もある。ここではこれら全てを自動判別できる imagecreatefromstring 関数を利用する。この関数はリソース生成失敗時にエラーを発生するため、1番目の例と同様に エラー制御演算子 @
を用いて抑制する必要がある。
<?php
if (isset($_FILES['upfile']['error']) && is_int($_FILES['upfile']['error'])) {
try {
// $_FILES['upfile']['error'] の値を確認
switch ($_FILES['upfile']['error']) {
case UPLOAD_ERR_OK: // OK
break;
case UPLOAD_ERR_NO_FILE: // ファイル未選択
throw new RuntimeException('ファイルが選択されていません');
case UPLOAD_ERR_INI_SIZE: // php.ini定義の最大サイズ超過
case UPLOAD_ERR_FORM_SIZE: // フォーム定義の最大サイズ超過
throw new RuntimeException('ファイルサイズが大きすぎます');
default:
throw new RuntimeException('その他のエラーが発生しました');
}
// GD画像リソースの生成を試みる
if (!$img = @imagecreatefromstring(file_get_contents($_FILES['upfile']['tmp_name']))) {
throw new RuntimeException('有効な画像ファイルを指定してください');
}
// ファイルデータからSHA-1ハッシュを取ってファイル名を決定し、保存する
if (!imagepng($img, sprintf('./uploads/%s.png', sha1_file($_FILES['upfile']['tmp_name'])))) {
throw new RuntimeException('ファイル保存時にエラーが発生しました');
}
$msg = ['green', 'ファイルは正常にアップロードされました'];
} catch (RuntimeException $e) {
$msg = ['red', $e->getMessage()];
}
// リソースを解放
if (isset($img) && is_resource($img)) {
imagedestroy($img);
}
}
// XHTMLとしてブラウザに認識させる
// (IE8以下はサポート対象外w)
header('Content-Type: application/xhtml+xml; charset=utf-8');
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>画像アップロード</title>
</head>
<body>
<?php if (isset($msg)): ?>
<fieldset>
<legend>結果</legend>
<span style="color:<?=$msg[0]?>;"><?=$msg[1]?></span>
</fieldset>
<?php endif; ?>
<form enctype="multipart/form-data" method="post" action="">
<fieldset>
<legend>画像ファイルを選択</legend>
<input type="file" name="upfile" /><br />
<input type="submit" value="送信" />
</fieldset>
</form>
</body>
</html>
3. 複数の画像ファイルを受け取ってそれぞれリサイズして保存する
<input type="file" name="upfile[]">
<input type="file" name="upfile[]">
のように受け取った時、例えば各項目の error
は
$_FILES['upfile'][0]['error']
$_FILES['upfile'][1]['error']
になると思われがちだが、実際には
$_FILES['upfile']['error'][0]
$_FILES['upfile']['error'][1]
となることに要注意。なお、ここでは実際にサイズ取得が必要なので getimagesize 関数を利用することにする。
<?php
if (isset($_FILES['upfile']['error']) && is_array($_FILES['upfile']['error'])) {
// 各ファイルをチェック
foreach ($_FILES['upfile']['error'] as $k => $error) {
try {
// 更に配列がネストしていれば不正とする
if (!is_int($error)) {
throw new RuntimeException("[{$k}] パラメータが不正です");
}
// $_FILES['upfile']['error'][$k] の値を確認
switch ($error) {
case UPLOAD_ERR_OK: // OK
break;
case UPLOAD_ERR_NO_FILE: // ファイル未選択
continue 2;
case UPLOAD_ERR_INI_SIZE: // php.ini定義の最大サイズ超過
case UPLOAD_ERR_FORM_SIZE: // フォーム定義の最大サイズ超過
throw new RuntimeException("[{$k}] ファイルサイズが大きすぎます");
default:
throw new RuntimeException("[{$k}] その他のエラーが発生しました");
}
// $_FILES['upfile']['mime']の値はブラウザ側で偽装可能なので
// MIMEタイプを自前でチェックする
if (!$info = @getimagesize($_FILES['upfile']['tmp_name'][$k])) {
throw new RuntimeException("[{$k}] 有効な画像ファイルを指定してください");
}
if (!in_array($info[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
throw new RuntimeException("[{$k}] 未対応の画像形式です");
}
// 画像処理に使う関数名を決定する
$create = str_replace('/', 'createfrom', $info['mime']);
$output = str_replace('/', '', $info['mime']);
// 縦横比を維持したまま 120 * 120 以下に収まるサイズを求める
if ($info[0] >= $info[1]) {
$dst_w = 120;
$dst_h = ceil(120 * $info[1] / max($info[0], 1));
} else {
$dst_w = ceil(120 * $info[0] / max($info[1], 1));
$dst_h = 120;
}
// 元画像リソースを生成する
if (!$src = @$create($_FILES['upfile']['tmp_name'][$k])) {
throw new RuntimeException("[{$k}] 画像リソースの生成に失敗しました");
}
// リサンプリング先画像リソースを生成する
$dst = imagecreatetruecolor($dst_w, $dst_h);
// getimagesize関数で得られた情報も利用してリサンプリングを行う
imagecopyresampled($dst, $src, 0, 0, 0, 0, $dst_w, $dst_h, $info[0], $info[1]);
// ファイルデータからSHA-1ハッシュを取ってファイル名を決定し、保存する
if (!$output(
$dst,
sprintf('./resized/%s%s',
sha1_file($_FILES['upfile']['tmp_name'][$k]),
image_type_to_extension($info[2])
)
)) {
throw new RuntimeException("[{$k}] ファイル保存時にエラーが発生しました");
}
$msgs[] = ['green', "[{$k}] リサイズして保存しました"];
} catch (RuntimeException $e) {
$msgs[] = ['red', $e->getMessage()];
}
// リソースを解放
if (isset($msg) && is_resource($img)) {
imagedestroy($img);
}
if (isset($dst) && is_resource($dst)) {
imagedestroy($dst);
}
}
}
// XHTMLとしてブラウザに認識させる
// (IE8以下はサポート対象外w)
header('Content-Type: application/xhtml+xml; charset=utf-8');
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>画像アップロード</title>
</head>
<body>
<?php if (!empty($msgs)): ?>
<ul>
<?php foreach ($msgs as $msg): ?>
<li style="color:<?=$msg[0]?>;"><?=
htmlspecialchars($msg[1], ENT_QUOTES, 'UTF-8')
?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<form enctype="multipart/form-data" method="post" action="">
<fieldset>
<legend>画像ファイルを選択(GIF, JPEG, PNGのみ対応)</legend>
<ul>
<?php for ($i = 0; $i < 10; $i++): ?>
<li><input type="file" name="upfile[]" /></li>
<?php endfor; ?>
</ul>
<input type="submit" value="送信" />
</fieldset>
</form>
</body>
</html>