WordPressのフックを理解し「アップデートしたらサイトが壊れる」プラグインを作る

こんにちは。GMOペパボ株式会社の西田です。

WordPressは、非常に人気のあるコンテンツ管理システムの1つで、世界中のウェブサイトで利用されています。この記事では「アップデートしたらサイトが壊れる」という一風変わったプラグインを紹介しつつ、それを実現するWordPressの仕組みについて解説したいと思います。
この記事は GMOインターネットグループ Advent Calendar 2024 4日目の記事です。

アップデートしたらサイトが壊れるプラグイン

今回実装したプラグインはこちらです。
https://github.com/kinosuke01/wp-update-breaker

これをWordPress製のサイトにインストールすると、コントロールパネルにアップデート通知が出ます。そこで「更新」をクリックしてプラグインをアップデートすると。

以下のように、サイトの表示が著しく崩れます。

なぜ作ったのか?

ある社内業務にて「WordPressのプラグインをアップデートしたらサイトが壊れた」を再現する必要があったためです。

WordPressのプラグインアップデートはワンクリックで済むことが多いですが、サイトが壊れたときの復旧が大変という側面があります。なので、WordPressを運用管理している企業の社員研修で、サイトが壊れたときの訓練用途としても使えるかもしれません。

どのように設計したのか?

一般的にWordPressプラグインのアップデートは以下のような流れで実現されます。

このアップデートを仕組みに載るには、wordpress.orgのプラグイン登録審査が必要になります。しかし、社内業務のためだけに、WordPress公式からの承認を得る必要性については検討する必要がありました。

そこで今回は「このプラグインに関してはアップデートのフローが変わるように、プラグイン自体で拡張する」というアプローチを取りました。プラグインのホスト先にはGithubPagesを用いることで、コストもかからないようにしました。

あとは、新しいバージョンのプラグインにのみ、レイアウトを崩すCSSを仕込んでおけば、「アップデートしたらサイトが壊れる」プラグインが完成します。

どのように実装したのか?

ここから実装したコードを見ていきますが、その前にWordPressのフックについて説明をしたいと思います。WordPressはブラウザからのアクセスを契機に複数の処理が実行され、htmlとしてレスポンスを返します。この複数の処理には、それぞれ「フック」が設けられており、プラグインは任意のフックに対して関数を追加することで、WordPressの挙動を拡張することが可能になります。

プラグインアップデートに関する実装は、以下のようになりました。

<?php
if ( ! function_exists( 'get_plugin_data' ) ) {
    require_once( ABSPATH . 'wp-admin/includes/plugin.php' );
}

class WP_Update_Breaker_Updater {
    private $plugin_data;
    private $api_url;
    private $plugin_slug;
    private $version;

    public function __construct() {
        $plugin_file = WP_PLUGIN_DIR . '/wp-update-breaker/wp-update-breaker.php';
        $plugin_data = get_plugin_data($plugin_file);

        $this->plugin_slug = strtolower(str_replace(' ', '-', $plugin_data['Name']));
        $this->version = $plugin_data['Version'];
        $this->api_url = $plugin_data['UpdateURI'];

        add_filter('site_transient_update_plugins', [$this, 'check_for_plugin_update']);
        add_filter('plugins_api', [$this, 'plugin_info'], 10, 3);
    }

    public function check_for_plugin_update($transient) {
        if (empty($transient->checked)) {
            return $transient;
        }

        // Check cache
        $cache_key = 'wp_update_breaker_api_response';
        $api_response = get_site_transient($cache_key);

        if ($api_response === false) {
            // Only request API if cache does not exist
            $response = wp_remote_get($this->api_url);
            if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
                $api_response = json_decode(wp_remote_retrieve_body($response), true);
                // Save cache for 24 hours
                set_site_transient($cache_key, $api_response, 24 * HOUR_IN_SECONDS);
            }
        }

        // Check update information if API response is valid
        if ($api_response) {
            if (version_compare($this->version, $api_response['version'], '<')) {
                $plugin_data = [
                    'slug'        => $this->plugin_slug,
                    'new_version' => $api_response['version'],
                    'package'     => $api_response['download_link'],
                ];

                $transient->response[$this->plugin_slug . '/' . $this->plugin_slug . '.php'] = (object) $plugin_data;
            }
        }

        return $transient;
    }

    // Provide detailed information about the plugin
    public function plugin_info($false, $action, $args) {
        if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) {
            return false;
        }

        // Retrieve detailed plugin information from the API
        $response = wp_remote_get($this->api_url);
        if (is_wp_error($response)) {
            return false;
        }

        $api_response = json_decode(wp_remote_retrieve_body($response), true);
        if (!$api_response) {
            return false;
        }

        // Return detailed plugin information
        return (object) [
            'name'          => $api_response['name'],
            'slug'          => $api_response['slug'],
            'version'       => $api_response['version'],
            'author'        => $api_response['author'],
            'download_link' => $api_response['download_link'],
            'sections'      => $api_response['sections'],
        ];
    }
}

// Start update check when the plugin is loaded
new WP_Update_Breaker_Updater();

ポイントとなるフックは「plugins_api 」と「site_transient_update_plugins」の2点です。順に見ていきましょう。

plugins_api

WordPressには、引数で渡したプラグインの情報をwordpress.org APIから取得するplugin_apiというフックと同じ名前の関数があります。これは主にプラグインの情報を管理ページで表示する目的で使われているものです。plugin_apiフックは、plugins_api関数の中で呼ばれていて、処理を上書きできるものになります。なおこの挙動は、WordPress本体のソースコードに丁寧にコメントで記載されています。

今回の実装では、自作したプラグイン名が引数でわたってきたときだけ、自身が準備したサイトにアクセスしてプラグイン情報を取得するように変更しています。

site_transient_update_plugins

このフックについて説明する前に、WordPressのtransientというキャッシュ機構について説明します。transientは、外部のAPIから取得したデータをキャッシュすることにも使用される機構で、プラグインのアップデートの有無は、 update_plugins をキーとした transient に保持されています。このtransientは get_site_transient (‘update_plugins’) のような実装で値を取り出すことが可能で、WordPressは随所でこれを呼び出して、プラグインアップデートの有り無しを判定しています。

さて、site_transient_update_pluginsフックですが、get_site_transient (‘update_plugins’)の結果に対して、変更や追記削除ができるフィルターとして機能するものになります。WordPressのソースコードの該当箇所を読むことでその挙動が把握できます。

今回の実装では、キャッシュされているプラグインアップデート情報を取り出したときに、自作プラグインのアップデート情報を追記することで、WordPressにアップデート情報を与えています。

ここまで記載したところで疑問を持った方もいらっしゃるかと思います。

「プラグインのアップデートの有無を、update_plugins をキーとしたtransient に保持しているといったが、このキャッシュを生成するときに、自作プラグインの情報を含むようにしたらよいのでは?」

その疑問はごもっともです。しかし残念ながら、このアプローチを取ることはできませんでした。キャッシュに保存する処理は wp_update_plugins という関数で実行されているのですが、そこにはhookの実装がないためです。そこでハック気味な使い方にはなるのですが、site_transient_update_plugins フックを活用したという経緯になります。

まとめ

「アップデートしたらサイトが壊れる」という一風変わったWordPressプラグインの紹介をしました。さらに、そのプラグインを紐解くことで、WordPressのフックの仕組みについても解説しました。この記事が、WordPressをより深く活用していきたい方々の一助になれば幸いです。

ブログの著者欄

西田 貴之

GMOペパボ株式会社

Webアプリケーションエンジニア。2020年9月GMOペパボ株式会社入社。ホスティングサービスや新規事業開発を主に担当。バックエンドもフロントエンドも幅広く対応でき、Webアプリケーションの開発や運用保守に取り組んでいる。

採用情報

関連記事

KEYWORD

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック