不安定と言われているWordPressのコンテンツ埋め込み(oEmbed)についてのメモ

12月16日にWordPress5がリリースされ、新エディタ「Gutenberg」となりましたが、まだ未成熟な部分もあって嫌だ、または、いままでのエディタ感覚のまま使い方をしたいなど色々な理由で、Classicエディタ「TinyMCE」を継続して利用しておられる方も多いと思います。筆者もその一人です。下記、Classicエディタ「TinyMCE」利用を前提としてのメモになります。

本題に入りますが、WordPressの管理画面より記事投稿の際、TinyMCE上でURLを入力すると、そのURLのサイトがoEmbed (「おーえんぶど」と読むそうです)に対応していれば、埋め込み用のコードが差し込まれます。しかしながら、これが差し込まれない場合もあって、また、TinyMCE上では差し込まれてないけど、公開ページでは差し込まれてたり、ネットで言われているとおり不安定な動きを確認しつつ、筆者も放置していました。この度、この奇っ怪な動きを調べる機会があったので、そのメモをご紹介します。

原因その1: 自分のネットワーク構成

自分のホームページから、同じWebサーバに共存するバーチャルドメインのホームページへのoEmbedが動作しない原因のひとつ。

WordPressで構築したホームページをインターネット上に公開する場合、上記のようなネットワーク構成のパターンがあると思います。インターネット層(external-zone)とWebサーバ層の(internal-zone)の間に、ファイアウォールを挟み、internal-zoneのWebサーバにはIPv4のプライベートIPアドレスを割り振るパターンです。当然このパターンでは、external-zoneからinternal-zoneに流れるIPv4のIPパケットはファイアウォールでNAT変換しています。

しかしながら、internal-zoneのWebサーバからexternal-zoneのグローバルIPアドレスが付与されているインターフェースにアクセスに対応できないファイアウォールが存在します。これは、internal-zoneからexternal-zoneにバインドされたパケットはNAT変換対象外としているファイアウォールの仕様によるものですが、その場合、自分のWebサーバから自分のWebサーバのexternal-zoneのグローバルIPアドレスには、アクセスできないという状況になってしまいます。これによって、oEmbedの埋め込み用のコード取得に失敗し、結果、埋め込みされない結果になってしまいます。

通常、このパターンのネットワーク構成では、内部DNSやHOSTSファイルなどを利用して、自分のWebサーバから自分のWebサーバにアクセスする際の名前解決はプライベートIPアドレスで行えるようにしますが、それでも、結果、oEmbedの埋め込み用のコード取得に失敗し、結果、埋め込みされない結果になってしまいます。その理由は、oEmbedの埋め込み用のコード取得は、wp-includes/class-wp-embed.php内のshortcodeメソッドで、wp_oembed_get関数で取得しています。このwp_oembed_get関数がHTTP通信でHTMLを取得する際に「reject_unsafe_urls」が真(true)とされ、オブジェクトWP_Httpのrequestメソッドで取得しています。「reject_unsafe_urls」が真(true)の場合、wp_http_validate_url関数にてURLを厳密にチェックしているのですが、そのチェックの一つに、バリデイト対象のURL内のホスト名を名前引きして、それが、プライベートIPアドレスだったらNGとしています

	if ( ! $same_host ) {
		$host = trim( $parsed_url['host'], '.' );
		if ( preg_match( '#^(([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)$#', $host ) ) {
			$ip = $host;
		} else {
			$ip = gethostbyname( $host );
			if ( $ip === $host ) // Error condition for gethostbyname()
				$ip = false;
		}
		if ( $ip ) {
			$parts = array_map( 'intval', explode( '.', $ip ) );
			if ( 127 === $parts[0] || 10 === $parts[0] || 0 === $parts[0]
				|| ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] )
				|| ( 192 === $parts[0] && 168 === $parts[1] )
			) {
				// If host appears local, reject unless specifically allowed.
				/**
				 * Check if HTTP request is external or not.
				 *
				 * Allows to change and allow external requests for the HTTP request.
				 *
				 * @since 3.6.0
				 *
				 * @param bool   false Whether HTTP request is external or not.
				 * @param string $host IP of the requested host.
				 * @param string $url  URL of the requested host.
				 */
				if ( ! apply_filters( 'http_request_host_is_external', false, $host, $url ) )
					return false;
			}
		}
	}

幸いにも、上記のとおり、「http_request_host_is_external」というフィルターがあるので、$hostが自分のホームページのWebサーバに共存するホームページURLだったら真(true)を返してあげればこのケースの現象は直りますが、重要なセキュリティの要のフィルターのひとつなので、注意が必要です。

自分のホームページで、自分のホームページへのoEmbedの場合は、wp_oembed_get関数から呼び出しているWP_oEmbedのget_htmlメソッドで、HTTP通信せずに、「pre_oembed_result」フィルターにapply_filtersされているwp_filter_pre_oembed_result関数で、url_to_postid関数などを使って、埋め込みコードを取得しています。

原因その2: アクセス制御・BASIC認証など

oEmbedが動作しないの原因のひとつに、「原因その1:自分のネットワーク構成」でも触れましたが、HTTP通信で埋め込むコードを取得しにいってるので、その先がアクセス制御やBASIC認証などの制限でHTTPアクセスができない状況である場合は、埋め込みできないということになります。ポイントは、手持ちのブラウザから対象先のURLにHTTP通信できるかということではなく、Webサーバから対象先のURLにHTTP通信できるかというところです。

原因その3: SSL/TLS

SSL/TLSが原因で、oEmbedが動作したりしなかったりするケースがあります。これは、Classicエディタ「TinyMCE」側でURLを検知し、それをJavaScriptから/wp-admin/admin-ajax.phpに、oEmbedの埋め込むコードを取得する命令を伝えているのですが、その命令をadmin-ajax.phpで受け取って、/wp-admin/includes/ajax-actions.phpのwp_ajax_parse_embed関数が動いています。そのwp_ajax_parse_embed内に下記のコードがあります。

	if ( ! empty( $no_ssl_support ) || ( is_ssl() && ( preg_match( '%<(iframe|script|embed) [^>]*src="http://%', $parsed ) ||
		preg_match( '%<link [^="">]*href="http://%', $parsed ) ) ) ) {
		// Admin is ssl and the embed is not. Iframes, scripts, and other "active content" will be blocked.
		wp_send_json_error( array(
			'type' =&gt; 'not-ssl',
			'message' =&gt; __( 'This preview is unavailable in the editor.' ),
		) );
	}

ここでの注釈(コメント)のとおり、こちらがSSL/TLSなのに、いまから埋め込もうとしているiframeのコードが非SSL/TLSだった場合、埋め込みをブロックします。まあ、当たり前のことですが、確かに、SSL/TLSなページ内で非SSL/TLSの部品は表示できませんからね。

まとめますとこのパターンでoEmbedが動作しないケースは下記のような感じになると思います。

  1. 自分のホームページで、管理画面(通常:/wp-admin/)だけSSL/TLS化して、一般公開のページは非SSL/TLS化のケース (このケースの場合は、管理画面のTinyMCEではoEmbedは動作しませんが、一般公開のページでは動作します)。
  2. 自分のホームページが完全SSL/TLS化しているけど、oEmbedの先が非SSL/TLSのケース。

以上ですが、不安定と言われているWordPressのコンテンツ埋め込み(oEmbed)。WordPressはぜんぜん悪くなく、自分が不安定ということを嫌という程、知らされました(笑)


その他: キャッシュについて

oEmbedはHTTP通信で埋め込むコードを取得していますが、ボトルネックが激しいので、WordPressでは、その記事毎にpost_meta形式でキャッシュ保存しています。具体的には、/wp-includes/class-wp-embed.phpのWP_Embedオブジェクトのshortcodeメソッドです。

		/**
		 * Filters the oEmbed TTL value (time to live).
		 *
		 * @since 4.0.0
		 *
		 * @param int    $time    Time to live (in seconds).
		 * @param string $url     The attempted embed URL.
		 * @param array  $attr    An array of shortcode attributes.
		 * @param int    $post_ID Post ID.
		 */
		$ttl = apply_filters( 'oembed_ttl', DAY_IN_SECONDS, $url, $attr, $post_ID );

		$cache      = '';
		$cache_time = 0;

		$cached_post_id = $this-&gt;find_oembed_post_id( $key_suffix );

		if ( $post_ID ) {
			$cache = get_post_meta( $post_ID, $cachekey, true );
			$cache_time = get_post_meta( $post_ID, $cachekey_time, true );

			if ( ! $cache_time ) {
				$cache_time = 0;
			}
		} elseif ( $cached_post_id ) {
			$cached_post = get_post( $cached_post_id );

			$cache      = $cached_post-&gt;post_content;
			$cache_time = strtotime( $cached_post-&gt;post_modified_gmt );
		}

		$cached_recently = ( time() - $cache_time ) &lt; $ttl; if ( $this-&gt;usecache || $cached_recently ) {
			// Failures are cached. Serve one if we're using the cache.
			if ( '{{unknown}}' === $cache ) {
				return $this-&gt;maybe_make_link( $url );
			}

			if ( ! empty( $cache ) ) {

上記のように、このキャッシュのTTLは「DAY_IN_SECONDS」つまり1日ですので、開発中に誤ったoEmbedの埋め込みコードを取得した場合、1日はキャッシュが効いて変わりません。このキャッシュを削除するには、下記のようなSQL文で強制的に削除することもできます。

delete from wp_postmeta where meta_key like '_oembed_%';

wp_の部分はPrefixですので、WPインストールの際に指定したものをご利用ください。また、SQL文の操作になりますので、WordPressが壊れないように、事前にバックアップや入念にチェックした上で行うようにしてください