Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
parameters:
level: 3
paths:
- src
- search-replace-command.php
scanDirectories:
- vendor/wp-cli/wp-cli/php
scanFiles:
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
treatPhpDocTypesAsCertain: false
219 changes: 159 additions & 60 deletions src/Search_Replace_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,114 @@

class Search_Replace_Command extends WP_CLI_Command {

/**
* @var bool
*/
private $dry_run;

/**
* @var false|resource
*/
private $export_handle = false;

/**
* @var int
*/
private $export_insert_size;

/**
* @var bool
*/
private $recurse_objects;

/**
* @var bool
*/
private $regex;

/**
* @var string
*/
private $regex_flags;

/**
* @var string
*/
private $regex_delimiter;

/**
* @var int
*/
private $regex_limit = -1;

/**
* @var string[]
*/
private $skip_tables;

/**
* @var string[]
*/
private $skip_columns;

/**
* @var string[]
*/
private $include_columns;

/**
* @var string
*/
private $format;

/**
* @var bool
*/
private $report;

/**
* @var bool
*/
private $verbose;

/**
* @var bool
*/
private $report_changed_only;
private $log_handle = null;

/**
* @var false|resource
*/
private $log_handle = null;

/**
* @var int
*/
private $log_before_context = 40;
private $log_after_context = 40;
private $log_prefixes = array( '< ', '> ' );

/**
* @var int
*/
private $log_after_context = 40;

/**
* @var array{0: string, 1: string}
*/
private $log_prefixes = array( '< ', '> ' );

/**
* @var array<string, array<mixed>>
*/
private $log_colors;

/**
* @var string|false
*/
private $log_encoding;

/**
* @var float
*/
private $start_time;

/**
Expand Down Expand Up @@ -169,24 +255,28 @@
* else
* wp search-replace 'http://example.com' 'http://example.test' --recurse-objects --skip-columns=guid --skip-tables=wp_users
* fi
*
* @param array<string> $args Positional arguments.
* @param array{'dry-run'?: bool, 'network'?: bool, 'all-tables-with-prefix'?: bool, 'all-tables'?: bool, 'export'?: string, 'export_insert_size'?: string, 'skip-tables'?: string, 'skip-columns'?: string, 'include-columns'?: string, 'precise'?: bool, 'recurse-objects'?: bool, 'verbose'?: bool, 'regex'?: bool, 'regex-flags'?: string, 'regex-delimiter'?: string, 'regex-limit'?: string, 'format': string, 'report'?: bool, 'report-changed-only'?: bool, 'log'?: string, 'before_context'?: string, 'after_context'?: string} $assoc_args Associative arguments.
*/
public function __invoke( $args, $assoc_args ) {
global $wpdb;
$old = array_shift( $args );
$new = array_shift( $args );
$total = 0;
$report = array();
$this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run' );
$php_only = Utils\get_flag_value( $assoc_args, 'precise' );
$this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run', false );
$php_only = Utils\get_flag_value( $assoc_args, 'precise', false );
$this->recurse_objects = Utils\get_flag_value( $assoc_args, 'recurse-objects', true );
$this->verbose = Utils\get_flag_value( $assoc_args, 'verbose' );
$this->verbose = Utils\get_flag_value( $assoc_args, 'verbose', false );
$this->format = Utils\get_flag_value( $assoc_args, 'format' );
$this->regex = Utils\get_flag_value( $assoc_args, 'regex', false );

$default_regex_delimiter = false;

if ( null !== $this->regex ) {
$default_regex_delimiter = false;
$this->regex_flags = Utils\get_flag_value( $assoc_args, 'regex-flags', false );
$this->regex_delimiter = Utils\get_flag_value( $assoc_args, 'regex-delimiter', '' );
$this->regex_flags = Utils\get_flag_value( $assoc_args, 'regex-flags', false );
$this->regex_delimiter = Utils\get_flag_value( $assoc_args, 'regex-delimiter', '' );
if ( '' === $this->regex_delimiter ) {
$this->regex_delimiter = chr( 1 );
$default_regex_delimiter = true;
Expand All @@ -213,7 +303,7 @@
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Preventing a warning when testing the regex.
if ( false === @preg_match( $search_regex, '' ) ) {
$error = error_get_last();
$preg_error_message = ( ! empty( $error ) && array_key_exists( 'message', $error ) ) ? "\n{$error['message']}." : '';
$preg_error_message = ! empty( $error ) ? "\n{$error['message']}." : '';
if ( $default_regex_delimiter ) {
$flags_msg = $this->regex_flags ? "flags '$this->regex_flags'" : 'no flags';
$msg = "The regex pattern '$old' with default delimiter 'chr(1)' and {$flags_msg} fails.";
Expand Down Expand Up @@ -242,16 +332,16 @@
$this->export_handle = STDOUT;
$this->verbose = false;
} else {
$this->export_handle = @fopen( $assoc_args['export'], 'w' );
$this->export_handle = @fopen( $export, 'w' );
if ( false === $this->export_handle ) {
$error = error_get_last();
WP_CLI::error( sprintf( 'Unable to open export file "%s" for writing: %s.', $assoc_args['export'], $error['message'] ) );
WP_CLI::error( sprintf( 'Unable to open export file "%s" for writing: %s.', $export, $error['message'] ?? '(unknown error)' ) );
}
}
$export_insert_size = Utils\get_flag_value( $assoc_args, 'export_insert_size', 50 );
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- See the code, this is deliberate.
if ( (int) $export_insert_size == $export_insert_size && $export_insert_size > 0 ) {
$this->export_insert_size = $export_insert_size;
$this->export_insert_size = (int) $export_insert_size;
}
$php_only = true;
}
Expand All @@ -261,54 +351,53 @@
if ( true === $log || '-' === $log ) {
$this->log_handle = STDOUT;
} else {
$this->log_handle = @fopen( $assoc_args['log'], 'w' );
$this->log_handle = @fopen( $log, 'w' );
if ( false === $this->log_handle ) {
$error = error_get_last();
WP_CLI::error( sprintf( 'Unable to open log file "%s" for writing: %s.', $assoc_args['log'], $error['message'] ) );
WP_CLI::error( sprintf( 'Unable to open log file "%s" for writing: %s.', $log, $error['message'] ?? '(unknown error)' ) );

Check warning on line 357 in src/Search_Replace_Command.php

View check run for this annotation

Codecov / codecov/patch

src/Search_Replace_Command.php#L357

Added line #L357 was not covered by tests
}
}
if ( $this->log_handle ) {
$before_context = Utils\get_flag_value( $assoc_args, 'before_context' );
if ( null !== $before_context && preg_match( '/^[0-9]+$/', $before_context ) ) {
$this->log_before_context = (int) $before_context;
}

$after_context = Utils\get_flag_value( $assoc_args, 'after_context' );
if ( null !== $after_context && preg_match( '/^[0-9]+$/', $after_context ) ) {
$this->log_after_context = (int) $after_context;
}
$before_context = Utils\get_flag_value( $assoc_args, 'before_context' );
if ( null !== $before_context && preg_match( '/^[0-9]+$/', $before_context ) ) {
$this->log_before_context = (int) $before_context;
}

$log_prefixes = getenv( 'WP_CLI_SEARCH_REPLACE_LOG_PREFIXES' );
if ( false !== $log_prefixes && preg_match( '/^([^,]*),([^,]*)$/', $log_prefixes, $matches ) ) {
$this->log_prefixes = array( $matches[1], $matches[2] );
}
$after_context = Utils\get_flag_value( $assoc_args, 'after_context' );
if ( null !== $after_context && preg_match( '/^[0-9]+$/', $after_context ) ) {
$this->log_after_context = (int) $after_context;
}

if ( STDOUT === $this->log_handle ) {
$default_log_colors = array(
'log_table_column_id' => '%B',
'log_old' => '%R',
'log_new' => '%G',
);
} else {
$default_log_colors = array(
'log_table_column_id' => '',
'log_old' => '',
'log_new' => '',
);
}
$log_prefixes = getenv( 'WP_CLI_SEARCH_REPLACE_LOG_PREFIXES' );
if ( false !== $log_prefixes && preg_match( '/^([^,]*),([^,]*)$/', $log_prefixes, $matches ) ) {
$this->log_prefixes = array( $matches[1], $matches[2] );
}

$log_colors = getenv( 'WP_CLI_SEARCH_REPLACE_LOG_COLORS' );
if ( false !== $log_colors && preg_match( '/^([^,]*),([^,]*),([^,]*)$/', $log_colors, $matches ) ) {
$default_log_colors = array(
'log_table_column_id' => $matches[1],
'log_old' => $matches[2],
'log_new' => $matches[3],
);
}
if ( STDOUT === $this->log_handle ) {
$default_log_colors = array(
'log_table_column_id' => '%B',
'log_old' => '%R',
'log_new' => '%G',
);
} else {
$default_log_colors = array(
'log_table_column_id' => '',
'log_old' => '',
'log_new' => '',
);
}

$this->log_colors = $this->get_colors( $assoc_args, $default_log_colors );
$this->log_encoding = 0 === strpos( $wpdb->charset, 'utf8' ) ? 'UTF-8' : false;
$log_colors = getenv( 'WP_CLI_SEARCH_REPLACE_LOG_COLORS' );
if ( false !== $log_colors && preg_match( '/^([^,]*),([^,]*),([^,]*)$/', $log_colors, $matches ) ) {
$default_log_colors = array(
'log_table_column_id' => $matches[1],
'log_old' => $matches[2],
'log_new' => $matches[3],
);
}

$this->log_colors = $this->get_colors( $assoc_args, $default_log_colors );
$this->log_encoding = 0 === strpos( $wpdb->charset, 'utf8' ) ? 'UTF-8' : false;
}

$this->report = Utils\get_flag_value( $assoc_args, 'report', true );
Expand Down Expand Up @@ -383,6 +472,8 @@
WP_CLI::log( sprintf( 'Checking: %s.%s', $table, $col ) );
}

$serial_row = false;

if ( ! $php_only && ! $this->regex ) {
$col_sql = self::esc_sql_ident( $col );
$wpdb->last_error = '';
Expand Down Expand Up @@ -422,7 +513,7 @@
}

if ( 'count' === $this->format ) {
WP_CLI::line( $total );
WP_CLI::line( (string) $total );
return;
}

Expand Down Expand Up @@ -698,8 +789,9 @@

if ( method_exists( $wpdb, 'remove_placeholder_escape' ) ) {
// since 4.8.3
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- verified inputs above
$sql = $wpdb->remove_placeholder_escape( $wpdb->prepare( $sql, array_values( $values ) ) );

// @phpstan-ignore method.nonObject
$sql = $wpdb->remove_placeholder_escape( $wpdb->prepare( $sql, array_values( $values ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- verified inputs above
} else {
// 4.8.2 or less
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- verified inputs above
Expand Down Expand Up @@ -766,8 +858,9 @@
// 4.0
$old = $wpdb->esc_like( $old );
} else {
// phpcs:ignore WordPress.WP.DeprecatedFunctions.like_escapeFound -- BC-layer for WP 3.9 or less.
$old = like_escape( esc_sql( $old ) ); // Note: this double escaping is actually necessary, even though `esc_like()` will be used in a `prepare()`.
// Note: this double escaping is actually necessary, even though `esc_like()` will be used in a `prepare()`.
// @phpstan-ignore function.deprecated
$old = like_escape( esc_sql( $old ) ); // phpcs:ignore WordPress.WP.DeprecatedFunctions.like_escapeFound -- BC-layer for WP 3.9 or less.

Check warning on line 863 in src/Search_Replace_Command.php

View check run for this annotation

Codecov / codecov/patch

src/Search_Replace_Command.php#L863

Added line #L863 was not covered by tests
}

return $old;
Expand All @@ -777,8 +870,9 @@
* Escapes (backticks) MySQL identifiers (aka schema object names) - i.e. column names, table names, and database/index/alias/view etc names.
* See https://dev.mysql.com/doc/refman/5.5/en/identifiers.html
*
* @param string|array $idents A single identifier or an array of identifiers.
* @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings.
* @param string|string[] $idents A single identifier or an array of identifiers.
* @return string|string[] An escaped string if given a string, or an array of escaped strings if given an array of strings.
* @phpstan-return ($idents is string ? string : string[])
*/
private static function esc_sql_ident( $idents ) {
$backtick = static function ( $v ) {
Expand All @@ -794,8 +888,9 @@
/**
* Puts MySQL string values in single quotes, to avoid them being interpreted as column names.
*
* @param string|array $values A single value or an array of values.
* @return string|array A quoted string if given a string, or an array of quoted strings if given an array of strings.
* @param string|string[] $values A single value or an array of values.
* @return string|string[] A quoted string if given a string, or an array of quoted strings if given an array of strings.
* @phpstan-return ($values is string ? string : string[])
*/
private static function esc_sql_value( $values ) {
$quote = static function ( $v ) {
Expand Down Expand Up @@ -1035,6 +1130,10 @@
* @param array $new_bits Array of new replacement log strings.
*/
private function log_write( $col, $keys, $table, $old_bits, $new_bits ) {
if ( ! $this->log_handle ) {
return;

Check warning on line 1134 in src/Search_Replace_Command.php

View check run for this annotation

Codecov / codecov/patch

src/Search_Replace_Command.php#L1134

Added line #L1134 was not covered by tests
}

$id_log = $keys ? ( ':' . implode( ',', (array) $keys ) ) : '';
$table_column_id_log = $this->log_colors['log_table_column_id'][0] . $table . '.' . $col . $id_log . $this->log_colors['log_table_column_id'][1];

Expand Down
Loading