WordPressプラグインMT Style Post Nameの重複タグ問題を解決(WP3.2)

WordPressユーザーの間では有名な(たぶん)プラグインである「MT Style Post Name」。一応説明しておくと、これは、スラッグ(パーマリンク文字列)の生成をMTっぽくするプラグインである。

さて、これだけ有名なプラグインではあるが、古いためか、WP3系で使用すると、重大なバグが発生する。例えば、管理画面のJavascriptが誤動作するというバグや、全角文字のみのタグの場合投稿するたびに同名のタグがどんどん増殖してゆくというバグである。前者に対しては、カスタム投稿タイプでpermalink設定がうまくできないときの対策を無い知恵絞って考えた。 | Liglogでも対応が試みられている。本稿ではこのプログラムをさらに改良し、後者に対応してゆきたい。

本家のコードでは、単純に正規表現で全角文字を除去するだけの実装だ。一方、Liglogのコードでは、postidに置き換えられることを期待して、全角文字が含まれていた場合には問答無用で空文字を返却するという処理を行っている。確かに記事投稿の場合はこの処理で問題ない。しかし、タグやカテゴリのスラッグ生成にもここで使用されている関数が用いられているらしく、どうやらこのタグ登録時の処理に異常を来してしまうらしい。

実際にソースを確認したわけではないのだが、入力文字列から一意に決定されるスラッグ文字列を出力しなければ出力されるスラッグ文字列は入力文字列に対して1つに定まらなければ都合が悪いようだ。postidやtagidなどは可変であるため、同じ入力文字列でも異なるスラッグが出力されてしまう。このあたりの挙動が、重複タグ問題を起こしているらしい。

そこで、入力文字列に対して一意な文字列を出力すれば良いということになる。bin2hexを用いて文字列を16進数文字列化しても良いし、md5ハッシュを求めても良い。ただ、それでは桁数が増えすぎるため、今回はCRC32チェックサムを用いることにした。ソースコードを次に示す。

/*
* Plugin Name: MT Style Post Name Kai
* Description: MT Style Post Nameのタグ重複問題対応版。スラッグに強制的にタイトルのCRC32値を出力する
* Author: 449, Modified By Butaman-kun
* Plugin URI: http://pc10.2ch.net/test/read.cgi/blog/1163599919
* Version: 0.2.1
* */
add_filter('sanitize_title','sanitize_title_numalpha_only',9);
function sanitize_title_numalpha_only($title) {
    if(strlen($title) == 0){
        return '';//空文字の入力時は空文字を返す
    }elseif(mb_strlen($title) == strlen($title)){
        return $title;//2バイト文字が含まれていない場合はそのまま値を返す
    }else{
        $title_old = $title;

        //とりあえず2バイト文字を置換する処理
        $title = preg_replace('/[^%a-zA-Z0-9 \(\)_-]/', '-', $title);
        $title = preg_replace('/\-+/', '-', $title);
        $title = trim($title, '-');

        if(mb_strlen($title) == 0) {
            //何も残らなかったらCRC32のチェックサムに変換
            //(同じ入力に対して同じ出力になればいいのでmd5とかでも良い)
            $title = sprintf("%x", crc32($title_old));
        }
        return $title;
    }
}

さて、元の処理から正規表現による置換が増えたことが気になる方もおられるだろう。もともとのコードでは返却先で不正な文字列は除去されることを期待して、完璧なスラッグ文字列を出力していなかった。たとえば「あいう」という入力に対しては「—」と返却されていたのだ。しかし、これでは「すべてパーマリンクに使用出来ない文字(全角文字など)か」という判定を行うには都合が悪い。

そこで、2連続以上の連続ハイフンの除去、行頭、行末のハイフンの除去を行う処理を追加した。この時点で空文字になっていれば、それは入力文字列がすべて不正文字であったと判定できる(そう言う意味では、正規表現を使わずにハイフンを空文字に置換して判定しても良いかも知れない)。すべて全角であったと判定されたときに限り、CRC32チェックサムを返却する。

なお、入力文字列が空文字の場合のみ空文字を返却するようにしている。(さもなくば、0が返却されてしまうので)

パフォーマンスはあまり考慮していないコードだが、とりあえず適当にテストしたところ、うまく動いているようだ。

何か問題点があればご指摘願いたい。

追記 (2011.09.19)
掲載しているソースコードを差し替え。動作に変更はないが、置換処理の正規表現を減らした。

その後、ソースを確認してみた。結論から言うと、(1)タグやカテゴリのスラッグ生成ではPostの場合と異なり最初にterm_id(タグやカテゴリのID)が補完されるわけではないこと、(2)タグやカテゴリは名前ではなくスラッグによって重複確認がされていること、が誤動作の原因であった。

まず、wp-includes/formatting.php内のsanitize_title関数は、次の通りである。

/**
 * Sanitizes title or use fallback title.
 *
 * Specifically, HTML and PHP tags are stripped. Further actions can be added
 * via the plugin API. If $title is empty and $fallback_title is set, the latter
 * will be used.
 *
 * @since 1.0.0
 *
 * @param string $title The string to be sanitized.
 * @param string $fallback_title Optional. A title to use if $title is empty.
 * @param string $context Optional. The operation for which the string is sanitized
 * @return string The sanitized string.
 */
function sanitize_title($title, $fallback_title = '', $context = 'save') {
	$raw_title = $title;

	if ( 'save' == $context )
		$title = remove_accents($title);

	$title = apply_filters('sanitize_title', $title, $raw_title, $context);

	if ( '' === $title || false === $title )
		$title = $fallback_title;

	return $title;
}

これを見る限り、第1引数($title)の文字列を各フィルタ(sanitize_title_numalpha_only関数やsanitize_title_with_dashes関数など)に通した結果が空文字であれば、第2引数($fallback_title)の文字列を結果として返却するという実装になっているようだ。
次に、postのスラッグを生成する処理をwp-includes/post.phpから抜粋すると次のようになっている。

$post_name = sanitize_title($post_title, $post_ID);

したがって、sanitize_title_numalpha_only関数で空文字を返却すると、post_idがslugとして使用されるのだ。

一方で、タグやカテゴリの場合はどうなっているのだろうか。タグやカテゴリの追加処理を行うのは、wp-includes/taxonomy.php内のwp_insert_term関数のようだ。その中で、スラッグが未指定の場合は、sanitize_title関数を使用してスラッグを生成する処理がある。しかし、ここでは第2引数は指定されていない

	if ( empty($slug) )
		$slug = sanitize_title($name);

したがって、ここで$slugには空文字が代入されてしまう。これが後々誤動作を引き起こす。
次に$slugが使用されるのは次の箇所。

	if ( $term_id = term_exists($slug) ) {

ここで重複確認が行われているのだが、$slugが空の時は0になり、重複は存在しないと判断される。
このif文のelse句は以下のようになっている。

	} else {
		// This term does not exist at all in the database, Create it.
		$slug = wp_unique_term_slug($slug, (object) $args);
		if ( false === $wpdb->insert( $wpdb->terms, compact( 'name', 'slug', 'term_group' ) ) )
			return new WP_Error('db_insert_error', __('Could not insert term into the database'), $wpdb->last_error);
		$term_id = (int) $wpdb->insert_id;
	}

wp_unique_term_slug関数は、既に同じスラッグが存在していれば、ユニークになるように末尾に数字を付け足したりする関数なのだが、この場合は$slugは空文字でDB上には存在しないスラッグであり、なにもしないでそのまま空文字が返却される。この時点でまだ$slugは空文字である。ここで、スラッグが空のままデータベースに登録(ここで、増殖が起こってしまった)され、その時に振られたterm_idを取得している。

そして、次の部分でやっとこのことでスラッグにterm_idが入力される。

	// Seems unreachable, However, Is used in the case that a term name is provided, which sanitizes to an empty string.
	if ( empty($slug) ) {
		$slug = sanitize_title($slug, $term_id);
		do_action( 'edit_terms', $term_id );
		$wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) );
		do_action( 'edited_terms', $term_id );
	}

このときには既に増殖してしまった後なので、もはや後の祭りである。

ただ、不思議なのは、空文字を返さないようにすれば、上記のコードにもかかわらず正常に動作することだ。
タグ名やカテゴリ名が同じものが存在すれば、sanitize_title関数の結果とスラッグが異なっていても、重複したタグが生成されることはない。上記のコードの所々で空文字かどうかというのが判定基準に使われているところがあり、それが関係しているのだろう。
とりあえずうまく動いているのでこれ以上深追いはしないことにする。

追記(2011.09.20)
本文を少し手直し。

「WordPressプラグインMT Style Post Nameの重複タグ問題を解決(WP3.2)」への5件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です