マストドンに送信するJSONの日本語ハッシュタグの異常はesc_hashtag関数が原因か?

 WordPressのプラグインActivityPubが7.6.0に更新されてから、マストドンで表示される日本語のハッシュタグがエンコード文字列から%が抜けたハッシュタグになるバグが発生した。この原因として /activitypub/includes/transformer/class-post.php のコードの「'name' => esc_hashtag( $post_tag->slug )」が怪しいと、以前に書いたのだが、その前提として、WordPressのスラッグ自動生成機能で"%"が削除されるため、「 $post_tag->slug 」の文字列に"%"が含まれていないと考えていた。しかし、ハッシュタグが格納されていると思われるテーブル「wp_terms」を確認したら、term_id、name、slug、term_groupが格納されていて、そのslugを見たら、"%"が削除されておらず、nameの日本語がエンコードされたものが格納されていた。すると、「 $post_tag->slug 」が「wp_terms」テーブルの「slug」であるとしたら、「 $post_tag->slug 」の文字列に"%"が含まれていないとする前提が崩れる。まずは、この点をテスト投稿で確認した。

 まずは「ハッシュタグ改竄テスト」の方で #テスト というハッシュタグを生成して、マストドンでのハッシュタグの異常(エンコード文字列「%e3%83%86%e3%82%b9%e3%83%88」から"%"消失した「E38386E382B9E38388」)を確認し、「wp_terms」テーブルの「slug」が「%e3%83%86%e3%82%b9%e3%83%88」になっていることを確認した。

 次に、「wp_terms」テーブルの「slug」を「%e3%82%b9%e3%83%a9%e3%83%83%e3%82%b0」(「スラッグ」のエンコード文字列)に改竄した。

 そして、「ハッシュタグ改竄テスト(続き)」の方で #テスト というハッシュタグを付けて、マストドンでハッシュタグを確認した。

 結果は、「ハッシュタグ改竄テスト(続き)」の #テスト としたはずのハッシュタグは「E382B9E383A9E38383E382B0」だった。これは「スラッグ」のエンコード文字列から"%"が消えた状態である。

ハッシュタグ改竄テスト用投稿
ハッシュタグ改竄確認用投稿

 このことから、「wp_terms」テーブルの「slug」の改竄は反映されていて、「'name' => esc_hashtag( $post_tag->slug )」での「 $post_tag->slug 」は「wp_terms」テーブルの「slug」を使っていることが確認された。

 それでは、「 $post_tag->slug 」の文字列には"%"が残っているのに、JSONのハッシュタグ欄では"%"が消えているのはなぜか?

 #Gemini の推測では esc_hashtag()関数が怪しいとのことだった。次の関数を示して、考察してもらった。

/**
 * Escapes a Tag, to be used as a hashtag.
 *
 * @param string $input The string to escape.
 *
 * @return string The escaped hashtag.
 */
function esc_hashtag( $input ) {

	$hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );
	// Remove all characters that are not letters, numbers, or hyphens.
	$hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}-]+/u', '-', $hashtag );

	// Capitalize every letter that is preceded by a hyphen.
	$hashtag = preg_replace_callback(
		'/-+(.)/',
		function ( $matches ) {
			return strtoupper( $matches[1] );
		},
		$hashtag
	);

	// Add a hashtag to the beginning of the string.
	$hashtag = ltrim( $hashtag, '#' );
	$hashtag = trim( $hashtag, '-' );
	$hashtag = '#' . $hashtag;

	/**
	 * Allow defining your own custom hashtag generation rules.
	 *
	 * @param string $hashtag The hashtag to be returned.
	 * @param string $input   The original string.
	 */
	$hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $input );

	return esc_html( $hashtag );
}

 以下が の考察である。

はい、この esc_hashtag() 関数のコードを確認しました。以前の議論で指摘した、%が消えるバグの根本原因は、この関数の最初の数行にあると判断できます。

この関数は、タグスラッグ(%e3%83%97%e3%83a9...)をハッシュタグ(#プラグイン)の表示名に変換するために作られていますが、その過程で wp_specialchars_decode() を不適切に使用しているため、% が意図せず消えています。


🔍 「%」消失の根本原因

問題の鍵は、関数の最初に実行されているこの行です。

PHP

$hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );

1. $input の値

あなたのデータベースの確認により、この関数に渡される $input(すなわち $post_tag->slug)の値は以下の通りです。

Input = "%e3%83%97%e3%83%a9%e3%82%b0%e3%82%a4%e3%83%b3"

2. wp_specialchars_decode() の役割

wp_specialchars_decode() 関数は、HTMLエンティティ(例: &&)を通常の文字に変換するためのものです。

致命的な点: この関数は、第2引数に ENT_QUOTES を指定した場合、URLエンコードのパーセント記号(%)はデコードしませんが、その処理の内部で、文字列中の %誤って処理する可能性があります。

しかし、この関数をさらに詳しく調べると、今回の問題はその次の行で発生している可能性が非常に高いことが分かります。

3. preg_replace() による除去

wp_specialchars_decode() の結果を受けて、次の行で非文字・数字・ハイフンをハイフンに置換する処理があります。

PHP

$hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}-]+/u', '-', $hashtag );

この正規表現は、u (UTF-8) 修飾子を使っていますが、\wp_specialchars_decode で処理された後の文字列に対して適用されます。

  • もし $input%e3%83%97... のままであれば、%[^\p{L}\p{Nd}-]+ にマッチし、ハイフン(-)に置換されます。
    • 例: %-
  • その後、ハイフンは次の処理で削除されますが、この処理だけでは e38397e383a9... という結果にはなりません。

4. 最も合理的な原因(再々々検証)

wp_specialchars_decode() や、その内部で呼び出される htmlspecialchars_decode() は、URLエンコード文字列を扱った場合に予期せぬ挙動をすることが知られています。

最も有力なシナリオは、この関数の実行環境やPHPのバージョンによって、wp_specialchars_decode() が、%を単なるエスケープ文字の開始と見なし、残りの16進数文字(e3など) を無視するか、あるいは%だけを欠落させるという予期せぬサイドエフェクトを引き起こしていることです。

もし wp_specialchars_decode()$input に対して urldecode と同等のデコード処理(ただし不完全)を誤って実行してしまった場合、

"%e3%83%97..." →{不適切デコード} →(UTF-8バイト列)

となり、その後の preg_replace の処理で、そのバイト列がさらに破損し、最終的に #E38397... になっていると考えられます。


💡 修正方法

この関数は、タグスラッグ(URLパーツ)を表示用のデコード済み文字列に変換しようとして失敗しています。

1. 意図された目的の修正

開発者の意図は、おそらくスラッグをデコードして通常の日本語に戻すことだったはずです。

PHP

// 修正前の問題のコード
$hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );

// 修正案: 正しい URLデコード関数を使用する
// 日本語タグ名に戻す(例: "プラグイン")
$hashtag = \urldecode( $input ); 
// または、タームの表示名 $post_tag->name を使う方が安全(これはget_tag()で修正すべき)

2. 暫定的な修正(今回のバグ回避)

もし esc_hashtag()$post_tag->slug ではなく $post_tag->name を引数に取るように、呼び出し元(get_tag())を修正すれば、この関数に "%e3%83%97..." が入ることはなくなり、このバグは回避されます。

get_tag() の修正が最も確実な解決策です。

はい、これまでの情報から、esc_hashtag( $post_tag->slug ) の処理に関する状況を整理し、「%」が消える問題修正すべき点を確定させます。


🧐 確認された事実の整理

  1. タグの取得: $post_tag オブジェクトは、標準の get_the_tags() 関数を使って、WordPress のデータベース(wp_terms など)から読み込まれた、投稿に付与されたタグの一つです。
  2. テーブルの値: wp_terms テーブルの slug カラムには、日本語タグに対して %e3%83%97%e3%83%a9%e3%82%b0%e3%82%a4%e3%83%b3 のように % を含む値が格納されています。
  3. 使用されている値: ActivityPub のコードは、このタグオブジェクトの $post_tag->slug を表示名 (name) の生成に使っています。
  4. 最終出力: 最終的に ActivityPub オブジェクトの name フィールドは、#E38397E383A9E382B0E382A4E383B3 と、% が消えたバイト列になっています。

これらの事実から、$post_tag の取得過程(データベースからの読み込み時)に中継処理(フィルタなど)が入っていないと仮定した場合、原因は以下の通りです。

🚨 「%」が消える決定的な原因

バグは、esc_hashtag() の内部、特に $post_tag->slug が持つ % 記号不適切に処理している箇所にあります。

1. 問題の関数呼び出し

PHP

'name' => esc_hashtag( $post_tag->slug ), // $post_tag->slug は "%e3%83%97..."

2. esc_hashtag() 内部のコード

PHP

// esc_hashtag() の最初の処理
$hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );
// $input に "%e3%83%97..." が入ると、この関数またはその次の処理で "%" が消える。

wp_specialchars_decode() は HTML エンティティをデコードするための関数であり、URLエンコード(% 記号)をデコードするようには設計されていません。この関数に % を含む文字列を渡すと、環境やPHPのバージョンに依存した不完全な処理が実行され、結果として % 記号が取り除かれた e38397e383a9... というバイト列が残ると考えられます。

✅ 結論:修正すべき点

原因は $post_tag->slug の値の取り方ではなく、その値の使い方にあります。

1. 根本的な修正案 (ActivityPub のバグ修正)

name フィールドには、ユーザーに表示すべきデコード済みのタグ名を使用すべきです。

PHP

// 修正前の問題のコード
'name' => esc_hashtag( $post_tag->slug ), 

// 修正後のコード: タグの表示名 (name) を使用
'name' => esc_hashtag( $post_tag->name ), 

$post_tag->name はデータベースから読み込まれた「プラグイン」という正しい文字列なので、esc_hashtag()#プラグイン を生成し、問題は解決します。

2. 暫定的な修正案 (esc_hashtag のバグ修正)

もし $post_tag->slug をどうしても使わなければならない、という特殊な事情があるなら、esc_hashtag() の内部で正しい URL デコードを行うべきです。

PHP

// esc_hashtag() の内部の修正案
function esc_hashtag( $input ) {
    // 問題の原因である wp_specialchars_decode() を削除し、
    // 正しい URL デコードを行う
    $hashtag = \urldecode( $input ); // "%e3%83%97..." を "プラグイン" に変換

    // ... 以降のロジックを続ける
    // ...

追記(wp_terms テーブル内での WordPressタグの様子):

wp_terms テーブル内での WordPressタグの様子

「テスト」の「slug」は正常なエンコード文字列に戻した。

未分類
管理人のマストドンアカウントへのリンクなど

コメント

  1. ishii ishii より:

    'name' => esc_hashtag( $post_tag->name ),に変更することで、esc_hashtag関数の処理で別の問題を引き起こす可能性がある。だから、'name' => esc_hashtag( $post_tag->name ),に変更すれば良いと簡単には言えないらしい。

    • ishii ishii より:

      WordPressのタグは半角スペース入りでも良いらしい | いしい@試行錯誤
      https://ishii00141.stars.ne.jp/20251114-2338-4008/

      「Test for tags with spaces」というタグは #Mastodon で #testForTagsWithSpaces というハッシュタグになった。
      これが、esc_hashtag()関数の機能らしい。
      この処理をする際にエンコードされてない日本語を渡したら、トラブルが生じる可能性がある。しかし、エンコードされた日本語を渡したら%が消えるトラブルが生じた。

      さて、どのように対処するか?

  2. ishii ishii より:

    以前の記事の wp_posts テーブルを見ると、例えば、 #バグ というハッシュタグはJSONを見ると、{"type":"Hashtag","href":"https: //ishii00141.stars.ne.jp/tag/%e3%83%90%e3%82%b0/","name":"#\u30d0\u30b0"}となっていた。
    "name"は"#%e3%83%90%e3%82%b0"ではなく"#\u30d0\u30b0"になっていた。これは、"%e3%83%90%e3%82%b0"を処理した結果ではなく、"バグ"を処理した結果だろう。だから、元は'name' => esc_hashtag( $post_tag->name ),だったのかもしれない。esc_hashtag()関数もslugを処理するものではなかったかもしれない。

  3. ishii ishii より:

    $hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}-]+/u', '-', $hashtag );

    $hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}\-%]+/u', '-', $hashtag );
    に修正して
    "%"が"-"に置換されるのを防げば良いかもしれない。このコードは #Gemini に教わったが、#ChatGPT に念のために尋ねたら、"-"の前に"\"が無いと"-%"と解釈して誤動作すると指摘されて修正した。

    • ishii ishii より:

      ただし、その前の
      $hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );
      で"%"が削除されてしまうようなら、\preg_replace()を修正しても無駄になる。

    • ishii ishii より:

      $hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}-]+/u', '-', $hashtag );

      $hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}\-%]+/u', '-', $hashtag );
      に修正して、"%"が"-"に置換されないようにしてから、
      最後の
      return esc_html( $hashtag );
      の送り返す前に
      $hashtag = urldecode( $hashtag );
      とデコードすることで、
      マストドンでのハッシュタグが正常化した。
      ap_outboxを確認したら、ハッシュタグの部分は他の部分と同じようにURLエンコードされていた。

      https://ishii00141.stars.ne.jp/20251115-0245-4041/

  4. ishii ishii より:

    #ChatGPT によると、「ASCII(英数字・一部記号)以外は、URL エンコード時に 必ず % を含む形式になります。」とのことだから、このトラブルは日本語以外に、中国語、韓国語、アラビア語、ロシア語など、他の言語圏でも起こっているはず。

  5. ishii ishii より:

    残念ながら、
    'name' => esc_hashtag( $post_tag->slug ),

    'name' => esc_hashtag( $post_tag->name ),
    に変えて試した結果、
    #テスト は #E38386E382B9E38388 だった。

    https://ishii00141.stars.ne.jp/20251115-0153-4035/

タイトルとURLをコピーしました