WordPressの安全なファイルアップロードをAJAXで実現

2016/08/23

Firdaus Zahari

24

Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

WordPressで写真などの画像を投稿できるサイトを作るときに必要なのが、アップローダーですよね。コアの機能を上手に利用することで、セキュリティにも考慮しながら使いやすい機能を自作する方法を解説します。

Frontend submission form (Logged In)

プラグインでのアップロード機能の実現は、いつも一筋縄ではいきません。アップロードにおける優れたユーザー体験(UX)の実現が必要な一方、セキュリティ問題からも目が離せません。セキュリティ対策が適切でないと、サイトをセキュリティ上のさまざまな脆弱性発生の潜在的な危険にさらすことになりかねません。

ソリューション全体をスクラッチで構築せず、WordPressのコアコード、特にasync-upload.phpファイルをwp-adminディレクトリに置いて活用すれば、開発をスピードアップできるでしょう。

async-upload.phpファイルの使用にはいくつかのメリットがあります。このファイルはWordPressコア自体によってメディアライブラリーでの非同期アップロード用に使われるので、コードを確実に標準化できます。次に、バリデーションと権限のチェックをすべてしてくれるので、自分でする必要がなくなります。

必要条件

async-upload.phpファイルを使用するには、従うべき規則がいくつかあります。以下にそれぞれの規則を説明します。

  • 使用されるfileインプット(ファイル入力欄)はasync-uploadに設定されたname属性を有していなければならない

ひとたびasync-upload.phpファイル内部でバリデーションが通ると、次いでwp_ajax_upload_attachmentが、第1引数としてasync-uploadを使用するmedia_handle_upload関数を呼び出すためです。ほかの値の使用は無効です。

  • AJAXリクエストと共に送信されるノンスは、wp_create_nonce('media-form')関数で生成された値とともにデフォルトの_wpnonceキーを使用しなければならない

wp_ajax_upload_attachment関数内部で発生するcheck_ajax_referer形式でのバリデーションのためです。

  • AJAXリクエストを介して送信されるデータも、upload-attachmentの値とともにactionキーを有する必要がある

async-upload.phpファイル内部で検証され、値が正しく設定された場合のみwp_ajax_upload_attachment関数をトリガーします。

プラグインについて

プラグインにAJAXファイルアップロードのカスタム機能を構築するアイデアをよりよく説明するため、簡単なプラグインを作成して実際に試します。

この記事では、登録済みのユーザーがちょっとしたコンテストに画像を投稿できるプラグインを作成します。フロントエンドの投稿フォーム、つまりユーザーが直接画像をアップロードできる特定のページに表示されるアップロードフォームです。まさにAJAXアップロード機能の実装にぴったりです。

記事の長さの関係で話をシンプルにしておくため、このプラグインで実現することとしないことを決めておきます。

このプラグインでできることは次のとおりです。

  • ショートコードでの任意のページへのフォームの追加を管理者に許可する
  • AJAXアップロード機能を備えた投稿フォームをユーザーに表示する
  • 投稿後ただちにサイト管理者に通知するEメールを送信する

この記事の範囲では、プラグインに次の機能は付けません。

  • 投稿内容をデータベースに格納する
  • バックエンドで投稿内容を一覧表示する
  • 匿名のユーザーにファイルのアップロードを許可する

プラグインのブートストラッピング

wp-content/pluginsフォルダーに移動して、すべてのプラグインコードを置く新規フォルダーを作成します。ここから先、記事ではこのフォルダー名をsitepoint-uploadとし、すべての関数、フック、コールバックにsu_プリフィックスを付けます。

次に、メインのプラグインファイルを作成し、分かりやすくするためにフォルダーと同じ名前にします。プラグインフォルダー内にjsフォルダーも作成し、とりあえず空のscript.jsファイルを入れておきます。

ここまでで、プラグインのディレクトリ構造は次のようになります。

wp-content/
|-- plugins/
    |-- sitepoint-upload/
        |-- js/
        |   |-- script.js
        |--sitepoint-upload.php

プラグインのメインファイルsitepoint-upload.phpに簡単なプラグインヘッダーを付け、その後プラグインのページに進んで有効化します。以下が作成例です。

<?php
/*
Plugin Name: Simple Uploader
Plugin URI: http://sitepoint.com
Description: Simple plugin to demonstrate AJAX upload with WordPress
Version: 0.1.0
Author: Firdaus Zahari
Author URI: http://www.sitepoint.com/author/fzahari/
*/

スクリプトのエンキュー

ここで空のscript.jsファイルをフロントエンドにエンキューできます。これをAJAXアップロード機能の処理にも、投稿フォームの拡張にも使います。

function su_load_scripts() {
    wp_enqueue_script('image-form-js', plugin_dir_url( __FILE__ ) . 'js/script.js', array('jquery'), '0.1.0', true);
}
add_action('wp_enqueue_scripts', 'su_load_scripts');

wp_localize_script関数を使って、script.js内部で使用されるデータもローカライズします。必要事項が3つあります。1つ目はadmin-ajax.phpファイルへの正確なURL(AJAX経由でもフォームに投稿するため)、2つ目はasync-upload.phpファイルへの正確なURL、3つ目はwp_create_nonce関数を使って生成するノンスのローカライズです。

ここまで説明してきたwp_enqueue_scriptsフック用のコールバック関数は次のようになります。

function su_load_scripts() {
    wp_enqueue_script('image-form-js', plugin_dir_url( __FILE__ ) . 'js/script.js', array('jquery'), '0.1.0', true);

    $data = array(
                'upload_url' => admin_url('async-upload.php'),
                'ajax_url'   => admin_url('admin-ajax.php'),
                'nonce'      => wp_create_nonce('media-form')
            );

    wp_localize_script( 'image-form-js', 'su_config', $data );
}
add_action('wp_enqueue_scripts', 'su_load_scripts');

投稿フォーム用のショートコードを登録する

次に、同じことを繰り返しマークアップしなくても希望のページに登録フォームを簡単に追加できるよう、投稿フォーム用のショートコードを登録する必要があります。フォームに実装する内容は次のとおりです。

  • ユーザー名用のテキスト入力フィールド
  • ユーザーのEメールアドレス用の別のEメール入力フィールド
  • AJAXアップロード用のファイル入力欄「async-upload
  • Eメールのプレビュー、エラーメッセージ、その他のアイテム用の多数のプレースホルダーdiv

また、ユーザーが現在ログインしていない場合は投稿フォームを完全に無効にし、代わりにログインリンクを表示します。

function su_image_form_html(){
    ob_start();
    ?>
        <?php if ( is_user_logged_in() ): ?>
            <p class="form-notice"></p>
            <form action="" method="post" class="image-form">
                <?php wp_nonce_field('image-submission'); ?>
                <p><input type="text" name="user_name" placeholder="Your Name" required></p>
                <p><input type="email" name="user_email" placeholder="Your Email Address" required></p>
                <p class="image-notice"></p>
                <p><input type="file" name="async-upload" class="image-file" accept="image/*" required></p>
                <input type="hidden" name="image_id">
                <input type="hidden" name="action" value="image_submission">
                <div class="image-preview"></div>
                <hr>
                <p><input type="submit" value="Submit"></p>
            </form>
        <?php else: ?>
            <p>Please <a href="<?php echo esc_url( wp_login_url( get_permalink() ) ); ?>">login</a> first to submit your image.</p>
        <?php endif; ?>
    <?php
    $output = ob_get_clean();
    return $output;
}
add_shortcode('image_form', 'su_image_form_html');

上記のショートコードのコールバック関数について説明します。

  • 登録するショートコードはimage_form
  • ショートコードのコールバック関数内部でexposeする部分をよりフレキシブルにするため、出力バッファリングを使用
  • ファイル入力欄での画像用のファイルの選択をaccept属性経由のみに制限する。ちなみにこれは実質上のファイルバリデーションに代わるものではない(詳しくはこちら
  • ログインURLに関して、ログイン成功後ただちにユーザーが投稿ページにリダイレクトできるように、wp_login_urlへの現在のページのパーマリンクを供給

特定のユーザー権限にupload_files機能を追加する

プラグインを確実に機能させるために、subscriber(購読者)権限の機能を変更する必要があります。なぜならsubscriber権限のユーザーは、デフォルトでファイルのアップロード機能を持たないからです。

function su_allow_subscriber_to_uploads() {
    $subscriber = get_role('subscriber');

    if ( ! $subscriber->has_cap('upload_files') ) {
        $subscriber->add_cap('upload_files');
    }
}
add_action('admin_init', 'su_allow_subscriber_to_uploads');

ちなみに、subscriber権限はその時点でupload_files機能を持っていない場合のみ変更されます。

これでプラグインの基礎部分ができました。これから、投稿フォームを表示する新規ページを作成します。

Submit Your Image WordPress

ここではtwentysixteenテーマを有効にしたデフォルトのWordPress実装において、フロントエンドでフォームがどのように見えるかを示しています。

Frontend submission form (Logged In)

サイトからログアウトすると、代わりに通知が表示されます。

Frontend submission form (Logged out)

プラグインが、いい感じになってきましたね!

AJAXアップロードを実装する

プラグインの基礎部分が正しく構成されたところで、必要なコア機能、AJAXアップロードに集中できます。

進めるにあたって、フォルダー内にあるscript.jsファイルを開き、最初にすべてのコードを即時呼び出し関数式(immediately-invoked function expression:IIFE)内にラップします。

次に、コードをスピードアップするためにいくつかのセレクターをキャッシュします。画像プレビューのdiv、入力されたファイル、アップロード通知の表示に使用されるdivへの参照も含まれます。

(function($) {
    $(document).ready(function() {
        var $formNotice = $('.form-notice');
        var $imgForm    = $('.image-form');
        var $imgNotice  = $imgForm.find('.image-notice');
        var $imgPreview = $imgForm.find('.image-preview');
        var $imgFile    = $imgForm.find('.image-file');
        var $imgId      = $imgForm.find('[name="image_id"]');
    });
})(jQuery);

キャッシュされたセレクターは、あとで役立ちます。先に述べたとおり、aync-upload.phpファイルでのバリデーションが通るために従うべきルールがいくつかあります。指定された正しいキーまたは値のペアで、AJAXを介してaync-upload.phpファイルへのPOSTリクエストの作成は、FormData APIを使って実行できます。

最初にファイル入力欄にchangeイベントをフックし、入力が変更される場合にかぎり、アップロードをトリガーします。

$imgFile.on('change', function(e) {
    e.preventDefault();

    var formData = new FormData();

    formData.append('action', 'upload-attachment');
    formData.append('async-upload', $imgFile[0].files[0]);
    formData.append('name', $imgFile[0].files[0].name);
    formData.append('_wpnonce', su_config.nonce);

    $.ajax({
        url: su_config.upload_url,
        data: formData,
        processData: false,
        contentType: false,
        dataType: 'json',
        type: 'POST',
        success: function(resp) {
            console.log(resp);
        }
    });
});

コードを上記のようにしてアップロード機能をテストし、うまくいくことを確認します。開発者コンソール(使用しているブラウザに応じた)を使って、出力用のコンソールタブをチェックしてください。アップロード成功後のaync-upload.phpファイルからのレスポンスのサンプルは次のようになります。

upload php

wp-content/uploadsディレクトリに移動してファイルがあるかどうかもチェックできます。アップロード機能がきちんと動作していることを確かめたら、アップロードのスクリプトに少しばかり改良を加えます。考えられるいくつかの改良点は次のとおりです。

  • アップロード処理中にプログレスバーまたはテキストを表示する
  • アップロード成功時にアップロードされた画像のプレビューを表示する
  • アップロードが失敗した場合、エラーを表示する
  • ユーザーが新規画像をアップロードして現在の画像と差し替える方法を提供する

これらの実装方法を1つずつ説明します。

アップロード処理中にプログレスバーまたはテキストを表示する

これはとても簡単です。jQuery AJAXのbeforeSend用にコールバックを定義するだけで大丈夫です。AJAXアップロード用のコードのある場所に、次のコードブロックを置きます。

beforeSend: function() {
    $imgFile.hide();
    $imgNotice.html('Uploading&hellip;').show();
},

ユーザーに進捗状況をテキスト表示するようにあらかじめ定義されたimage-noticeクラスで空のdivを使用します。また、アップロード処理中はファイル入力欄を隠します。

対応しているブラウザーでは、オリジナルのjQuery「xhr」オブジェクトを自分でオーバーライドして、アップロードのパーセンテージも表示できます。$.ajax構成にこれを追加すると次のようになります。

xhr: function() {
    var myXhr = $.ajaxSettings.xhr();

    if ( myXhr.upload ) {
        myXhr.upload.addEventListener( 'progress', function(e) {
            if ( e.lengthComputable ) {
                var perc = ( e.loaded / e.total ) * 100;
                perc = perc.toFixed(2);
                $imgNotice.html('Uploading&hellip;(' + perc + '%)');
            }
        }, false );
    }

    return myXhr;
}

対応しているブラウザーでは、コードのUploadingテキストのあとにアップロードのパーセンテージを簡単に追加できます。なかなか素敵な拡張ですね。対応していないブラウザーでは、この優れたグレースフル・デグラデーションは実装できません。

アップロード成功時にアップロードされた画像のプレビューを表示し、失敗時にはエラーを表示する

async-upload.phpスクリプトからのレスポンスによって、ユーザーに別々のメッセージを表示します。successキーがtrueに設定された場合、アップロードされた画像をユーザーに表示し、ファイル入力欄を隠せます。アップロードが失敗した場合、div内のテキストを前述のimage-noticeに置きかえます。

success: function(resp) {
    if ( resp.success ) {
        $imgNotice.html('Successfully uploaded.');

        var img = $('<img>', {
            src: resp.data.url
        });

        $imgId.val( resp.data.id );
        $imgPreview.html( img ).show();

    } else {
        $imgNotice.html('Fail to upload image. Please try again.');
        $imgFile.show();
        $imgId.val('');
    }
}

$imgIdは、アップロードされた画像のIDの参照に使用する隠しデータです。この値はあとでフォーム投稿に使用しますので、いまは気にしなくて大丈夫です。

ユーザーが新規画像をアップロードして現在の画像と差し替える方法を実現する

ユーザーが現在アップロードされている画像を新規画像と差し替えるための方法として、リンクを供給します。アップロード成功時に表示される通知について、下のコードを、

$imgNotice.html('Successfully uploaded.');

次のコードに変更します。

$imgNotice.html('Successfully uploaded. <a href="#" class="btn-change-image">Change?</a>');

btn-change-imageクラスへのアンカーを設定していますが、メリットがあります。アンカーにイベントリスナーを追加して、クリックされたときに現在の画像のプレビューを消去できるからです。また、通知メッセージを隠し、リセット済みの値でファイル入力欄を再び表示します。

$imgForm.on( 'click', '.btn-change-image', function(e) {
    e.preventDefault();
    $imgNotice.empty().hide();
    $imgFile.val('').show();
    $imgId.val('');
    $imgPreview.empty().hide();
});

さらにchangeイベントを再びトリガーできるように、クリックされたときにfile inputの値をリセットする必要があります。

$imgFile.on('click', function() {
    $(this).val('');
    $imgId.val('');
});

次のセクションに進む前に、もう一度アップロード機能全体を実行して、すべてが目的どおりに動くか確認してください。

プラグインの完成

AJAX経由でフォーム投稿を処理するにあたり、フォームのsubmitイベントにイベントリスナーをバインドします。

$imgForm.on('submit', function(e) {
    e.preventDefault();

    var data = $(this).serialize();

    $.post( su_config.ajax_url, data, function(resp) {
        if ( resp.success ) {
            $formNotice.css('color', 'green');
            $imgForm[0].reset();
            $imgNotice.empty().hide();
            $imgPreview.empty().hide();
            $imgId.val('');
            $imgFile.val('').show();
        } else {
            $formNotice.css('color', 'red');
        }

        $formNotice.html( resp.data.msg );
    });
});

上記のコードをベースに、WordPress AJAXアクションのビルトインを使ってバックエンドで投稿を処理します。投稿が成功したら、ただちにフォームをリセットし、画像のプレビューを消去してフォームの通知メッセージを緑色に設定します。

投稿が失敗した場合は、フォームの通知メッセージを赤に設定するだけです。こうしておけば、ユーザーは再試行の前にデータから画像を再確認できます。

ここで再びプラグインのメインファイルを開いてAJAXコールバックに追加します。image_submissionaction値を設定しているので、wp_ajax_image_submissionアクションに有効なコールバックを追加することが必要です。

add_action('wp_ajax_image_submission', 'su_image_submission_cb');

コールバック関数において、最初にしておくべきことがいくつかあります。有効なAJAXノンスのチェックと、ユーザー入力の検証も必要です。この記事の範囲では、新規投稿があったことをサイト管理者にEメールで通知するのみとします。

AJAXコールバック関数のコード全体を示します。

function su_image_submission_cb() {
    check_ajax_referer('image-submission');

    $user_name  = filter_var( $_POST['user_name'],FILTER_SANITIZE_STRING );
    $user_email = filter_var( $_POST['user_email'], FILTER_VALIDATE_EMAIL );
    $image_id   = filter_var( $_POST['image_id'], FILTER_VALIDATE_INT );

    if ( ! ( $user_name && $user_email && $image_id ) ) {
        wp_send_json_error( array('msg' => 'Validation failed. Please try again later.') );
    }

    $to      = get_option('admin_email');
    $subject = 'New image submission!';
    $message = sprintf(
                    'New image submission from %s (%s). Link: %s',
                    $user_name,
                    $user_email,
                    wp_get_attachment_url( $image_id )
                );

    $result = wp_mail( $to, $subject, $message );

    if ( $result ) {
        wp_send_json_error( array('msg' => 'Email failed to send. Please try again later.') );
    } else {
        wp_send_json_success( array('msg' => 'Your submission successfully sent.') );
    }
}

今回の目的は、シンプルなcheck_ajax_refererでのチェックとネイティブなPHP関数filter_varでユースケースに事足ります。wp_send_json_errorwp_send_json_success関数を使ってレスポンスを返すこともできます。

これでプラグインが完成し、すべての機能が使えるようになります。検証のため、フォームをきちんと完成して、アップロードされた画像へのリンクを記載したEメールが受信されるか確認してください。

さらなる改良

記事では、内部async-upload.phpファイルを介したAJAXアップロード方法を示すことに焦点を当ててきたため、実際ところどころで説明を端折ってきました。参考までに、プラグイン全体を拡張できるシンプルな点をいくつか紹介します。

  • 投稿に関する付加的な値をキャプチャするフィールドをさらに追加する
  • 通知とアップロード進捗のフォームをより良いスタイルにするため、分離したCSSファイルをエンキューする
  • 投稿されたデータを再確認できるように、データベースに保存する
  • セキュリティ強化のため、アップロード処理のバリデーションをさらに追加する

完成したプラグインのソースコードをGitHubから利用できます。

最後に

結論としては、目のつけどころが分かっていると、プラグインでのAJAXアップロードの実装をスピードアップできるということです。async-upload.phpファイルを使うことで、機能を実装する開発時間を削減できます。また、このファイルはWordPressコアによって管理画面のダッシュボードでのユーザーアップロードの処理に使われているため、信頼性も得られます。

(原文:Enabling AJAX File Uploads in Your WordPress Plugin

[翻訳:新岡祐佳子]
[編集:Livit

Copyright © 2016, Firdaus Zahari All Rights Reserved.

Firdaus Zahari

Firdaus Zahari

遠くマレーシア出身のWeb開発者です。主にWordPressとフロントエンド開発(これに限られてはいないが)に情熱を注いでいます。

Loading...