なんと実環境でサイトデザインを中心にメンテ中...しばらく読みづらいかも知れません。
{初めての}WordPressプラグインを作りました。[CF7cleaner]

{初めての}WordPressプラグインを作りました。[CF7cleaner]

2022年4月6日

碌に記事こそ書かないのに、万年工事中・時々建て直されるサグラダファミリアの様なこのサイトですが、またもや運用サーバーが変わりました。現在はVPSを借りて、KUSANAGI9というCent OS Stream 8ベースのサーバーOSで運用中です。この辺の話はまた別の機会(いつ?)にするとして、これを機にWordPressの高速化に興味が湧いて色々考えて、今回はとある目的のプラグインを作ってみる流れになりました。

なお、この記事はREADME何書けばいいのか悩んだ結果でもあるので今回作ったプラグインのREADMEを兼ねるものになると思います。

Sponsored Link

CF7 JS&CSS Cleaner

Clean JS&CSS of Contact Form 7 on Pages other than Form page. – Hodokami/CF7clea…
github.com

今回作成したプラグインはGPL 3.0ライセンスのオープンソースソフトウェアとしてGitHubで公開しております。記事内のコードは概ね開発が落ち着いたバージョン0.5からの引用(コメントは変更)です。また、略称としてCF7 JS&CSS CleanerをCF7cleaner、Contact Form 7をCF7と呼んでます。

プラグインの目的

Contact Form 7というWordPressプラグインをご存知でしょうか。WordPressサイトを構築した経験がある方なら名前くらいは聞いたことのあるプラグインかと思いますが、所謂「お問い合わせフォーム」を簡単に作成できるプラグインでして、このサイトでも以下のページで採用しております。

tellaresdo.com

このプラグイン、簡単に多種多様なメールフォームが作成できて、WordPress内のさまざまな場所に設置できる上、Google reCAPTCHA v3を利用したスパム・ロボット対策や外部サービス連携など非常に高機能なのですが、この高機能の代償としてCF7の動作に必要なJavaScriptやCSSを全てのページで読み込んでしまうという欠点があります。

特にレスポンスが重視されるトップページに於いてフォームが存在しないにも関わらずこれらを読み込んでしまうことでレスポンスの低下が発生してしまいます。実際にGoogleのPageSpeed Insightsを利用し、計測すると、この読み込みが無駄な読み込みとして警告されてしまいます。(まあ、必ずしもこの測定の通りにすればいいわけではないと思いますし、JavaScriptの大半がAdsense等のGoogle産というのが実態なんですが……)

pagespeed.web.dev

そんなわけで、とりあえずレスポンス改善の第一歩として、この欠点の改善に踏み切ったというわけです。

動作環境

文字通り動作した環境です。他の環境でテストしたわけでもなければ、これじゃないと動かないというわけでもない。多分割と前のバージョンでも動きます。ただCF7はreCAPTCHA v3があるバージョンじゃないとダメだと思います。

  • PHP 8.0
  • WordPress 5.9
  • Contact Form 7 5.5.6

使用方法

基本的に以下の3ステップで導入可能です。こんな適当なプラグインWordPress.orgに登録申請なんかできないのでGitHubからのダウンロードのみでの提供です。

  1. 貴サイトでCF7が有効であることを確認 (この操作が必要な理由)
  2. GitHubからダウンロードしたフォルダをWordPressのプラグインフォルダに設置
  3. プラグインページからCF7cleanerの有効化
  4. フォームが設置されている全ての投稿・固定ページの編集画面を開いて更新 (この操作が必要な理由)

実際のPHPプログラミング

要件

そもそも、この目的だけであればプラグインを作る必要性はありません。この問題はCF7の開発側も認識しており、CF7公式に対処法が存在します。

こちらに書かれてあるとおり、使用しているテーマのfunctions.phpとお問合せフォーム専用のphpに指定のコードを書き込めば終わりです。が、

  • テーマの変更や更新をする場合
  • そもそも”お問合せフォーム専用のPHP”なんてものが無い

なんて場合はどうでしょう。テーマを変更したり更新した場合、functions.phpの書き込み作業はやり直しです。(一応「子テーマ」で回避もできますが。)また、お問合せフォームのテンプレートが無い場合はこの方法自体実行できません。それに、せっかくどんな場所にもフォームを設置できるプラグインなのに、指定のPHPを適用してないページでは読み込まれないというのでは意味がありません。

後者に関してはコードを改良し、function.phpに書き込めば適用できるようにしている方もいらっしゃいますし、調べたかんじそれが主流です。ただ、とにかく私はfunctions.phpの書き込み作業と、フォームの追加設置やテーマの変更の度にこの作業を繰り返す必要性を避けたかったわけですね。

長くなりましたが以上を踏まえてこのプラグインが満たすべき最低限の要件は

  • Contact Form 7プラグインの有効状態の確認
  • ページ内に設置されたフォームの有無を確認
  • フォームの有無でJSとCSSの読み込みを切り替え

となるわけです。

Contact Form 7プラグインの有効状態の確認

とりあえずこのプラグインはCF7のためのプラグインのためその動作は既にそのWordPressサイトでContact Form 7が有効になっていることが前提となります。(一応、CF7が無い状態で動かしても”何もしない”だけなんですけどね。)これを実現するのが、以下のコードです。

add_filter('pre_update_option_active_plugins', 'cleaner_load_before_cf7', 10, 2);
// 有効化及び無効化の直後に動作
function cleaner_load_before_cf7($active_plugins, $old_value)
{
	// プラグインのパスを取得
	$cf7cleaner = str_replace(wp_normalize_path(WP_PLUGIN_DIR).'/', '',wp_normalize_path(__FILE__) );
	$contactform7 = wp_normalize_path('contact-form-7/wp-contact-form-7.php');
	$cf7cremoved = 0;
	foreach ($active_plugins as $no=>$path)
	{
		if ($path == $cf7cleaner) // 有効状態なら一度無効化
			{
				unset($active_plugins[$no]);
				$active_plugins = array_values($active_plugins);
				// 配列の整列
				$cf7cremoved = 1; // 無効化した事実を記録
				break;
			}
	
	}
	unset($no);
	if ($cf7cremoved == 1) // 無効化した事実が存在 = 有効化されていた
	{
		foreach ($active_plugins as $no=>$path)
		{
			if ($path == $contactform7) // Contact Form 7の起動順を探す
			{
				array_splice($active_plugins, $no, 0, $cf7cleaner); // CF7 の前に CF7cleaner を挿入
				break;
			}
		}
	}
	$active_plugins = array_values($active_plugins); // 配列の整列
	return $active_plugins;
}

プラグインの有効化・無効化時に動作して

  1. CF7cleanerの有効化を確認。
  2. 有効化されていれば一度無効化。無効化した事実を記録。
  3. 無効化した事実がある時、有効化されているCF7を探す。
  4. CF7が見つかったらその直前に読み込まれるようにCF7cleanerを有効化

といった流れです。わかる人にはわかると思うんですが非常に危うい実装をしてます……テヘッ。あと、有効化直後に無効化されてしまうのでCF7が有効になっていないサイトではCF7cleanerはそもそも有効にできません。このプラグイン改造する人、この部分真似しない方がいいよ。

この関数を作成するにあたり、一度有効化したら二度と無効化できないゾンビプラグインが出来上がったり、WordPress自体を何度か壊してしまいました。今考えてみれば別にCF7の前にCF7cleanerを読み込ませるのは必須で無い気がしますがまあいいでしょう。何はともあれこれで1つ目の要件は満たせました。

ページ内に設置されたフォームの有無を確認

これなんですが、CF7がどんなページに設置されても認識を実現するためには本文から

[contact-form-7 id="1234" title="お問合せ"]

の様なCF7特有のショートコードの有無を検出するほかありません。ということでPHPのpreg_match関数を用いた本文検索をかけるわけですが、これをページを読む度に実行していてはせっかくの高速化が台無しです。そういった事情から、この確認は「記事の更新時」に実行することにしました。

このため、既にフォームが設置されているWordPressサイトでこのプラグインを使用する場合、既にフォームが設置されている全ての記事の編集画面に行き、更新ボタンを押す必要があります。この時のフォームの有無の確認を実現するのが以下のコード。

add_action('save_post', 'cf7cleaner_checker', 10, 1);
// 記事更新時に動作
function cf7cleaner_checker($post_ID) // オプション変更用
{
	$cf7csetting_ID = wp_is_post_revision($post_ID);
	if(false === $cf7csetting_ID) $cf7csetting_ID = $post_ID; // 投稿がrevisionの時親投稿のID、そうでない時普通にIDを取得。
	$cf7cID = cf7c_get(); // 現在のオプション内容を取得
	$init_cf7c = array(); // 初期設定用の空配列
	if(false === $cf7cID) cf7c_set($init_cf7c); // オプションの存在確認
	$cf7c_optscan = in_array($cf7csetting_ID, $cf7cID); // オプションからIDを検索
	$cf7cmatching_content = get_post($cf7csetting_ID)->post_content; // 記事本文を取得
	$cf7_match = preg_match('/\

エラー: コンタクトフォームが見つかりません。

/', $cf7cmatching_content); // CF7を本文から探す if (1 === $cf7_match && false === $cf7c_optscan) $cf7cID[] = $cf7csetting_ID; // 見つかった場合、IDがオプションになければ追加。 if (0 === $cf7_match && false !== $cf7c_optscan) foreach ($cf7cID as $no=>$ID) if ($ID == $cf7csetting_ID) unset($cf7cID[$no]); // 見つからないにも関わらずIDがオプションに存在したら削除 $cf7cID = array_values($cf7cID); // 配列の整列 cf7c_set($cf7cID); // 新しいオプション内容を登録 }

動作の大枠としては記事更新時に動作して

  1. この投稿のIDを取得。
  2. このプラグインに関するオプションを取得。
  3. 本文からCF7のショートコードを検索し、その有無を確認。
  4. ショートコードの有無とオプション内のIDを照合し、オプションを更新。
  5. 更新したオプションを適用。

といった流れです。なお、コード内に無いですがオプションの取得と更新にはWordPress内蔵のget_option関数とupdate_option関数を用いてます。

フォームの有無でJSとCSSの読み込みを切り替え

フォームが設置されているページがわかったので漸く実際にページを開いたときの動作に入ります。ただし、この項目では先に決めた要件以上に色々なことをしております。オープンソースのソフトウェアを使用するときはいつだってそうですが、このプラグインの使用は自己責任でよろしくお願いします。

add_action('wp_enqueue_scripts', 'enable_cf7_jscss', 21);
// ページの読み込み時に動作
function enable_cf7_jscss()
{
	add_filter( 'wpcf7_load_js', '__return_false' ); // JSの読み込み停止
	add_filter( 'wpcf7_load_css', '__return_false' ); // CSSの読み込み停止
	$nowID = get_the_ID(); // このページのIDを取得
	$cf7cID = cf7c_get(); // 現在のオプション内容を取得
	$is_cf7page = in_array($nowID, $cf7cID); // オプション内のIDの有無を確認
	if (true === $is_cf7page) // あるとき
	{
		if (function_exists('wpcf7_enqueue_scripts')) wpcf7_enqueue_scripts(); // JSを読み込み
		if (function_exists('wpcf7_enqueue_styles')) wpcf7_enqueue_styles(); // CSSを読み込み
		wp_enqueue_style('reCAPTCHA_masker', plugins_url('reCAPTCHA_masker.css', __FILE__)); // reCAPTCHAのマークを隠す
		add_filter('the_content', 'add_PPToS_tags', 10, 1); // 隠したマークの代わりに文章を挿入
	}
	else wp_deregister_script('google-recaptcha'); // ないときにreCAPTCHAを読み込まない
}
  1. 表示しているページのIDを取得。
  2. このプラグインに関するオプションを取得。
  3. オプションにIDが存在する(= フォームが設置されている)か確認。
  4. 有無によってJSやCSSの読み込みを変更。

といった流れでまあそんなに読み込みに負荷をかけないように動作できてると願うばかりですが、先に申し上げた通り、要件以上の以下の動作をしています。

  • フォームが有るページではreCAPTCHA v3のマークを隠す
  • 隠したマークの代わりにプライバシーポリシーと利用規約に関するリンクを投稿末尾に追加
  • フォームのないページではreCAPTCHAを読み込まない

見たことある方も居ると思うのですが、reCAPTCHAを導入しているサイトでは通常右下にreCAPTCHAを使用していることを示し、reCAPTCHAのプライバシーポリシーと利用規約を読めるマークが表示されます。

このマークは代わりにreCAPTCHAが動作しているページ全てにreCAPTCHAのプライバシーポリシーと利用規約へのリンクを設置するコードを追加するなら隠しても良いとGoogle DeveloperのFAQで回答されています。

Get answers to questions about reCAPTCHA Enterprise, versions, limits, customiza…
developers.google.com

ここまでは良いのですが、このプラグインでは、そもそもフォームの無いページではreCAPTCHA自体を読み込みません。これがGoogle的にやって良いことなのかが不明です。もしかしたらreCAPTCHAはサイト全体で有効化しなくてはいけないとされているかもしれません。その場合ルール違反なコードですね。

動かさない分にはいいだろと思いますが、AI学習目的にGoogleはreCAPTCHAが動作しているときのデータを可能な限り多く欲しがっているはずなので……。

まとめ

このようにだいぶ大雑把ですが、要点はおさえたプラグインが完成しました。GitHubのリリース機能を用いたアップデート機能などを用意しても良かったのですがこのプラグインが非常に単純であるためやめました。使う方もそうでない方も機能改善やバグ、セキュリティホールなどありましたらこのサイトのフォームかGitHubかTwitterに改善策セットで教えてくれると泣いて喜びます。

あと、コメントやTwitterで「ここはどうしてこうなってるの」といった質問も大歓迎です。ソース全文はGitHubで見れるので、新しくプラグインを作る人に参考になればいいなと思います。

このプラグイン、改造すれば「本文中に特定のコードが存在する記事でだけ特定の動作をする」プラグインが結構簡単に作れると思うので改造も歓迎です。そのためのGPLですからね。なお、GPLだからこそこれを改造した物のライセンスもGPLにしなくてはならないのでご注意を。

……ところで、本文中の”[“をソースコード表示に使用しているプラグインがUnicode Dacimal Code”[”にエスケープ処理してくれなかったのでCF7の無いこのページでも意図せずCF7cleanerがJSとCSSなどを読み込んでしまいました。対策にはプラグインの改造などが必要になり非常に面倒であったことと、この記事のみが対象となり得るのでデータベースを直接書き換えてエスケープ処理とCF7cleanerの動作対象から除外しました……手間かけさせやがって……