Skip to content

Commit 98e451b

Browse files
committed
Add JSON handling for search/replace in custom tables and nested JSON
1 parent 20351f6 commit 98e451b

3 files changed

Lines changed: 91 additions & 1 deletion

File tree

features/search-replace.feature

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,3 +1637,72 @@ Feature: Do global search/replace
16371637
"""
16381638
Table is read-only
16391639
"""
1640+
1641+
@require-mysql
1642+
Scenario: Search and replace handles JSON-encoded URLs in custom tables
1643+
Given a WP install
1644+
And I run `wp db query "CREATE TABLE wp_json_test ( id int(11) unsigned NOT NULL AUTO_INCREMENT, meta TEXT, PRIMARY KEY (id) ) ENGINE=InnoDB;"`
1645+
And I run `wp db query "INSERT INTO wp_json_test (meta) VALUES ('{\"confirmations\":{\"1\":{\"url\":\"https:\\/\\/oldsite.com\\/confirmation-page\",\"type\":\"redirect\"}}}');"`
1646+
1647+
When I run `wp search-replace 'https://oldsite.com' 'https://newsite.com' wp_json_test`
1648+
Then STDOUT should be a table containing rows:
1649+
| Table | Column | Replacements | Type |
1650+
| wp_json_test | meta | 1 | PHP |
1651+
1652+
When I run `wp db query "SELECT meta FROM wp_json_test WHERE id = 1" --skip-column-names`
1653+
Then STDOUT should contain:
1654+
"""
1655+
https:\/\/newsite.com\/confirmation-page
1656+
"""
1657+
And STDOUT should not contain:
1658+
"""
1659+
https:\/\/oldsite.com
1660+
"""
1661+
1662+
@require-mysql
1663+
Scenario: Search and replace handles nested JSON (JSON within serialized data)
1664+
Given a WP install
1665+
And a setup-nested-json.php file:
1666+
"""
1667+
<?php
1668+
$data = array(
1669+
'config' => json_encode( array(
1670+
'url' => 'https://oldsite.com/page',
1671+
'name' => 'Test',
1672+
) ),
1673+
);
1674+
update_option( 'nested_json_test', $data );
1675+
"""
1676+
And I run `wp eval-file setup-nested-json.php`
1677+
1678+
When I run `wp search-replace 'https://oldsite.com' 'https://newsite.com' wp_options --include-columns=option_value`
1679+
Then STDOUT should be a table containing rows:
1680+
| Table | Column | Replacements | Type |
1681+
| wp_options | option_value | 1 | PHP |
1682+
1683+
When I run `wp option get nested_json_test --format=json`
1684+
Then STDOUT should contain:
1685+
"""
1686+
newsite.com
1687+
"""
1688+
And STDOUT should not contain:
1689+
"""
1690+
oldsite.com
1691+
"""
1692+
1693+
@require-mysql
1694+
Scenario: Search and replace detects JSON columns for PHP mode automatically
1695+
Given a WP install
1696+
And I run `wp db query "CREATE TABLE wp_json_detect ( id int(11) unsigned NOT NULL AUTO_INCREMENT, data TEXT, PRIMARY KEY (id) ) ENGINE=InnoDB;"`
1697+
And I run `wp db query "INSERT INTO wp_json_detect (data) VALUES ('{\"site_url\":\"https:\\/\\/old.example.com\\/path\"}');"`
1698+
1699+
When I run `wp search-replace 'https://old.example.com' 'https://new.example.com' wp_json_detect`
1700+
Then STDOUT should be a table containing rows:
1701+
| Table | Column | Replacements | Type |
1702+
| wp_json_detect | data | 1 | PHP |
1703+
1704+
When I run `wp db query "SELECT data FROM wp_json_detect WHERE id = 1" --skip-column-names`
1705+
Then STDOUT should contain:
1706+
"""
1707+
new.example.com
1708+
"""

src/Search_Replace_Command.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,14 @@ public function __invoke( $args, $assoc_args ) {
562562
if ( false !== strpos( $wpdb->last_error, 'ERROR 1139' ) ) {
563563
$serial_row = true;
564564
}
565+
566+
// Also detect JSON objects/arrays so the PHP path can decode,
567+
// recurse into, and re-encode them — handling nested escaped
568+
// URLs that a simple SQL REPLACE cannot reach.
569+
if ( null === $serial_row ) {
570+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
571+
$serial_row = $wpdb->get_row( "SELECT * FROM $table_sql WHERE $col_sql REGEXP '^[\\\\[{]' LIMIT 1" );
572+
}
565573
}
566574

567575
if ( $php_only || $this->regex || null !== $serial_row ) {

src/WP_CLI/SearchReplacer.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,20 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis
200200
if ( $this->logging ) {
201201
$old_data = $data;
202202
}
203-
if ( $this->regex ) {
203+
204+
// Try to decode as a JSON object or array and recurse into the
205+
// decoded structure. This properly handles URLs stored inside
206+
// JSON-encoded columns (e.g. Gravity Forms confirmations, block
207+
// editor font data), including nested JSON where slashes are
208+
// double-escaped.
209+
$json_decoded = json_decode( $data, true );
210+
if ( null !== $json_decoded && is_array( $json_decoded ) ) {
211+
$json_decoded = $this->run_recursively( $json_decoded, false, $recursion_level + 1, $visited_data );
212+
$json_result = json_encode( $json_decoded );
213+
if ( false !== $json_result ) {
214+
$data = $json_result;
215+
}
216+
} elseif ( $this->regex ) {
204217
$search_regex = $this->regex_delimiter;
205218
$search_regex .= $this->from;
206219
$search_regex .= $this->regex_delimiter;

0 commit comments

Comments
 (0)