【WordPress5.8】新しいフック+GitHub APIで自作プラグインに更新通知を追加する

はじめに

公式のプラグインディレクトリにプラグインが公開されている場合、プラグインのバージョンが上がると、プラグインの一覧画面で該当プラグインに更新通知が表示され、プラグインをアップデートする事が出来ます。

また、WordPress5.5からテーマ・プラグインの自動更新機能を個別にオプトインする事ができ、有効化されたテーマ・プラグインについては1日2回の頻度でアップデートチェックと自動更新が行われます。

プラグイン開発者側としては、プラグインヘッダの Version を上げてSVNにcommitすればよいだけで、プラグイン側に自動更新機能を持たせる必要がありません。

ですが、何らかの理由でプラグインディレクトリに掲載しておらず独自に配布しているサードパーティ製のプラグインの場合は、更新に関して以下二つの問題点が考えられます。

同名のプラグインと衝突する可能性がある

プラグインディレクトリに登録されているプラグイン名(スラッグ)と同じ場合、後者のアップデートにより自作プラグインが上書きされてしまうというリスクがあります。

自作プラグインのリリース段階で一意のプラグイン名とした場合でも、後発の同名のプラグインがプラグインディレクトリに掲載されるという可能性もあります。

更新チェック機能を実装する必要がある

これまでのWordPressでは、サードパーティ製のプラグインの更新チェックは行ってくれませんので、自前で更新チェック機能を実装する必要があります。

一般的には、plugin-update-checkerなどのライブラリを使用したり、pre_set_site_transient_update_plugins フィルターフックなどを使って新バージョンの取得・バージョン比較等の処理を組み込むのではないかと思います。

WordPress5.8での変更点

WordPress5.8では、前述に2つの懸念点を改善するために、サードパーティ製プラグイン向けに以下2つの新機能が搭載されました。

“Update URI” プラグインヘッダ

プラグインヘッダに Update URI フィールドを追加し、https://wordpress.org/plugins/{$slug}/ または w.org/plugin/{$slug} 以外の URIを指定する事で、プラグインディレクトリに同名のプラグインがあったとしても、更新が行われなくなります。

これにより、自作プラグインがプラグインディレクトリに掲載された同名のプラグインに上書きされるというリスクを回避する事が出来ます。

サードパーティ製プラグインアップデート用のフィルターフック

update_plugins_{$hostname} という新しいフィルターフックが提供された事により、これまでに必要であったバージョンの比較や、transientへの追加などの処理を省略する事が出来るようになりました。

新しいフックを使った更新通知機能の実装

今回の記事の主目的として、この新しい機能とGitHub APIを使って、以下のような運用フローを実現してみたいと思います。

  • プラグインを更新したらタグ付けし、GitHubにPushする
  • プラグインのzipファイルを添付したReleaseを作成する
  • GitHubのLatest Releaseを参照して、WordPress管理画面に自作プラグインの更新通知が届く

WordPress環境、GitHubリポジトリの作成

2021年7月16日現在、WordPress5.8は正式リリース前ですので、WordPress Beta Testerプラグインなどで最新バージョンのWordPress 5.8-RC4にアップグレードしておきます。

WordPress 5.8-RC4にアップグレード

また本題ではないので詳細は省きますが、GitHub上にPublicなリポジトリを一つ作っておきます。 今回はリポジトリ名を「 my-plugin 」とします。

プラグインのベースを作成

管理画面でプラグインを認識させるための最低限の記述を行います。

<?php
/*
  Plugin Name: My Plugin v1.0.0
  Version: 1.0.0
  Update URI: taro-my-plugin
 */

ここでのキモは、もちろん Update URI なのですが、URIなので必ずしもURLである必要はありません。 また今のところ、Update URI の値は前述のフィルターフック名の一部としか使われていないので、存在しないURL等でも構わないと思います。

今回はGitHub APIとの連携を想定して、{username}-{plugin_name}の形式としました。

ちなみに Update URI は、以下のようにパースされてフィルターフック名の一部となります。

$hostname = wp_parse_url( esc_url_raw( $plugin_data['UpdateURI'] ), PHP_URL_HOST );
// ↓
$update = apply_filters( "update_plugins_{$hostname}", false, $plugin_data, $plugin_file, $locales )

具体的に、Update URIからどのようなフィルタ名になるか例をいくつかあげます。

Update URI: https://example.com
↓
update_plugins_example.com
-----
Update URI: https://www.example.com
↓
update_plugins_www.example.com
-----
Update URI: https://www.example.com
↓
update_plugins_www.example.com
-----
Update URI: hoge/fuga
↓
update_plugins_hoge
-----
Update URI: hoge-fuga
↓
update_plugins_hoge-fuga

プラグインが認識され有効化したら、GitHubのリポジトリの追加・pushを行っておきます。

git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:{username}/my-plugin.git
git push -u origin main

またInitial Releaseとして、1.0.0 のタグを付与しておきます。

git tag 1.0.0
git push --tags

ポイントは、タグ名は後で実装するバージョン比較用の文字列としても使用するため、純粋にバージョン名だけとし、v1.0.0 みたいにバージョン名以外の文字を入れない事です。

ここまでの作業で、GitHub上にタグが追加されていると思うので、プラグインフォルダから.gitフォルダを除いた上でzip化し、Releaseに添付します。

zipをReleaseに添付
zipをReleaseに添付

フィルターフックの導入

次に、今回使う新しいフック「 upd###ate_plugins_{$hostname} 」がちゃんと動作しているか、またどんな情報が渡ってくるかを確認してみます。

今回の例での Update URItaro-my-plugin ですので、以下のようにフックします。

<?php
/*
  Plugin Name: My Plugin v1.0.0
  Version: 1.0.0
  Update URI: taro-my-plugin
 */
​
function my_plugin_update_plugin( $update, $plugin_data ) {
    echo( '<pre>' );
    var_export( $update );
    echo "\n-----\n";
    var_export( $plugin_data );
    echo( '</pre>' );
    exit;
}
add_filter( 'update_plugins_taro-my-plugin', 'my_plugin_update_plugin', 10, 2 );

出力内容

false
-----
array (
    'Name' => 'My Plugin v1.0.0',
    'PluginURI' => '',
    'Version' => '1.0.0',
    'Description' => '',
    'Author' => '',
    'AuthorURI' => '',
    'TextDomain' => 'my-plugin',
    'DomainPath' => '',
    'Network' => false,
    'RequiresWP' => '',
    'RequiresPHP' => '',
    'UpdateURI' => 'taro-my-plugin',
    'Title' => 'My Plugin',
    'AuthorName' => '',
)

$update がfalseで、$plugin_data にはプラグインヘッダの情報が入っている事が確認出来ます。

GitHub APIで更新情報を取得する

WordPress側に更新がある事を伝えるには、最低限の情報として

  • 現バージョン番号
  • 新バージョン番号
  • 新バージョンのプラグインzipのダウンロードurl

をフック内でreturnする事で実現出来ます。

現バージョンは $plugin_data から分かるので、

  • 新バージョン番号
  • 新バージョンのプラグインzipのダウンロードurl

をGitHub APIで取得する事になります。

<?php
/*
  Plugin Name: My Plugin v1.0.0
  Version: 1.0.0
  Update URI: taro-my-plugin
 */
​
// Latest Releaseの情報を取得するためのエンドポイント
// https://api.github.com/repos/:owner/:repo/releases/latest
define( 'MY_PLUGIN_UPDATE_URL', 'https://api.github.com/repos/{user_name}/my-plugin/releases/latest' );
​
function my_plugin_update_plugin( $update, $plugin_data ) {
    // GitHub APIを使って、Releaseの最新バージョン情報を取得する
    $response = wp_remote_get( MY_PLUGIN_UPDATE_URL );
​
    // レスポンスエラー
    if( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
        return $update;
    }
​
    $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
    echo( '<pre>' );
    var_export( $response_body );
    echo( '</pre>' );
    exit;
}
add_filter( 'update_plugins_taro-my-plugin', 'my_plugin_update_plugin', 10, 2 );

出力結果(必要な情報のみ抜粋)

array (
	'tag_name' => '1.0.0',
	'name' => 'v1.0.0',
	'assets' => 
	array (
		0 => 
		array (
			'name' => 'my-plugin.zip',
			'browser_download_url' => 'https://github.com/{username}/my-plugin/releases/download/1.0.0/my-plugin.zip',
		),
	),
)

最新リリースのバージョン番号( tag_name )と、プラグインzipファイルのダウンロードURL( browser_download_url )が取得出来ている事が分かります。

これで、更新通知に必要な情報は揃ったので、コードを以下のようにします。

<?php
/*
  Plugin Name: My Plugin v1.0.0
  Version: 1.0.0
  Update URI: taro-my-plugin
 */

// Latest Releaseの情報を取得するためのエンドポイント
// https://api.github.com/repos/:owner/:repo/releases/latest
define( 'MY_PLUGIN_UPDATE_URL', 'https://api.github.com/repos/{user_name}/my-plugin/releases/latest' );

function my_plugin_update_plugin( $update, $plugin_data ) {
	// GitHub APIを使って、Releaseの最新バージョン情報を取得する
	$response = wp_remote_get( MY_PLUGIN_UPDATE_URL );

	// レスポンスエラー
	if( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
		return $update;
	}

	// 最新バージョン、zipファイルパッケージのURLを取得
	$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
	$new_version   = isset( $response_body['tag_name'] ) ? $response_body['tag_name'] : null;
	$package       = isset( $response_body['assets'][0]['browser_download_url'] ) ? $response_body['assets'][0]['browser_download_url'] : null;

	return array(
		'version'     => $plugin_data['Version'], // 現在のバージョン
		'new_version' => $new_version,            // 最新のバージョン
		'package'     => $package,                // zipファイルパッケージのURL
	);
}
add_filter( 'update_plugins_taro-my-plugin', 'my_plugin_update_plugin', 10, 2 );

ありがたい事に、バージョンが上がっているかどうかの処理( version_compare )やtransientへの追加は、フィルタの呼び元でよしなに処理してくれるので、自前で実装する必要はありません。

GitHubへのリリース

プラグインのが v1.0.0 から v2.0.0 にアップデートされたと想定して、とりあえず以下のようにプラグインヘッダの情報を変更します。

<?php
/*
  Plugin Name: My Plugin v2.0.0
  Version: 2.0.0
  Update URI: taro-my-plugin
 */

// 以下省略

次に、GitHubへのpushとタグ付けです。

git add .
git commit -m "v2.0.0"
git push
git tag 2.0.0
git push --tags

同様に、プラグインフォルダから.gitフォルダを除いた上でzip化し、Releaseを作成します。

Releaseを作成

更新通知の確認

プラグインをgit管理化においている開発環境では、既にプラグインのバージョンが v2.0.0 になってしまっているので、別にWordPress環境を立ち上げ、 Relseasesに添付されている v1.0.0のプラグインを手動インストール・有効化しておきます。 ※WordPress5.8へのアップデートを忘れずに!

更新通知の確認

ちゃんと通知が来てますね!

※アップデートチェックは1日2回の頻度でしか行われないので、すぐに更新通知を試してみたい方は、以下のコードを一時的に追加してみて下さい。

	// L327
	$time_not_changed = isset( $current->last_checked ) && $timeout > ( time() - $current->last_checked );
	// これを追加
	$time_not_changed = false;

	if ( $time_not_changed && ! $extra_stats ) {
		$plugin_changed = false;

更新もOK!

更新

問題点

ここまで書いておいてアレなのですが、今回の実装には2点問題があると思っています。 それぞれの問題点と対処法は以下。

GitHub APIのレート制限

GitHub APIのレスポンスを見ていただければ分かるのですが、Token無しの場合は「1時間に60リクエストまで」という制限があります。

'x-ratelimit-limit' => '60',
'x-ratelimit-remaining' => '45',
'x-ratelimit-reset' => '1626424370',
'x-ratelimit-resource' => 'core',
'x-ratelimit-used' => '15',

更新通知を実装するという事は、ある程度多数のユーザーに使用される事を想定していると思うので、レート制限に引っかかって更新が遅れるという可能性が考えられます。

対象法としては以下2つ。

  1. Tokenを導入する:上限を1時間に5000リクエストまで引き上げる事ができ、またプライベートリポジトリでも更新通知が実装出来ると思います。未検証ですが、トークンのScopeはrepoのみで良いようです。
  2. GitHub APIを使わない:APIを使うのは、プラグイン更新に必要な「新バージョン番号」「zipのダウンロードurl」を取得するためですので、自前で更新情報を記述したjsonファイルをホストして、そのファイルを参照する方法です。タグ付けをトリガーとしたGitHub Actionsを組むのも良いかもしれません。

更新情報を表示するモーダル表示に関する問題

プラグインに更新通知が表示されると、その中に「View version X.X.X details 」というリンクが貼られ、クリックすると指定したURLのページがモーダルウィンドウで表示されます。

これまでのコードでは特に指定していませんでしたが、その場合は以下のように同じページがiframeで表示されてしまいます。

更新情報を表示するモーダル

このURLは、以下のようにurlキーで指定する事が出来ます。

return array(
	'version'     => $plugin_data['Version'],
	'new_version' => $new_version,
	'package'     => $package,
	'url'         => 'https://github.com/taro/my-plugin/releases', // 「View version X.X.X details」のリンク先
);

GitHubのリリース一覧ページでも指定しておけばよいかと思ったのですが、ブラウザコンソールに以下のようなエラーが表示され埋め込む事が出来ませんでした。

Refused to frame 'https://github.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'none'.

これは、GitHubのコンテンツセキュリティポリシー(CSP)が原因のようです。

GitHub’s CSP journey | The GitHub Blog

おそらく、CSPを適切に設定した独自ページを指定すればよいと思うのですが、あまり詳しくないため、知見をお持ちの方がいましたらコメントお待ちしております。