2026年8月23日
2026年8月23日
WordPressのUpdate URIヘッダーでプラグインの更新先を自社サーバーに変更する方法
自作プラグインや社内向けプラグインの更新を WordPress.org 以外のサーバーから配りたい場合は、プラグインのヘッダーに Update URI を書き、そのホスト名に対応する update_plugins_{$hostname} フィルターで更新情報を返します。WordPress 5.8 で追加された仕組みで、同じスラッグの別プラグインが WordPress.org にあっても上書きされにくくなる効果もあります。この記事では、ヘッダーの書き方、更新情報を返すフィルターの実装、「詳細を表示」画面への対応、非公開配布での認証、動作確認とつまずきやすい点までを説明します。
以降のコードは、プラグインのメインファイル(またはそこから読み込むファイル)に書く前提です。テーマの functions.php ではなく、更新される側のプラグイン自身に組み込みます。本番サイトで試す前に、バックアップを取ったうえでステージング環境で更新の一連の流れを確認してください。
Update URI の役割を理解する
WordPress は通常、インストール済みプラグインの一覧を WordPress.org の API に送り、新しいバージョンがあるかを問い合わせます。このとき判断材料になるのはスラッグ(プラグインのフォルダー名)なので、自作プラグインのフォルダー名が WordPress.org の別プラグインと偶然同じだと、無関係なプラグインの「更新」が表示され、更新すると中身が置き換わってしまう事故が起こり得ました。
Update URI ヘッダーはこの問題への対策として導入されました。値が WordPress.org 以外を指していれば、そのプラグインは WordPress.org の更新対象から外れます。さらに WordPress は値からホスト名を取り出し、update_plugins_{ホスト名} という名前のフィルターを実行します。このフィルターで更新情報を返せば、通常の更新画面・一括更新・自動更新の仕組みにそのまま乗せられます。
用途は大きく2つです。
- WordPress.org からの更新を止めたいだけ:フォークして改造したプラグインなど。値に
falseのようにホスト名として解釈できない文字列を書くと、WordPress.org の更新対象から外れ、フィルターも実行されません。 - 自社サーバーから更新を配りたい:値に自社のURLを書き、フィルターで更新情報を返します。この記事の主題はこちらです。
手順1:プラグインヘッダーに Update URI を書く
メインファイルのヘッダーコメントに1行追加します。
<?php
/**
* Plugin Name: My Private Plugin
* Version: 1.2.0
* Requires PHP: 7.4
* Update URI: https://updates.example.com/my-private-plugin/
*/
defined( 'ABSPATH' ) || exit;
この例ではホスト名が updates.example.com なので、実行されるフィルター名は update_plugins_updates.example.com になります。URLのパス部分はフィルター名には使われませんが、WordPress は更新情報の id としてこの値を使うので、プラグインごとに一意になるように書いておきます。
Update URI を既存のプラグインに後から追加する場合、追加したバージョンまでは従来の仕組みで配る必要があります。古いバージョンのサイトはヘッダーを知らないまま動いているためです。
手順2:更新サーバーに情報ファイルを置く
更新サーバー側は、最新版の情報を返すJSONと、配布用のZIPファイルを置くだけで始められます。PHPで動く必要はありません。
{
"version": "1.3.0",
"download_url": "https://updates.example.com/my-private-plugin/my-private-plugin-1.3.0.zip",
"homepage": "https://updates.example.com/my-private-plugin/",
"tested": "6.8",
"requires_php": "7.4",
"changelog": "<h4>1.3.0</h4><ul><li>設定画面の不具合を修正</li></ul>"
}
ZIPの中身は、最上位に my-private-plugin/ フォルダーがあり、その中にメインファイルが入る構成にします。ここがずれると、更新後にフォルダー名が変わってプラグインが別物として扱われ、無効化されることがあります。GitHub が自動生成するアーカイブはフォルダー名に「リポジトリ名-タグ名」が付くため、そのままでは使わず、リリース用のZIPを自分で作るのが無難です。
手順3:update_plugins_{$hostname} フィルターで更新情報を返す
フィルターには4つの引数が渡されます。既定値の $update(false)、ヘッダーを読み取った $plugin_data、my-private-plugin/my-private-plugin.php 形式の $plugin_file、サイトの言語一覧 $locales です。
<?php
const MY_PRIVATE_PLUGIN_MANIFEST = 'https://updates.example.com/my-private-plugin/info.json';
/**
* 更新サーバーの情報を取得する(12時間キャッシュ)。
*/
function my_private_plugin_get_manifest() {
$cached = get_site_transient( 'my_private_plugin_manifest' );
if ( is_array( $cached ) ) {
return $cached;
}
$response = wp_remote_get( MY_PRIVATE_PLUGIN_MANIFEST, array( 'timeout' => 10 ) );
$data = array();
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$json = json_decode( wp_remote_retrieve_body( $response ), true );
if ( is_array( $json ) && my_private_plugin_is_valid_manifest( $json ) ) {
$data = $json;
}
}
// 失敗時も空配列を短時間キャッシュし、管理画面のたびに通信しないようにする
set_site_transient( 'my_private_plugin_manifest', $data, $data ? 12 * HOUR_IN_SECONDS : HOUR_IN_SECONDS );
return $data;
}
/**
* 受け取った情報を検証する。配布ZIPは自社ホストの HTTPS に限定する。
*/
function my_private_plugin_is_valid_manifest( array $json ) {
if ( empty( $json['version'] ) || ! preg_match( '/^\d+(\.\d+){0,3}$/', (string) $json['version'] ) ) {
return false;
}
$url = isset( $json['download_url'] ) ? (string) $json['download_url'] : '';
return 'https' === wp_parse_url( $url, PHP_URL_SCHEME )
&& 'updates.example.com' === wp_parse_url( $url, PHP_URL_HOST );
}
add_filter( 'update_plugins_updates.example.com', 'my_private_plugin_update_info', 10, 4 );
function my_private_plugin_update_info( $update, $plugin_data, $plugin_file, $locales ) {
// 同じホストを使う別プラグインの処理を邪魔しない
if ( plugin_basename( __FILE__ ) !== $plugin_file ) {
return $update;
}
$manifest = my_private_plugin_get_manifest();
if ( empty( $manifest['version'] ) ) {
return $update;
}
return array(
'slug' => dirname( $plugin_file ),
'version' => $manifest['version'],
'url' => esc_url_raw( $manifest['homepage'] ?? '' ),
'package' => esc_url_raw( $manifest['download_url'] ),
'tested' => sanitize_text_field( $manifest['tested'] ?? '' ),
'requires_php' => sanitize_text_field( $manifest['requires_php'] ?? '' ),
);
}
このコードで押さえておきたいのは次の点です。
- 自分のプラグインかを最初に確かめる:同じホスト名を
Update URIに使うプラグインが複数あると、このフィルターはそれぞれについて呼ばれます。自分以外のときは受け取った$updateをそのまま返します。plugin_basename( __FILE__ )はメインファイルに書いた場合に正しい値になるので、別ファイルに書くときはメインファイルのパスを定数などで渡してください。 - **
versionは必須**:WordPress は返された配列にversionが無いと無視します。現在のバージョンより新しければ「更新あり」、同じか古ければ「更新なし」の一覧に入ります。比較はversion_compare()の規則に従うので、番号の付け方をそろえておきます。 - **
packageが空だと更新ボタンを押しても失敗する**:ライセンス切れなどで配布できない場合は、packageを空にして「更新はあるがダウンロードできない」状態として見せる設計もあります。 - キャッシュする:WordPress の更新確認は管理画面の表示などをきっかけに定期的に走ります。そのたびに外部へ通信すると管理画面が遅くなり、更新サーバーにも負荷がかかります。
手順4:「詳細を表示」の画面に情報を出す
プラグイン一覧や更新画面の「詳細を表示」リンクは、plugins_api フィルターを通じて情報を取得します。何もしないと WordPress.org に問い合わせて「見つかりません」となるため、自分のスラッグのときだけ情報を返します。
<?php
add_filter( 'plugins_api', function ( $result, $action, $args ) {
if ( 'plugin_information' !== $action || empty( $args->slug ) || 'my-private-plugin' !== $args->slug ) {
return $result;
}
$manifest = my_private_plugin_get_manifest();
if ( empty( $manifest['version'] ) ) {
return $result;
}
return (object) array(
'name' => 'My Private Plugin',
'slug' => 'my-private-plugin',
'version' => $manifest['version'],
'requires_php' => sanitize_text_field( $manifest['requires_php'] ?? '' ),
'download_link' => esc_url_raw( $manifest['download_url'] ),
'sections' => array(
'changelog' => wp_kses_post( $manifest['changelog'] ?? '' ),
),
);
}, 10, 3 );
更新サーバーから受け取ったHTMLは、自社のサーバーであっても wp_kses_post() を通してから表示します。更新サーバーが乗っ取られた場合に、管理画面へスクリプトを差し込まれるのを防ぐためです。
手順5:非公開配布では認証ヘッダーを付ける
社内やライセンス購入者だけに配る場合、ダウンロードURLにトークンをクエリ文字列で付けると、サーバーのアクセスログやリファラーに残りやすくなります。http_request_args フィルターで、自社ホストへのリクエストにだけ認証ヘッダーを付ける方法が扱いやすいです。
<?php
add_filter( 'http_request_args', function ( $args, $url ) {
if ( 'updates.example.com' !== wp_parse_url( $url, PHP_URL_HOST ) || ! defined( 'MY_PRIVATE_PLUGIN_TOKEN' ) ) {
return $args;
}
if ( ! is_array( $args['headers'] ) ) {
$args['headers'] = array();
}
$args['headers']['Authorization'] = 'Bearer ' . MY_PRIVATE_PLUGIN_TOKEN;
return $args;
}, 10, 2 );
トークンは wp-config.php に define( 'MY_PRIVATE_PLUGIN_TOKEN', 'your-token' ); のように定義し、プラグインのコードやリポジトリには書きません。このフィルターは情報ファイルの取得とZIPのダウンロードの両方に効きます。
手順6:動作を確認する
更新情報は一定時間キャッシュされるため、サーバー側でバージョンを上げてもすぐには表示されません。確認時は WordPress 側のキャッシュと、上のコードで作った独自キャッシュの両方を消します。WP-CLI が使える場合は次のとおりです。
wp transient delete update_plugins --network
wp transient delete my_private_plugin_manifest --network
wp plugin list --fields=name,version,update,update_version
--network はサイトトランジェント(get_site_transient() で扱うもの)を対象にする指定で、マルチサイトでなくても使えます。update が available になれば更新情報が届いています。管理画面からは「ダッシュボード」→「更新」の再確認ボタンでも更新確認を走らせられます(表記はバージョンにより異なります)。
最後に、ステージング環境で実際に更新を実行し、プラグインが有効なまま新しいバージョンになることを確かめます。自動更新を使う場合は、プラグイン一覧で自動更新を有効にしたうえで、次回の自動更新後に動作を確認してください。
つまずきやすい点
- 無効化中のプラグインに更新が出ない:フィルターを登録しているのはプラグイン自身のコードなので、プラグインを無効にしている間はフィルターが動かず、更新情報も表示されません。更新の仕組みを常に動かしたい場合は、更新確認の部分だけを mu-plugin に分ける方法があります。
- フィルターが呼ばれない:フィルター名のホスト名が
Update URIと一致しているか、wwwの有無まで含めて確認します。値がホスト名として解釈できない場合はフィルター自体が実行されません。 - 更新後にプラグインが無効になる:ZIPの最上位フォルダー名がプラグインのフォルダー名と違います。手順2のとおり作り直します。
- ダウンロードに失敗する:更新パッケージの取得は安全性を確認するHTTP関数を通るため、更新サーバーがプライベートIPアドレス(社内LANなど)にあると拒否されることがあります。外部から到達できるホスト名で配るのが基本です。
- WordPress.org の配布を続けたいプラグインに書いてしまった:
Update URIに WordPress.org 以外の値を書くと、WordPress.org からの更新が届かなくなります。WordPress.org で公開中のプラグインには書かないでください。
安全面の注意
WordPress.org 経由の配布では、公開時の審査や、WordPress が行うパッケージ署名の確認といった仕組みが一部働きますが、独自サーバーからの更新はそれらの対象外です。更新サーバーを管理できる人は、そのプラグインを入れた全サイトで任意のコードを実行できる立場になります。更新サーバーは HTTPS で運用し、管理画面やデプロイの権限を絞り、二段階認証を有効にしてください。
自分で実装する代わりに、更新確認の仕組みを提供するライブラリ(Plugin Update Checker など)や、GitHub などから更新を取得する Git Updater プラグインを使う方法もあります。どちらも開発が続いているプロジェクトですが、対応する機能や設定方法は変わることがあるため、導入時に公式のドキュメントを確認してください(2026年10月現在)。
まとめ
Update URIヘッダーに自社のURLを書くと、WordPress.org の更新対象から外れ、update_plugins_{ホスト名}フィルターが実行されます。- フィルターでは自分のプラグインかを確かめ、
versionとpackageを含む配列を返します。外部への通信結果はキャッシュします。 - 「詳細を表示」には
plugins_api、非公開配布の認証にはhttp_request_argsを使います。受け取ったHTMLやURLは検証・サニタイズします。 - ZIPの最上位フォルダー名をプラグインのフォルダー名と一致させないと、更新後に無効化されることがあります。
- 更新サーバーは全サイトにコードを配れる立場になるため、アクセス権と通信の安全を優先して運用します。