WordPressでファイルアップロードをAjaxでやる方法

会員制サイトとかで、アイコンとかを管理画面からではなく、固定ページなどからアップロードしたいことがあるかと思います。

Ajaxを使ってやる方法は結構出ているのですが、独自関数みたいなのを色々使ったりして結果複雑化していたりします。

でも実はWordPressには非同期でファイルアップロードをするための方法もちゃんと準備されていて結構簡単に実装できたのでノウハウの共有です。

WordPressのコアにajaxからファイルをアップロードするための機能はある

async-upload.php というファイルが wp-admin に存在します。このファイルにAjaxからファイルをPOSTすることで、バリデーションチェックや権限チェックをしてくれるので、そのあたりのことを何も考える必要がありません。

どうやら、管理画面でファイルアップロードする際に使用されているっぽいです。

管理画面で使用されているからこその制約

そのため、いくつか制約があります。

  • Ajaxで送られるデータに action キーが必要で、その値は upload-attachment である必要がある。
  • アップロードするユーザが upload_files の権限を持つ必要がある。
  • fileのインプットタグに付けるネーム属性は async-upload である必要がある。
  • Ajaxリクエスト時に nonce を送信する必要があり、そのアクション名は media-form である必要がある。
    • nonce のキーは _wpnonce である必要がある。

以上が私が確認している制約です。
私が作成した際に偶然通過した制約もあるかもしれませんが。。。

実際に作ってみる

今回使用するフォーム

<form action="#" class="image-form">
	<input type="file" name="file" class="file">
</form>

とりあえず今回はシンプルにこんな形。

フォームタグの中に input タグが一つあるだけ

wp_localize_script() を使って Ajax 送信するデータのローカライズ

上記制約の中に nonce キーを作成する必要があるので、PHP側から情報を受け渡す必要があります。その他送信先の情報など必要なものがありますので、ローカライズして出力しておく必要があります。

以下に実例を示します。

function theme_enqueue_scripts() {
	wp_enqueue_script( 'upload_image', get_theme_file_uri( 'upload-image.js' ), array( 'jquery' ), filemtime( get_theme_file_path( 'upload-image.js' ) ) );
	$data = array(
		'upload_url' => admin_url( 'async-upload.php' ),
		'nonce'      => wp_create_nonce( 'media-form' ),
	);

	wp_localize_script( 'handle_name', 'data_name', $data );
}
add_action( 'wp_enqueue_scripts', 'theme_enqueue_scripts' );

これらの要素を、作成しているテーマの functions.php や プラグインファイルに記述してください

それぞれの詳細は以下のとおりです。

$data

受け渡すデータ配列 (変数名は何でも良いですが、変更した場合 8 行目にある wp_localize_script() の第 3 引数も忘れず変更してください。)

upload_url

アップロード処理するファイル ( async-upload.php ) の正確なパス( url )
admin_url() を使用することで、正確なパスを返してくれます。

nonce

アップロード時に送信する nonce の値。 先述の通りアクション名は media-form である必要があります。

wp_localize_script( $handle, $name, $data )

ローカライズスクリプトを出力するための関数です。
日本語Codexは未翻訳のため簡単な説明を書いておきます (そのうち翻訳しようね)

$handle

例では 'handle_name' としています。スクリプトを登録するハンドル名で一意である必要があります。

$name

例では 'data_name' としています。登録されたスクリプトを示す名前で、こちらも一意である必要があります。

$data

例では先に説明した $data を設定しています。ローカライズスクリプトに設定する値が入っています。

Ajax送信部分の作成

次に Ajax で実際に送信するスクリプトを書きます。

( function ( $ ) {
	$( function () {
		let image_form = $( '.image-form' );
		let image_file = image_form.find( '.file' );

		image_file.on( 'change', function ( e ) {
			// 通常動作を止める
			e.preventDefault();

			let data = new FormData;

			data.append( 'action', 'upload-attachment' );
			data.append( 'async-upload', image_file[0].files[0] );
			data.append( 'name', image_file[0].files[0].name );
			data.append( '_wpnonce', data_name.nonce );

			$.ajax( {
				url         : data_name.upload_url,
				data        : data,
				processData : false,
				contentType : false,
				dataType    : 'json',
				type        : 'POST',
			} )
				.then(
					function ( data ) {
						console.log( 'complete!' );
						console.log( data );
					},
					function ( jqXHR, textStatus, errorThrown ) {
						console.log( 'error!' );
						console.log( 'jqXHR' );
						console.log( jqXHR );
						console.log( 'textStatus' );
						console.log( textStatus );
						console.log( 'errorThrown' );
						console.log( errorThrown );
					}
				);
		} );
	} );
} )(jQuery);

最初の変数2つはフォームの情報を取得しています。

そして、input が変更されたタイミングでイベントを発火させるように書きます。
ただし、Ajax でデータを送信しますので、input 要素の通常動作はしないように最初に処理を止めます。( e.preventDefault() )

送信準備

送信準備として 変数 data を作成しています。 formData クラスを使用して作成し、 append() メソッドで要素を追加していきます。

追加する要素は以下のとおりです。

action

前述の通り、送信されるときに action キーが必要で値は upload-attachment である必要があります。

async-upload

こちらも前述のとおりです。送信されるファイルは async-upload というキーで渡す必要があります。

name

ファイル名を指定します。キーはこの値で渡す必要があります。

_wpnonce

これも前述の通り。 _wpnonce というキー名で nonce の値を送る必要があります。

送信処理

やっと Ajax で実際に送信処理を行います。

url には、先にローカライズしていた upload_url の値を使用します。 wp_localize_script() 関数の第 2 引数 $name の名前のオブジェクトに入っていますので設定します。

data には、送信準備で用意していたものを使用します。

type はメソッド名を指定し POST を使用します。

processDatacontentTypefalse に指定します。送信時のデータ形式を示すものです。今回は画像ファイルを送るので false です。このあたりはちょっと別の話になってくるのでこんなものだと思ってください。

送信後の処理

送信後の処理はメソッドチェーンの then() で受け取っています。第 1 引数が成功時、第 2 引数が失敗時の処理です。
この部分についてはどの様に書きたいかはお好みで。多分 success とか error で書いても動く (モダンでは無いけど)。

成功時には、登録された画像情報が返されます。

返される情報は wp_prepare_attachment_for_js() 関数の返り値と success : true です。

この情報の中にアップロードされた URL なども含まれているので、サムネイルを表示したり、 attachment_id を先のページに渡して別の処理を PHP で行うということも可能です。(例えば投稿に紐付けるとか)

まとめ

既存の機能を使うことで比較的簡単に処理を作ることができるかと思います。
私も最初は、 jQuery-File-Upload を使用して作成しようと思っていたのですが、思いの外 WordPress に紐付ける処理が複雑になり、どこでエラーが発生するかわかったもんじゃなかったので、切り替えました。
結果的には簡単に行ったかなという感じです。

実際現在やっている案件では、固定ページで利用者が投稿を作成する際に画像もアップロード可能という仕様のため、この後に投稿に紐付ける処理とかが必要になります。

また、そもそもアップロードディレクトリをユーザごとに変更する必要があるため、それ用の処理も別途準備していたりするので、気が向いた時にまたメモを残したいと思います。
こちらは upload_dir フィルターフックに処理を仕掛けるのですが、送信元がすべて async-upload.php になってしまっているが注意点です。ヒントとしては $_FILES['async-upload'] に情報が入っているということと $_SERVER[ 'HTTP_REFERER' ] が元の固定ページであるということだけを残しておきます。

フィードバックください

とりあえず、メモなのかノウハウの共有なのか迷走しながら書いているのでぜひフィードバックください。わからないことがあったら答えられる範囲で答えるのでどうぞ。

フィードバックいただきました。

実際に使っていたJSを元にブログ用に書き直したのですが、Ajaxのオプションに設定しているオブジェクトから、 contentType が欠落していました。

また、そのオブジェクトの設定時に何故かプロパティと値を = でつないでいました。

その他、sample.php のインプットにクラスを設定し忘れてたり、 file-upload.js の読み込み忘れてたりといろいろ欠落していました。

すべて修正済みです。

ご連絡いただいた 立花さん (@atachibana) ありがとうございました!

そしてそして、

ブログまで書いていただきましたー!私がざっくりとしか書いてない部分をフォローしてくださったり、カスタムブロック化までされていますので、ぜひご一読ください!

この記事を書いた人

Shizumi

熊本在住のWebプログラマ。熊本WordPressMeetupのオーガナイザー。WordPressを使い始めたのは2012年頃から。