WP CLI(Version=1.3.0)における正規表現を用いた置換処理のバグについて

WP CLI 1.3.0には、正規表現を用いた置換処理にバグがあり、特定のオプションに無効な引数を与えることにより、preg_replace関数のエラーを無視し処理を続行しようとします。

注記)
マイルストーンが切られ、Version=1.4.0にてこのバグは修正されます。詳細はエントリに追記しました。
https://github.com/wp-cli/search-replace-command/milestone/4

本来の挙動

普通の文字列置換

fooをbarに置換します。

# wp search-replace foo bar

該当WordPressサイト内のfooが文字列置換によりbarに書き換えられます。
これはstr_replace関数でさくさく実行されます。

正規表現を用いた文字列置換

–regexオプションをコマンドの後ろに付けることにより、preg_replaceによる正規表現を用いた置換処理に切り替わります。
また、–regex-flagsの引数にパターン修飾子を与えることにより、preg_replaceのマッチ方法を変えることができます。

# wp search-replace 'W.r.P.e.s' 'wordpress' --path=/home/kusanagi/wpclibugtest/DocumentRoot/ --regex --regex-flags='i' --dry-run

バグの内容

wp search-replaceは、通常はstr_replace関数により単純な文字列置換が行われます。

しかし、以下のドキュメントにある通り、特定のオプションを付けることにより、preg_replaceによる正規表現を用いた置換処理を行う事ができます。
wp search-replace | WordPress Developer Resources

--regex - 正規表現置換の有効化
--regex-flags= - 正規表現オプションの指定

再現環境

# wp --version
WP-CLI 1.3.0

# kusanagi status
Profile: wpclibugtest
Type: WordPress
KUSANAGI Version 8.0.8-1
conoha

# which wp                                                                                             
alias wp='sudo -u kusanagi -- /usr/local/bin/wp'
        /usr/bin/sudo

正常な動作をする事を確認

正常に正規表現置換を記述した場合、下記の例では問題なく「WordPress」が「wordpress」に書き換わります。
※実際には–dry-runを行っているので、書き込みはされません。

# wp search-replace 'W.r.P.e.s' 'wordpress' --path=/home/kusanagi/wpclibugtest/DocumentRoot/ --regex --regex-flags='i' --dry-run
+------------------+-----------------------+--------------+------+
| Table            | Column                | Replacements | Type |
+------------------+-----------------------+--------------+------+
| wp_commentmeta   | meta_key              | 0            | PHP  |
| wp_commentmeta   | meta_value            | 0            | PHP  |
| wp_comments      | comment_author        | 1            | PHP  |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| wp_users         | user_activation_key   | 0            | PHP  |
| wp_users         | display_name          | 0            | PHP  |
+------------------+-----------------------+--------------+------+
Success: 4 replacements to be made.

無効な置換オプションを渡した時の挙動

「–regex」オプションにより、str_replace関数ではなく、preg_replaceによる正規表現を用いた置換が可能になります。

この場合、preg_replaceのneedleは、fromと–regex-flagsにより組み立てられます。

104041                 if ( $this->regex ) {
104042                     $data = preg_replace( "/$this->from/$this->regex_flags", $this->to, $data );
104043                 } else {
104044                     $data = str_replace( $this->from, $this->to, $data );
104045                 }

例えば正常性試験の内容をpreg_replace関数の引数になおすと、

$data = preg_replace("/W.r.P.e.s"/i", 'wordpress', $data);

こうなり、一見成功をします。

しかし、–regex-flagsに対しバリデーション処理が行われていない為、任意のオプションをpreg_replaceに渡す事によりpreg_replaceを止める事が可能です。

再現例

–regex-flags=オプションに無効な価を渡してみる場合、以下のようになります。

# wp search-replace '.W.r...e.s' 'wordpress' --path=/home/kusanagi/wpclibugtest/DocumentRoot/ --regex --regex-flags='kppr' --dry-run                                                                                         
PHP Warning:  preg_replace(): Unknown modifier 'k' in phar:///usr/local/bin/wp/vendor/wp-cli/search-replace-command/src/WP_CLI/SearchReplacer.php on line 86
Warning: preg_replace(): Unknown modifier 'k' in phar:///usr/local/bin/wp/vendor/wp-cli/search-replace-command/src/WP_CLI/SearchReplacer.php on line 86
PHP Warning:  preg_replace(): Unknown modifier 'k' in phar:///usr/local/bin/wp/vendor/wp-cli/search-replace-command/src/WP_CLI/SearchReplacer.php on line 86
......
+------------------+-----------------------+--------------+------+
| Table            | Column                | Replacements | Type |
+------------------+-----------------------+--------------+------+
| wp_commentmeta   | meta_key              | 0            | PHP  |
| wp_commentmeta   | meta_value            | 0            | PHP  |
| wp_comments      | comment_author        | 1            | PHP  |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| wp_users         | user_activation_key   | 0            | PHP  |
| wp_users         | display_name          | 1            | PHP  |
+------------------+-----------------------+--------------+------+
Success: 388 replacements to be made.

つまり、以下のバグを内包しています。

  • preg_replace関数をtry-cacheで括ってなくて、成功してようと失敗してようと成功したようにかうんとしている
  • preg_replaceに渡すneedleの引数をValidationしていない

本来wp cliが行うべき対処

–regex-flagsへのValidationの実装

preg_replace関数は、–regex-flagsの引数として、以下のパターン修飾子期待しています。
まず、このバリデーション処理が入っていない為に、このバグは発生しています。

if (preg_match('[imsxADSUXJu]{1}', $this->regex_flags) != 1 ) { #ERROR_LOGIC }

置換処理へのtry-cacheの実装

次に、置換処理そのものに対しtry-cacheが組まれていない為、エラーが出た場合でも強引にreplaceに成功した事になっています。

103961 class SearchReplacer {
104001     private function _run( $data, $serialised, $recursion_level = 0, $visited_data = array() ) {
104040             else if ( is_string( $data ) ) {
104041                 if ( $this->regex ) {
104042                     $data = preg_replace( "/$this->from/$this->regex_flags", $this->to, $data );
104043                 } else {
104044                     $data = str_replace( $this->from, $this->to, $data );
104045                 }
104046             }

マイルストーン1.0.4にて修正

このエントリのバグは@miya0001さんにより修正され、Version 1.0.4にてリリースされます。
https://github.com/wp-cli/search-replace-command/pull/28
https://github.com/wp-cli/search-replace-command/milestone/4