WordPress の REST API の出力内容をフィルタリング (プラグイン編)

経緯

WordPress の REST API で出力されるデータを GETリクエストのパラメータでフィルタリングすると、クライアント側で指定する URL がごちゃごちゃするので WordPress 側で制御できないかと考えました。

「REST API にアクセスされた際に、レスポンスデータを出力する直前」のタイミングという、そこそこカスタマイズのニーズがありそうなポイントではアクションフックが用意されていそうなので、「アクションフックで余計な出力が出ないように制限する」ということはできそうな気がします。

調査

調べてみたらあったのでこの方法で考えます。ついでにアクションフックは rest_prepare_post が良さそうです。

実装

<?php
/*
Plugin Name: Filter REST
Description: REST API の出力結果をフィルタリングするプラグイン (パーマリンク設定は「基本」または「数字ベース」にしてください)
Version: 0.0.1
Author: アルム=バンド
*/

/**
 * Filter REST : REST API の出力結果をフィルタリングするプラグイン
 */
class FilterREST
{
    /**
     * __construct : プラグイン有効時にメソッドを呼ぶ
     *
     */
    public function __construct() {
        // 有効化の際に発動
        add_action(
            'rest_prepare_post',
            [
                $this,
                'remove_data',
            ],
            10,
            3
        );
    }
    /**
     * remove_data                : _links や不要な値を非出力にする
     *
     * @param {Object} $response  : レスポンスとして返却するデータ
     * @param {Object} $post      :
     * @param {Array} $request    : リクレストデータ
     *
     * @return {Object} $response : レスポンスとして返却するデータ
     *
     */
    public function remove_data ( $response, $post, $request )
    {
        // embed用の要素の削除
        unset( $response->data['type'] );
        unset( $response->data['link'] );
        unset( $response->data['excerpt'] );
        unset( $response->data['author'] );
        unset( $response->data['featured_media']); // アイキャッチだが即座に画僧のパスを取得できるものではないので削除
        unset( $response->data['date_gmt'] );
        unset( $response->data['modified_gmt'] );
        unset( $response->data['guid'] );
        unset( $response->data['excerpt'] );
        unset( $response->data['comment_status'] );
        unset( $response->data['ping_status'] );
        unset( $response->data['sticky'] );
        unset( $response->data['template'] );
        unset( $response->data['format'] );
        // _links の全要素を取得
        $links = $response->get_links();
        // 要素を foreach で全て削除
        foreach ( $links as $key => $value ) {
            $response->remove_link( $key );
        }
        return $response;
    }
}

// instantiate
if( !is_admin() ) {
    // 管理者画面以外
    $filterrest = new FilterREST();
}

ブロックエディタは REST API を使用していますし、他のプラグインでも使用しているケースがあるので、ログインしていない場合のみ絞る方向で調整してみようかと。

それから、一応クラスにしてなるべくグローバルスコープを汚染しないようにしておきます。ついでにプラグインとして実装してみました。

応用

ついでに「本文内にある href 属性内の URL がサイト内部へのリンクだった場合は置換をする」ということもやってみます。

// 略

class FilterREST
{
    /**
     * __construct : プラグイン有効時にメソッドを呼ぶ
     *
     */
    public function __construct() {
        // 有効化の際に発動
        add_action(
            'rest_prepare_post',
            [
                $this,
                'remove_data',
            ],
            10,
            3
        );
        add_action(
            'rest_prepare_post',
            [
                $this,
                'url_replace',
            ],
            11,
            3
        ); // 追加
    }

    // 略

    /**
     * url_replace                : content のリンクを書き換える
     * 
     * @param {Object} $response  : レスポンスとして返却するデータ
     * @param {Object} $post      :
     * @param {Array} $request    : リクレストデータ
     *
     * @return {Object} $response : レスポンスとして返却するデータ
     * 
     */
    public function url_replace ( $response, $post, $request )
    {
        $search_pattern = '/href=\\"([^"]*?)\\"/i';
        $response->data['content']['rendered'] = preg_replace_callback(
            $search_pattern,
            [
                $this,
                'replace_process',
            ],
            $response->data['content']['rendered']
        );
        return $response;
    }
    /**
     * replace_process          : 外部サイトへのリンクはそのままで、それ以外は置換
     * 
     * @param {string} $matches : url_replace で $search_pattern にマッチした文字列
     * 
     * @return {string}         : 置換された文字列
     * 
     */
    public function replace_process ( $matches )
    {
        $siteurl    = site_url();
        $static_url = 'https://example.com/releasenote/'; // 何かしらの静的サイトの更新履歴の個別記事ページを想定
        if( strpos( $matches[1], $siteurl ) === false ) {
            // サイト外へのリンクならばリンク文字列をそのまま返す
            return $matches[0];
        }
        else {
            // サイト内リンクならば置換する
            if( strpos( $matches[1], '?p=' ) !== false ) {
                // 「基本」
                $id = explode( '?p=', $matches[1] )[1];
                return 'href="' . $static_url . $id . '"';
            }
            else if( strpos( $matches[1], 'archives/' ) !== false ) {
                // 「数字ベース」
                $id = explode( 'archives/', $matches[1] )[1];
                return 'href="' . $static_url . $id . '"';
            }
            else {
                // ルールに該当しない場合はブランク
                return 'href="#"';
            }
        }
    }
}

// 略

こんなことをすると、同じ WordPress の投稿記事のどれかの記事へのリンクは https://example.com/releasenote/{記事ID} という形で置換されます。投稿を静的サイト側の JS でごにょごにょしたい……という場合には使えるかもしれません。

参考

WordPress の REST API

ブロックエディタは REST API を使用していますし、他のプラグインでも使用しているケースがあるので、ログインしていない場合のみ絞る方向で調整してみようかと。

なお、アクションフックとしては rest_api_init, rest_endpoints, rest_prepare_post 等がある模様。

不要な出力を削除

今回は使わなかったが今後使いそうなもの

カテゴリの詳細情報も取得

アイキャッチ画像の情報を取得

PHP

正規表現

正規表現や後方参照について。念のため。

一部に文字列を含むか

WordPress テンプレートタグ

たまにだと site_url()home_url() か迷うので。

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。