前置き

ファイルのアップロードをajaxでできるようにするのが目的です。
今回は、ユーザーがファイルをアップロードし、特定のディレクトリの配下に画像を保存するディレクトリを作成してファイルをアップロードする方法です。

ファイルアップロードの条件

  • 使用できるファイルタイプ画像ファイルのみ(jpg,png,gif,bmp)とする。
  • .php,.py等のプログラムファイルはアップロードさせないため。
    .php,.py等のプログラムファイルをアップロードさせる場合はセキュリティを十分に考量した上で処理を作成してください。
    今回はプログラムファイル等のアップロードは対象外とします。

  • アップロードしたファイルの名前はプログラムで生成したものを使用する。
  • アップロードしたファイル名をそのまま使用した場合、ファイル名に「../」等の文字列が含まれていると予期せぬ動作や想定外の場所にファイルをアップロードできてしまう可能性があるため。

  • アップロード可能なファイルサイズを2MBとする。
  • これに関しては特に制限する必要がなければ無視してもらっても構わないが、phpの場合デフォルトでアップロードできる容量が制限されているため規定値以上のファイルはアップロードできない。
    規定値を変更する方法は後述します。

環境

サーバーサイドの言語は「php」を使用していて、バージョンは「5.6」になります。

ソースコード

まずはhtml

<form action="" method="post">
    <input type="file" name="hoge">
    <!-- ※1 -->
    <!-- <input type="submit" name="upload" value="アップロード"> -->
</form>
※1 ファイルが選択した時に自動でアップロードさせるためsubmitボタンはコメントアウトする。
もし、アップロードボタンを押下したときにajaxでアップロードさせる場合はコメントを解除する。

次にjs

// ※2
$(document).on('change','input[name="hoge"]',function(){
    // ※3
    var fd = new FormData();
    if ($("input[name='hoge']").val()!== '') {
        fd.append( "file", $("input[name='hoge']").prop("files")[0] );
    }
    var postData = {
        type : "POST",
        dataType : "text",
        data : fd,
        processData : false,
        contentType : false
    };
    $.ajax(
        "ajax/index.php", postData
    ).done(function( text ){
        console.log(text);
    });
});
※2 アップロードボタンを押下したときにajaxでアップロードさせる場合は、「change」の部分を「click」に変更する。
※3 対応していないブラウザがある。
  対応していない場合はiframe等で処理を作成する必要がある。

ブラウザが「FormData」に対応しているの確認

if( window.FormData ){
    // 対応している
} else {
    // 対応していない
}

最後にphp

$post_name = filter_input(INPUT_POST, "file");

try{
    if (!isset($post_name) || empty($post_name)){
    	throw new RuntimeException('ファイルが選択されていません');
    }
	if($_FILES[$post_name]["size"] > 2097152){
		throw new RuntimeException('ファイルサイズが大きすぎます');
	}
	// アップロードされたファイルのエラー値を確認
	switch ($_FILES[$post_name]['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('その他のエラーが発生しました');
	}
	
	if(isset($_FILES[$post_name]['tmp_name'])){
		$file_dir = date("Ym");
		$file_type = explode(".",$_FILES[$post_name]['name']);
		$file_type = end($file_type);
		$type = @exif_imagetype($_FILES[$post_name]['tmp_name']);
		if (!in_array($type, [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG,IMAGETYPE_BMP], true)) {
			throw new RuntimeException('ファイル形式が対応していません(対応ファイル形式:jpg,png,gif,bmp)');
		}
		if (!in_array(strtolower($file_type), array("jpg","jpeg","png","gif","bmp"), true)) {
			throw new RuntimeException('ファイル形式が対応していません(対応ファイル形式:jpg,png,gif,bmp)');
		}
		$name = date("YmdHis") . "." . $file_type;
		$file = 'img/'.$file_dir;
		if(!file_exists($file)){
			mkdir($file,0755);
		}
		if (move_uploaded_file($_FILES[$post_name]['tmp_name'], $file."/".$name)) {
			chmod($file."/".$name, 0644);
		}else{
			throw new RuntimeException('ファイルをアップロードできませんでした。もう一度やり直してください。');
		}
	}else{
		throw new RuntimeException('ファイルをアップロードできませんでした。もう一度やり直してください。');
	}
}catch(RuntimeException $e){
	echo $e->getMessage();
}

まとめ

需要があるかどうかはわかりませんが実際に書いて動かしてみると自分でも勉強になることあると思います。
ただ、ソースコードの中に脆弱性がある場合があるのでそのまま使用する場合はセキュリティ面のチェックを行ってください。
また、別の機会にでもiframeを使用した非同期のファイルアップロードの記事を書きたいと思います。