- admin-ajax と REST API の使い分け
- Ajax(admin-ajax.php)の実装パターン
- REST API の実装パターン(register_rest_route)
- セキュリティの「型」|nonce・権限・サニタイズ
- 外部API通信|HTTP API(wp_remote_*)
- キャッシュ|Transients API
- Cron|重い処理の非同期化
- デバッグとログ|非同期処理のトラブルシューティング
- 拡張性設計|自前フック(apply_filters / do_action)
- コンテキスト検出|wp_doing_ajax / wp_doing_cron / wp_get_environment_type
- REST APIヘルパー関数|rest_url / rest_ensure_response
- HTTP通信の完全版|PUT/DELETE対応とSSRF対策
- ユーティリティ関数|wp_json_encode / wp_parse_args / wp_unslash
- セキュリティユーティリティ|トークン生成・ハッシュ・ファイル操作
- まとめ:壊れない非同期機能の共通パターン
この記事の結論
WordPressの非同期通信はadmin-ajax.php(既存互換・小規模向け)とREST API(設計重視・拡張向け)の2方式があり、どちらを選んでも守るべきセキュリティ対策は同じです。「権限チェック → nonce検証 → サニタイズ → バリデーション → エスケープ」の5段階を型として身につけ、外部API通信にはTransientsキャッシュ、重い処理にはWP-Cronを組み合わせてください。
検証環境:WordPress 6.9 / Xserver / PHP 8.5.2(2026年3月時点)
モダンなWordPressプラグインでは、非同期通信(Ajax/REST API)が欠かせません。「いいね」ボタン、無限スクロール、管理画面での非同期保存、外部サービスとの連携——ユーザー体験を向上させる多くの機能が非同期通信で実現されています。
しかし、非同期機能が増えるほどセキュリティ設計がボトルネックになります。外部からリクエストを受け付ける機能は、攻撃者にとって格好のターゲットだからです。
この記事では、admin-ajax.php方式とREST API方式の両方について、「どちらを選ぶべきか」から「安全に実装するパターン」まで、関数ベースで解説します。
この記事はWordPress関数解説シリーズのPart3(Ajax・REST API・セキュリティ編)です。テーマ制作で使う関数はPart1、プラグインの設定画面・メニュー・CPTはPart2で解説しています。
admin-ajax と REST API の使い分け
迷ったら「既存機能の保守ならadmin-ajax、新規開発ならREST API」で判断してください。どちらを選んでもセキュリティ対策(nonce・権限・サニタイズ)は同じで、違いは入口(エンドポイント)の設計だけです。
WordPressで非同期通信を実装する方法は、大きく分けて2つあります。
admin-ajax.php を選ぶケース
古くからの定番で実装がシンプルです。ログイン必須の管理系機能、既存プラグインやテーマとの互換性維持、小規模な機能でRESTの設計までは不要な場合に向いています。
REST API を選ぶケース
設計が明確でエンドポイントの構造が綺麗になります。フロントエンド(React、Vue等)との連携、外部アプリケーションとの連携、ブロックエディタ(Gutenberg)との統合、将来的な拡張性を重視する場合に選択します。
共通して守るべきこと
どちらの方式でも、セキュリティ対策は同じです。nonce検証(CSRF対策)、権限チェック、入力値のサニタイズ、出力時のエスケープ——この4つは方式に関係なく必須です。
Ajax(admin-ajax.php)の実装パターン
admin-ajax.phpは、wp_ajax_{action}でログインユーザー向け、wp_ajax_nopriv_{action}で未ログインユーザー向けのエンドポイントを登録します。必ずcheck_ajax_referer()でnonce検証し、wp_send_json_success()/wp_send_json_error()で統一されたレスポンスを返してください。
admin-ajax.phpは、WordPressに最初から用意されているAjax処理の仕組みです。フォームデータを/wp-admin/admin-ajax.phpにPOSTすることで、PHPの処理を非同期に呼び出せます。
wp_ajax_{action}|ログインユーザー向け
このアクションフックは、ログイン済みユーザーからのリクエストのみを処理します。管理画面での設定保存、ユーザー固有のデータ操作など、認証が必要な機能に使用します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// PHPでエンドポイントを登録 add_action( 'wp_ajax_myplugin_toggle_favorite', 'myplugin_toggle_favorite' ); function myplugin_toggle_favorite() { // nonce検証 check_ajax_referer( 'myplugin_nonce', 'nonce' ); // 権限チェック if ( ! current_user_can( 'read' ) ) { wp_send_json_error( array( 'message' => '権限がありません' ), 403 ); } // 処理を実行 $post_id = absint( $_POST['post_id'] ?? 0 ); $result = myplugin_do_toggle_favorite( $post_id, get_current_user_id() ); // 結果を返す if ( $result ) { wp_send_json_success( array( 'status' => 'favorited' ) ); } else { wp_send_json_error( array( 'message' => '処理に失敗しました' ), 500 ); } } |
wp_ajax_nopriv_{action}|未ログインユーザーも含む
noprivは「no privileges(権限なし)」の略で、未ログインユーザーからのリクエストも受け付けるエンドポイントです。公開フォームの送信処理、投票機能、問い合わせフォームなどに使用します。このエンドポイントは「公開API」に近い存在であり、入力チェック、レート制限、スパム対策がより重要になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// ログインユーザーと未ログインユーザーの両方に対応 add_action( 'wp_ajax_myplugin_submit_form', 'myplugin_submit_form' ); add_action( 'wp_ajax_nopriv_myplugin_submit_form', 'myplugin_submit_form' ); function myplugin_submit_form() { check_ajax_referer( 'myplugin_form_nonce', 'nonce' ); $name = sanitize_text_field( $_POST['name'] ?? '' ); $email = sanitize_email( $_POST['email'] ?? '' ); $message = sanitize_textarea_field( $_POST['message'] ?? '' ); if ( empty( $name ) || empty( $email ) || empty( $message ) ) { wp_send_json_error( array( 'message' => '必須項目を入力してください' ), 400 ); } // 処理を実行... wp_send_json_success( array( 'message' => '送信しました' ) ); } |
check_ajax_referer()|CSRF対策
CSRF(クロスサイトリクエストフォージェリ)攻撃を防ぐ関数です。第1引数はnonce生成時のアクション名、第2引数はリクエストパラメータ名です。検証に失敗するとデフォルトでwp_die()が実行されます。第3引数にfalseを渡すと、戻り値で判定できます。
|
1 2 3 4 5 6 7 8 |
// デフォルト:検証失敗時は処理が終了する check_ajax_referer( 'myplugin_nonce', 'nonce' ); // 戻り値で判定したい場合 $result = check_ajax_referer( 'myplugin_nonce', 'nonce', false ); if ( $result === false ) { wp_send_json_error( array( 'message' => 'セキュリティトークンが無効です' ), 403 ); } |
wp_send_json_success() / wp_send_json_error()|統一レスポンス
適切なHTTPヘッダーを設定し、JSON形式のレスポンスを出力してスクリプトの実行を終了します。手動でjson_encode()やheader()を書く必要がなく、統一されたレスポンス形式を提供できます。
|
1 2 3 4 5 6 7 |
// 成功時 wp_send_json_success( array( 'message' => '保存しました', 'id' => $new_id ) ); // 出力: {"success":true,"data":{"message":"保存しました","id":123}} // 失敗時(第2引数でHTTPステータスコードを指定可能) wp_send_json_error( array( 'message' => '入力値が不正です' ), 400 ); // 出力: {"success":false,"data":{"message":"入力値が不正です"}} |
wp_localize_script()|JavaScriptへPHPの値を渡す
JavaScript側でAjaxリクエストを送信するには、admin-ajax.phpのURLとnonceが必要です。これらの値はPHP側でしか生成できないため、wp_localize_script()でJavaScriptのグローバル変数として渡します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// PHP側 add_action( 'wp_enqueue_scripts', function() { wp_enqueue_script( 'myplugin-script', plugin_dir_url( __FILE__ ) . 'assets/js/app.js', array( 'jquery' ), '1.0.0', true ); wp_localize_script( 'myplugin-script', 'MyPluginSettings', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'myplugin_nonce' ), 'postId' => get_the_ID(), ) ); }); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// JavaScript側(app.js) jQuery(document).ready(function($) { $('.favorite-button').on('click', function() { $.ajax({ url: MyPluginSettings.ajaxUrl, type: 'POST', data: { action: 'myplugin_toggle_favorite', nonce: MyPluginSettings.nonce, post_id: MyPluginSettings.postId }, success: function(response) { if (response.success) { alert('完了しました'); } else { alert(response.data.message); } } }); }); }); |
REST API の実装パターン(register_rest_route)
REST APIではregister_rest_route()でエンドポイントを登録し、permission_callbackで権限チェックを必ず設定します。省略するとデバッグモードで警告が出るだけでなく、セキュリティホールの原因になります。
WordPress REST APIは、WordPress 4.7から標準搭載された機能です。/wp-json/以下にエンドポイントを定義し、HTTPメソッド(GET、POST、PUT、DELETE等)に応じた処理を行います。
register_rest_route()|エンドポイントの登録
第1引数の名前空間はプラグイン識別子とバージョンを組み合わせます(例:myplugin/v1)。第2引数がパス、第3引数でメソッド・コールバック・権限チェック等を指定します。
|
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 |
add_action( 'rest_api_init', function() { // GETエンドポイント register_rest_route( 'myplugin/v1', '/status', array( 'methods' => 'GET', 'callback' => function( WP_REST_Request $request ) { return array( 'ok' => true, 'time' => current_time( 'mysql' ) ); }, 'permission_callback' => '__return_true', ) ); // POSTエンドポイント(管理者限定) register_rest_route( 'myplugin/v1', '/settings', array( 'methods' => 'POST', 'callback' => 'myplugin_save_settings', 'permission_callback' => function() { return current_user_can( 'manage_options' ); }, 'args' => array( 'enabled' => array( 'required' => true, 'type' => 'boolean' ), 'limit' => array( 'required' => false, 'type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint', ), ), ) ); }); |
permission_callback|アクセス権限の制御(最重要)
permission_callbackは、REST APIのセキュリティにおいて最も重要な設定です。このコールバックがtrueを返した場合のみ、メインのコールバックが実行されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// ログインユーザー限定 'permission_callback' => function() { return is_user_logged_in(); } // 管理者限定 'permission_callback' => function() { return current_user_can( 'manage_options' ); } // 投稿編集権限を持つユーザー限定 'permission_callback' => function( WP_REST_Request $request ) { $post_id = $request->get_param( 'id' ); return current_user_can( 'edit_post', $post_id ); } // 誰でもアクセス可能(公開エンドポイント) 'permission_callback' => '__return_true' |
返り値|配列・WP_REST_Response・WP_Error
コールバック関数からは、配列(自動的にJSONに変換)、WP_REST_Response(HTTPステータスやヘッダーを制御)、WP_Error(エラーを返す)のいずれかを返します。
|
1 2 3 4 5 6 7 8 |
// 成功:配列を返す return array( 'id' => $new_id, 'message' => '作成しました' ); // 成功:ステータスコードを指定 return new WP_REST_Response( array( 'id' => $new_id ), 201 ); // エラー return new WP_Error( 'invalid_data', 'データが不正です', array( 'status' => 400 ) ); |
URLパラメータとルートパターン
URLパスの一部をパラメータとして取得でき、リソースIDをURLに含めたRESTfulな設計が可能です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// /wp-json/myplugin/v1/posts/123 に対応 register_rest_route( 'myplugin/v1', '/posts/(?P<id>\d+)', array( 'methods' => 'GET', 'callback' => function( WP_REST_Request $request ) { $post_id = $request->get_param( 'id' ); $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'not_found', '投稿が見つかりません', array( 'status' => 404 ) ); } return array( 'id' => $post->ID, 'title' => $post->post_title ); }, 'permission_callback' => '__return_true', ) ); |
セキュリティの「型」|nonce・権限・サニタイズ
非同期処理のセキュリティは「権限チェック → nonce検証 → サニタイズ → バリデーション → エスケープ」の5段階を毎回この順序で実行することが「型」です。1つでも欠けるとセキュリティホールになります。
非同期機能は「外部から叩かれる」機能です。だからこそ入口の守りがすべてです。
サニタイズ関数一覧
| 関数 | 用途 | 動作 |
|---|---|---|
sanitize_text_field() |
1行テキスト | HTMLタグ・改行を除去 |
sanitize_textarea_field() |
複数行テキスト | HTMLタグ除去・改行保持 |
sanitize_email() |
メールアドレス | メール形式に整形 |
absint() |
正の整数 | 絶対値の整数に変換 |
sanitize_key() |
キー名 | 英小文字・数字・_・-のみ |
wp_create_nonce() / wp_verify_nonce()|トークン生成と検証
nonceは「Number used ONCE」の略ですが、WordPressでは一定時間有効なトークンです。CSRF攻撃を防ぐために使用します。
|
1 2 3 4 5 6 7 |
// nonce生成(PHPで生成してJSに渡す) $nonce = wp_create_nonce( 'myplugin_action' ); // nonce検証(Ajax処理側) if ( ! wp_verify_nonce( $_POST['nonce'], 'myplugin_action' ) ) { wp_send_json_error( array( 'message' => 'セキュリティエラー' ), 403 ); } |
REST APIでのnonce
REST APIでも操作系エンドポイントではnonceの使用が推奨されます。WordPressのREST APIはX-WP-Nonceヘッダーまたは_wpnonceパラメータでnonceを受け付けます。
|
1 2 3 4 5 6 7 8 9 |
// JavaScript側 fetch('/wp-json/myplugin/v1/settings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpApiSettings.nonce }, body: JSON.stringify({ enabled: true }) }); |
外部API通信|HTTP API(wp_remote_*)
外部APIとの通信にはPHPのcURLではなくWordPressのHTTP API(wp_remote_get/wp_remote_post)を使ってください。プロキシ設定やSSL証明書の処理が自動化され、is_wp_error()でエラーハンドリングも統一できます。
外部サービスのAPIと連携する場合、PHPのfile_get_contents()やcURLを直接使うのではなく、WordPressのHTTP APIを使用します。WordPress環境に最適化されており、環境差異を吸収してくれます。
wp_remote_get() / wp_remote_post()
|
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 |
// GETリクエスト $response = wp_remote_get( 'https://api.example.com/items', array( 'timeout' => 10, 'headers' => array( 'Authorization' => 'Bearer ' . $api_token, 'Accept' => 'application/json', ), ) ); // エラーチェック if ( is_wp_error( $response ) ) { error_log( 'API Error: ' . $response->get_error_message() ); return false; } // HTTPステータスコード確認 $code = wp_remote_retrieve_response_code( $response ); if ( $code !== 200 ) { error_log( 'API returned status: ' . $code ); return false; } // レスポンスボディを取得 $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// POSTリクエスト(JSONを送信) $response = wp_remote_post( 'https://api.example.com/items', array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_token, ), 'body' => wp_json_encode( array( 'name' => $item_name, 'price' => $price, ) ), ) ); |
外部APIを扱う際の心構え
外部APIは「遅い・落ちる・仕様が変わる」ものです。タイムアウト設定(デフォルト5秒では短い場合がある)、エラーハンドリング(4xx、5xx、タイムアウトそれぞれへの対応)、Transientsによるキャッシュ、一時的なエラーへのリトライロジック——これらを前提に設計してください。
キャッシュ|Transients API
毎回外部APIを叩く、毎回重い集計クエリを実行する——これをやめるだけでサイトのレスポンスは劇的に改善します。set_transient()で有効期限付きキャッシュを保存し、キャッシュキーにはバージョン番号を含めることで一括無効化もできます。
Transients APIは、データベースのwp_optionsテーブルに保存される有効期限付きキャッシュです。Object Cache(Redis、Memcached等)が有効な環境では自動的にそちらを使用し、さらに高速になります。
set_transient() / get_transient() / delete_transient()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// キャッシュに保存(10分間有効) set_transient( 'myplugin_api_data', $data, 10 * MINUTE_IN_SECONDS ); // キャッシュから取得 $cached = get_transient( 'myplugin_api_data' ); if ( $cached === false ) { // キャッシュがない、または期限切れ $data = myplugin_fetch_from_api(); set_transient( 'myplugin_api_data', $data, 10 * MINUTE_IN_SECONDS ); } else { $data = $cached; } // キャッシュを削除(設定変更時など) delete_transient( 'myplugin_api_data' ); |
WordPressには有効期限を指定するための定数が用意されています。MINUTE_IN_SECONDS(60)、HOUR_IN_SECONDS(3600)、DAY_IN_SECONDS(86400)、WEEK_IN_SECONDS(604800)です。
キャッシュキー戦略
キャッシュで最も多い事故は「キーが雑で、異なる条件のデータが混在する」ことです。プラグイン名、バージョン(仕様変更時に一括無効化するため)、スコープ(マルチサイトならblog_id)、条件(検索語、ページ番号等)をキーに含めます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function myplugin_cache_key( $prefix, $context = array() ) { $version = get_option( 'myplugin_cache_version', 1 ); $context = array_merge( array( 'blog' => get_current_blog_id(), 'version' => $version, ), $context ); return 'myplugin_' . $prefix . '_' . md5( wp_json_encode( $context ) ); } // キャッシュの一括無効化(バージョンを上げるだけ) function myplugin_invalidate_all_cache() { $version = get_option( 'myplugin_cache_version', 1 ); update_option( 'myplugin_cache_version', $version + 1 ); } |
Cron|重い処理の非同期化
レスポンスタイムに影響する重い処理は、ユーザーのリクエスト中に実行するのではなくWP-Cronでバックグラウンド処理にしてください。プラグイン有効化時にwp_schedule_event()で登録し、無効化時にwp_clear_scheduled_hook()で必ず解除します。
wp_schedule_event() / wp_clear_scheduled_hook()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// プラグイン有効化時に登録 register_activation_hook( __FILE__, function() { if ( ! wp_next_scheduled( 'myplugin_hourly_task' ) ) { wp_schedule_event( time(), 'hourly', 'myplugin_hourly_task' ); } }); // タスク実行 add_action( 'myplugin_hourly_task', function() { myplugin_sync_external_data(); myplugin_cleanup_old_records(); }); // プラグイン無効化時に解除 register_deactivation_hook( __FILE__, function() { wp_clear_scheduled_hook( 'myplugin_hourly_task' ); }); |
wp_schedule_single_event()|1回だけ実行
即座には実行せず、指定時刻に1回だけ実行したいタスクに使用します。データインポート完了後のメール通知などに適しています。
|
1 2 3 4 5 6 |
// 5分後に1回だけ実行 wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'myplugin_send_notification', array( $user_id, $message ) ); |
WP-Cronの制限と対策
WP-Cronは「擬似Cron」であり、WordPressへのアクセスがあったときに初めてチェックが実行されます。アクセスが少ないサイトではスケジュール通りに実行されないことがあります。高い精度が必要な場合は、サーバーのcronで直接wp-cron.phpを叩く設定を検討してください。
|
1 2 |
# サーバーのcrontabに追加 */5 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1 |
この場合、wp-config.phpでWP-Cronの自動実行を無効化します。
|
1 |
define( 'DISABLE_WP_CRON', true ); |
WP-Cronが動かない問題の詳しい対処法は「WP-Cronが動かない|Xserverのcronで時間通りのメール送信を取り戻した話」で、Xserverでの具体的な設定手順を解説しています。
デバッグとログ|非同期処理のトラブルシューティング
非同期処理のバグは「通信が失敗しているのか」「権限で弾かれているのか」「nonceが期限切れなのか」の切り分けが9割です。ブラウザのNetworkタブでHTTPステータスを確認し、PHPのdebug.logでサーバー側のエラーを確認してください。
WP_DEBUG / WP_DEBUG_LOG
wp-config.phpで以下を設定すると、エラーがログファイルに記録されます。
|
1 2 3 |
define( 'WP_DEBUG', true ); define( 'WP_DEBUG_LOG', true ); // wp-content/debug.log に出力 define( 'WP_DEBUG_DISPLAY', false ); // 画面には表示しない(本番環境向け) |
error_log()|デバッグ情報の出力
|
1 2 3 4 5 6 |
error_log( 'myplugin: Processing started' ); error_log( 'myplugin: Data = ' . print_r( $data, true ) ); if ( is_wp_error( $result ) ) { error_log( 'myplugin: Error - ' . $result->get_error_message() ); } |
デバッグのチェックポイント
非同期処理がうまく動かないとき、以下の順序でチェックすると効率的です。リクエストはブラウザのNetworkタブで届いているか確認。HTTPステータスが403なら権限、400ならバリデーション、500ならPHPエラー。nonceは生成時刻とアクション名の一致を確認。権限はログイン状態とcapabilityを確認。PHPエラーはdebug.logを確認します。
拡張性設計|自前フック(apply_filters / do_action)
プラグインを「他の開発者が拡張できる設計」にするにはapply_filters()とdo_action()で独自フックを提供します。Part2で解説したadd_action/add_filterは既存フックに処理を追加する側でしたが、ここでは「フックを作る側」の設計を解説します。
プラグイン審査でも「適切なフックの提供」は品質シグナルとして評価されます。自分のプラグインのデータや動作を、テーマや他のプラグインがカスタマイズできるようにしておくことが、長期運用されるプラグインの条件です。
do_action()|処理の拡張ポイントを作る
「ここで何か追加の処理を実行したい人がいるかもしれない」というポイントに設置します。たとえばデータ保存後やメール送信前などです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// プラグイン側:フックを提供する function myplugin_process_order( $order_id ) { // 注文処理のメイン処理 $order = myplugin_save_order( $order_id ); // 拡張ポイント:注文処理完了後にカスタム処理を実行できるようにする do_action( 'myplugin_order_processed', $order_id, $order ); return $order; } // テーマやアドオン側:フックを使う add_action( 'myplugin_order_processed', function( $order_id, $order ) { // Slack通知を追加 myplugin_addon_notify_slack( $order ); }, 10, 2 ); |
apply_filters()|値の変更を許可する
「この値をカスタマイズしたい人がいるかもしれない」というデータに設置します。デフォルト値を渡し、フィルターを通して最終値を決定します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// プラグイン側:デフォルト値をフィルター可能にする function myplugin_get_max_items() { $default = 10; // テーマやアドオンがこの値を変更できる return apply_filters( 'myplugin_max_items', $default ); } // メール送信前にテンプレートを差し替え可能にする function myplugin_send_notification( $user_id, $message ) { $template = 'default'; $template = apply_filters( 'myplugin_email_template', $template, $user_id ); $headers = apply_filters( 'myplugin_email_headers', array( 'Content-Type: text/html; charset=UTF-8', ) ); wp_mail( $email, $subject, $body, $headers ); } // テーマ側:最大件数を変更 add_filter( 'myplugin_max_items', function( $default ) { return 20; // 20件に変更 }); |
フック命名規則
フック名には必ずプラグインのプレフィックスを付けてください。他プラグインとの名前衝突を防ぐためです。命名パターンは{prefix}_{対象}_{タイミング}が一般的です。例:myplugin_order_before_save、myplugin_api_response_filtered。
コンテキスト検出|wp_doing_ajax / wp_doing_cron / wp_get_environment_type
「今のリクエストがAjaxなのかCronなのか通常ページなのか」を正確に判定することで、不要な処理のスキップやデバッグの切り分けが容易になります。wp_get_environment_type()でステージング環境を検出すれば、本番では無効にすべきデバッグ処理を安全に制御できます。
wp_doing_ajax()|Ajaxリクエストかどうかを判定
WordPress 4.7で追加された関数で、defined( 'DOING_AJAX' ) && DOING_AJAXを短く書けるラッパーです。admin-ajax.php経由のリクエスト時にtrueを返します。REST APIリクエストではfalseを返す点に注意してください。
|
1 2 3 4 5 6 7 |
// Ajaxリクエスト時はサイドバーウィジェットの処理をスキップ add_action( 'widgets_init', function() { if ( wp_doing_ajax() ) { return; // Ajaxリクエストではウィジェット初期化不要 } register_sidebar( /* ... */ ); }); |
wp_doing_cron()|Cronリクエストかどうかを判定
WP-Cronで実行されているリクエストかどうかを判定します。Cron実行中に不要な処理(アセットの読み込みなど)をスキップする場合に使います。
|
1 2 3 4 5 6 7 |
// Cronジョブ中は重いフィルターをスキップ add_filter( 'the_content', function( $content ) { if ( wp_doing_cron() ) { return $content; // Cron中はフィルタリング不要 } return myplugin_enhance_content( $content ); }); |
wp_get_environment_type()|実行環境を検出
WordPress 5.5で追加された関数で、wp-config.phpのWP_ENVIRONMENT_TYPE定数を読み取り、local、development、staging、productionのいずれかを返します。未設定の場合はproductionがデフォルトです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// wp-config.php で設定 define( 'WP_ENVIRONMENT_TYPE', 'staging' ); // プラグイン内で環境に応じた処理を分岐 $env = wp_get_environment_type(); if ( $env === 'production' ) { // 本番のみ:エラーをメール通知 add_action( 'myplugin_api_error', 'myplugin_notify_admin' ); } elseif ( in_array( $env, array( 'development', 'local' ), true ) ) { // 開発環境のみ:詳細なデバッグログを出力 add_filter( 'myplugin_debug_level', function() { return 'verbose'; } ); } // ステージング環境では外部APIの本番エンドポイントを叩かない if ( $env !== 'production' ) { add_filter( 'myplugin_api_base_url', function() { return 'https://sandbox.api.example.com/'; }); } |
REST APIヘルパー関数|rest_url / rest_ensure_response
REST APIのURL生成にはrest_url()を使い、コールバックの返り値にはrest_ensure_response()を通すことで、エンドポイントの設計品質が上がります。URLのハードコードやレスポンス形式の不統一を防ぐ関数です。
rest_url()|REST APIエンドポイントのURLを安全に生成
JavaScriptに渡すAPIのベースURLを安全に生成します。/wp-json/のパスをハードコードすると、パーマリンク設定が「基本」の場合に?rest_route=/形式になることを見落として壊れます。
|
1 2 3 4 5 6 7 8 9 10 |
// PHP側:JavaScriptにREST APIのURLを渡す wp_localize_script( 'myplugin-app', 'MyPluginAPI', array( 'baseUrl' => esc_url_raw( rest_url( 'myplugin/v1/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), ) ); // JavaScript側 fetch( MyPluginAPI.baseUrl + 'settings', { headers: { 'X-WP-Nonce': MyPluginAPI.nonce } }); |
rest_ensure_response()|レスポンスを正規化
コールバック関数が配列、WP_REST_Response、WP_Errorのいずれを返しても、適切なWP_REST_Responseオブジェクトに変換します。コールバックの最後にこの関数を通しておくと、返り値の型を意識しなくて済みます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
register_rest_route( 'myplugin/v1', '/items', array( 'methods' => 'GET', 'callback' => function( WP_REST_Request $request ) { $items = myplugin_get_items(); if ( empty( $items ) ) { return new WP_Error( 'no_items', 'アイテムがありません', array( 'status' => 404 ) ); } // 配列でもWP_ErrorでもWP_REST_Responseに正規化される return rest_ensure_response( $items ); }, 'permission_callback' => '__return_true', ) ); |
HTTP通信の完全版|PUT/DELETE対応とSSRF対策
外部APIとのCRUD操作にはwp_remote_request()でPUT/DELETE/PATCHメソッドを使います。また、ユーザー入力のURLを外部リクエストに使う場合はwp_safe_remote_get()でSSRF攻撃(内部ネットワークへの不正アクセス)を防いでください。
wp_remote_request()|任意のHTTPメソッドでリクエスト
Section4で解説したwp_remote_get()とwp_remote_post()はGET/POST専用ですが、RESTful APIとの通信ではPUT・DELETE・PATCHも必要です。wp_remote_request()で任意のメソッドを指定できます。
|
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 |
// PUTリクエスト(リソースの更新) $response = wp_remote_request( 'https://api.example.com/items/123', array( 'method' => 'PUT', 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_token, ), 'body' => wp_json_encode( array( 'name' => '更新後の名前' ) ), ) ); // DELETEリクエスト(リソースの削除) $response = wp_remote_request( 'https://api.example.com/items/123', array( 'method' => 'DELETE', 'timeout' => 10, 'headers' => array( 'Authorization' => 'Bearer ' . $api_token, ), ) ); // PATCHリクエスト(部分更新) $response = wp_remote_request( 'https://api.example.com/items/123', array( 'method' => 'PATCH', 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_token, ), 'body' => wp_json_encode( array( 'status' => 'archived' ) ), ) ); |
wp_safe_remote_get() / wp_safe_remote_post()|SSRF対策
通常のwp_remote_get()はURLの検証を行いません。ユーザーが入力したURLをそのまま渡すと、http://localhost/やhttp://192.168.1.1/のような内部ネットワークへのリクエストが実行される危険があります(SSRF攻撃)。wp_safe_remote_get()はプライベートIPやループバックアドレスへのリクエストをブロックします。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// ❌ NG:ユーザー入力URLを検証なしでリクエスト $user_url = $_POST['webhook_url']; $response = wp_remote_get( $user_url ); // SSRFの危険 // ✅ OK:wp_safe_remote_get()でプライベートIPをブロック $user_url = esc_url_raw( $_POST['webhook_url'] ); $response = wp_safe_remote_get( $user_url ); if ( is_wp_error( $response ) ) { // プライベートIPの場合はここでエラーになる error_log( 'Blocked request: ' . $response->get_error_message() ); } |
wp_remote_retrieve_headers()|レスポンスヘッダーの取得
APIのレート制限情報やページネーション情報はレスポンスヘッダーに含まれることが多いです。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
$response = wp_remote_get( 'https://api.example.com/items' ); // 全ヘッダーを取得 $headers = wp_remote_retrieve_headers( $response ); // 特定のヘッダーを取得 $rate_limit = wp_remote_retrieve_header( $response, 'x-ratelimit-remaining' ); $next_page = wp_remote_retrieve_header( $response, 'link' ); if ( (int) $rate_limit < 10 ) { error_log( 'myplugin: API rate limit approaching: ' . $rate_limit ); } |
ユーティリティ関数|wp_json_encode / wp_parse_args / wp_unslash
非同期処理で頻繁に使うユーティリティ関数を3つ紹介します。wp_json_encode()はjson_encode()より安全な日本語対応のJSON出力、wp_parse_args()は関数の引数をデフォルト値とマージする定番パターン、wp_unslash()はPHPのマジッククォートを除去する前処理です。
wp_json_encode()|日本語対応の安全なJSON出力
PHPのjson_encode()はデフォルトで日本語をUnicodeエスケープ(\u3042形式)します。wp_json_encode()はJSON_UNESCAPED_UNICODEフラグを自動的に適用し、エンコードエラー時にもfalseではなく空文字列を返すため安全です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// ❌ json_encode():日本語がエスケープされる echo json_encode( array( 'name' => 'テスト' ) ); // 出力: {"name":"\u30c6\u30b9\u30c8"} // ✅ wp_json_encode():日本語がそのまま出力される echo wp_json_encode( array( 'name' => 'テスト' ) ); // 出力: {"name":"テスト"} // REST APIのコールバック内でJSONを返す場合 $response = new WP_REST_Response( array( 'message' => '保存しました', 'data' => $result, ) ); return $response; // 内部的にwp_json_encode()が使われる |
wp_parse_args()|引数のデフォルト値マージ
関数に渡されたユーザー引数とデフォルト値を安全にマージする関数です。shortcode_atts()がショートコード専用なのに対し、wp_parse_args()は汎用的に使えます。配列・オブジェクト・クエリ文字列のいずれの形式でも受け取れます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 関数定義側:デフォルト値を設定 function myplugin_get_items( $args = array() ) { $defaults = array( 'per_page' => 10, 'page' => 1, 'orderby' => 'date', 'order' => 'DESC', 'status' => 'active', ); // ユーザー引数で上書き、指定されていない項目はデフォルト値を使用 $args = wp_parse_args( $args, $defaults ); // $args['per_page'] = 10(デフォルト) // $args['order'] = 'DESC'(デフォルト) // ... } // 呼び出し側:必要な項目だけ指定すればよい $items = myplugin_get_items( array( 'per_page' => 20, 'status' => 'archived', ) ); |
wp_unslash()|マジッククォートの除去
WordPressは$_GET、$_POST、$_COOKIE、$_SERVERの値にスラッシュを自動追加します(wp_magic_quotes()による処理)。サニタイズ前にwp_unslash()で余分なスラッシュを除去しないと、「O\’Reilly」のようなデータがデータベースに入ります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// ❌ NG:スラッシュが二重に入る可能性 $name = sanitize_text_field( $_POST['name'] ); // O'Reilly → O\'Reilly がDBに保存される // ✅ OK:wp_unslash()で前処理 $name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); // O'Reilly → O'Reilly が正しく保存される // 配列にも使える $data = wp_unslash( $_POST['myplugin_settings'] ?? array() ); $clean = array( 'title' => sanitize_text_field( $data['title'] ?? '' ), 'desc' => sanitize_textarea_field( $data['desc'] ?? '' ), ); |
セキュリティユーティリティ|トークン生成・ハッシュ・ファイル操作
APIトークンの生成にはwp_generate_password()、データの完全性検証にはwp_hash()、ファイル操作にはWP_Filesystemを使ってください。PHPのrand()やfile_put_contents()を直接使うと、セキュリティ上の問題が生じます。
wp_generate_password()|暗号学的に安全なランダム文字列
APIトークン、セッションID、ワンタイムキーなど、推測不可能な文字列が必要な場面で使います。PHPのrand()やuniqid()は暗号学的に安全ではありませんが、この関数はrandom_bytes()ベースの安全な乱数を生成します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 32文字のランダムトークンを生成(特殊文字なし) $token = wp_generate_password( 32, false, false ); // 例: "aB3kL9mNpQrStUvWxYz1234567890ab" // 64文字の強力なトークン(特殊文字あり) $secret = wp_generate_password( 64, true, true ); // 使用例:APIキーの生成 function myplugin_generate_api_key() { $key = wp_generate_password( 40, false, false ); update_option( 'myplugin_api_key', wp_hash( $key ) ); // ハッシュして保存 return $key; // 平文はユーザーに1回だけ表示 } |
wp_hash()|HMACハッシュ
データの完全性を検証するためのHMACハッシュを生成します。WordPress固有のソルト(wp-config.phpのAUTH_SALT)を使用するため、同じ入力でもサイトごとに異なるハッシュが生成されます。
|
1 2 3 4 5 6 7 8 9 10 11 |
// データの完全性チェックに使う $data = wp_json_encode( array( 'user_id' => 42, 'action' => 'confirm' ) ); $signature = wp_hash( $data ); // 検証側 $received_data = $_GET['data']; $received_sig = $_GET['sig']; if ( wp_hash( $received_data ) !== $received_sig ) { wp_die( 'データが改ざんされています' ); } |
wp_upload_dir()|アップロードディレクトリの情報を取得
プラグインが一時ファイルやログを保存する場所を取得する際に使います。wp-content/uploads/のパスをハードコードすると、UPLOADS定数やフィルターでカスタマイズされている環境で壊れます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$upload_dir = wp_upload_dir(); // 返り値の構造 // $upload_dir['path'] → /var/www/html/wp-content/uploads/2026/03(絶対パス) // $upload_dir['url'] → https://example.com/wp-content/uploads/2026/03(URL) // $upload_dir['basedir'] → /var/www/html/wp-content/uploads(ベース絶対パス) // $upload_dir['baseurl'] → https://example.com/wp-content/uploads(ベースURL) // $upload_dir['error'] → false(エラー時はメッセージ文字列) // プラグイン専用ディレクトリの作成 $plugin_dir = $upload_dir['basedir'] . '/myplugin-logs/'; if ( ! file_exists( $plugin_dir ) ) { wp_mkdir_p( $plugin_dir ); // ← mkdir()ではなくwp_mkdir_p()を使う // .htaccessで直接アクセスをブロック(セキュリティ対策) file_put_contents( $plugin_dir . '.htaccess', 'Deny from all' ); } |
WP_Filesystem|安全なファイル読み書き
プラグイン審査ではfile_put_contents()やfile_get_contents()の直接使用は指摘対象です。WP_Filesystemを使うことで、FTP/SSH経由のファイル操作にも対応した安全なファイル操作が実現します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// WP_Filesystemの初期化 global $wp_filesystem; if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } WP_Filesystem(); // ファイルの読み込み $content = $wp_filesystem->get_contents( $file_path ); // ファイルの書き込み $wp_filesystem->put_contents( $file_path, $data, FS_CHMOD_FILE // 適切なパーミッション(0644) ); // ファイルの存在確認 if ( $wp_filesystem->exists( $file_path ) ) { // ファイルの削除 $wp_filesystem->delete( $file_path ); } |
sanitize_file_name()|アップロードファイル名の無害化
ユーザーがアップロードしたファイル名には、ディレクトリトラバーサル攻撃のための../や、特殊文字が含まれる可能性があります。sanitize_file_name()は危険な文字を除去し、安全なファイル名に変換します。
|
1 2 3 4 5 6 7 8 9 |
// ❌ NG:ユーザーのファイル名をそのまま使う $filename = $_FILES['upload']['name']; // 例: "../../wp-config.php" (ディレクトリトラバーサル) // ✅ OK:sanitize_file_name()で無害化 $filename = sanitize_file_name( $_FILES['upload']['name'] ); // 例: "wp-config.php"(危険なパス部分が除去される) $safe_path = $upload_dir['path'] . '/' . $filename; |
wp_json_file_decode()|JSONファイルの安全な読み込み
WordPress 5.9で追加された関数で、JSONファイルを読み込んでデコードします。file_get_contents() + json_decode()の組み合わせを1関数で安全に行えます。設定ファイルやデータファイルの読み込みに使います。
|
1 2 3 4 5 6 7 8 |
// プラグインの設定JSONを読み込む $config_path = plugin_dir_path( __FILE__ ) . 'config/defaults.json'; $config = wp_json_file_decode( $config_path, array( 'associative' => true ) ); if ( $config === null ) { error_log( 'myplugin: Failed to decode config file' ); $config = array(); // フォールバック } |
まとめ:壊れない非同期機能の共通パターン
Ajax/REST APIを使った「壊れにくい」機能には共通パターンがあります。入口の守り(権限・nonce・サニタイズの3点セット)、統一されたレスポンス形式、Transientsによるキャッシュ、外部APIを信頼しない設計、問題追跡のためのログ出力——この5つを守れば、安全で保守しやすい非同期機能が作れます。
| 原則 | 要点 |
|---|---|
| 入口の守りが固い | 権限チェック・nonce検証・サニタイズの3点セットが常に存在する |
| レスポンス形式が統一 | 成功も失敗も一貫したJSON構造とHTTPステータスで返す |
| 重い処理を直接叩かない | Transientsでキャッシュ、WP-Cronでバックグラウンド処理 |
| 外部APIを信頼しない | タイムアウト・エラーハンドリング・フォールバックを必ず実装 |
| ログが残る | 問題発生時に原因を追跡できるerror_log()が仕込まれている |
重要なのは、これらの関数を暗記することではありません。「どんな場面で」「何を守りながら」「どう組み合わせるか」というパターンを身につけることです。
なお、プラグイン開発で使うセキュリティ関数(エスケープ・サニタイズ)の基本はPart1で、Settings API・メニュー・CPTはPart2で解説しています。
プラグイン審査では、ここで解説したnonce検証やサニタイズの実装が審査項目に含まれています。審査の全手順と差し戻し対応については「WordPress.orgのプラグイン審査に一発で通すための全手順」を参照してください。










コメント