Current path: home/webdevt/www/cryptoimpot.fr/wp-content/themes/blocksy/inc/classes/db-versioning/utils/
?? Go up: /home/webdevt/www/cryptoimpot.fr/wp-content/themes/blocksy/inc/classes/db-versioning
<?php
namespace Blocksy\Database;
class SearchReplace {
private $dry_run = false;
public function invoke($args = []) {
global $wpdb;
$args = wp_parse_args($args, [
'old' => '',
'new' => '',
'dry_run' => true,
'tables' => null
]);
$old = $args['old'];
$new = $args['new'];
$this->dry_run = $args['dry_run'];
$skip_columns = [];
// never mess with hashed passwords
$skip_columns[] = 'user_pass';
$tables = $args['tables'];
if (! $tables) {
$tables = Utils::wp_get_relevant_table_names();
}
$result = [
'tables' => [],
];
foreach ($tables as $table) {
$table_sql = $this->esc_sql_ident($table);
list($primary_keys, $columns, $all_columns) = self::get_columns($table);
// since we'll be updating one row at a time,
// we need a primary key to identify the row
if (empty($primary_keys)) {
continue;
}
foreach ($columns as $col) {
if (in_array($col, $skip_columns, true)) {
continue;
}
$col_sql = $this->esc_sql_ident($col);
$wpdb->last_error = '';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
$serial_row = $wpdb->get_row(
"SELECT * FROM $table_sql WHERE $col_sql REGEXP '^[aiO]:[1-9]' LIMIT 1"
);
// When the regex triggers an error, we should fall back to PHP
if (false !== strpos($wpdb->last_error, 'ERROR 1139')) {
$serial_row = true;
}
if (null !== $serial_row) {
$type = 'PHP';
$count = $this->php_handle_col(
$col,
$primary_keys,
$table,
$old,
$new
);
} else {
$type = 'SQL';
$count = $this->sql_handle_col(
$col,
$primary_keys,
$table,
$old,
$new
);
}
if (intval($count) > 0) {
if (! isset($result['tables'][$table])) {
$result['tables'][$table] = 0;
}
$result['tables'][$table] += intval($count);
}
}
}
$total = 0;
foreach ($result['tables'] as $table => $count) {
$total += $count;
}
$result['total'] = $total;
return $result;
}
/**
* 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.
*/
private function esc_sql_ident($idents) {
$backtick = static function ($v) {
// Escape any backticks in the identifier by doubling.
return '`' . str_replace('`', '``', $v) . '`';
};
if (is_string($idents)) {
return $backtick($idents);
}
return array_map($backtick, $idents);
}
private function get_columns($table) {
global $wpdb;
$table_sql = $this->esc_sql_ident($table);
$primary_keys = array();
$text_columns = array();
$all_columns = array();
$suppress_errors = $wpdb->suppress_errors();
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
$results = $wpdb->get_results("DESCRIBE $table_sql");
if (! empty($results)) {
foreach ($results as $col) {
if ('PRI' === $col->Key) {
$primary_keys[] = $col->Field;
}
if ($this->is_text_col($col->Type)) {
$text_columns[] = $col->Field;
}
$all_columns[] = $col->Field;
}
}
$wpdb->suppress_errors($suppress_errors);
return array($primary_keys, $text_columns, $all_columns);
}
private function is_text_col($type) {
foreach (array('text', 'varchar') as $token) {
if (false !== stripos($type, $token)) {
return true;
}
}
return false;
}
private function sql_handle_col($col, $primary_keys, $table, $old, $new) {
global $wpdb;
$table_sql = $this->esc_sql_ident($table);
$col_sql = $this->esc_sql_ident($col);
if ($this->dry_run) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT($col_sql) FROM $table_sql WHERE $col_sql LIKE BINARY %s;", '%' . $this->esc_like($old) . '%'
)
);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
$count = $wpdb->query(
$wpdb->prepare(
"UPDATE $table_sql SET $col_sql = REPLACE($col_sql, %s, %s);",
$old,
$new
)
);
}
return $count;
}
private function esc_like($old) {
global $wpdb;
// Remove notices in 4.0 and support backwards compatibility
if (method_exists($wpdb, 'esc_like')) {
// 4.0
$old = $wpdb->esc_like($old);
}
return $old;
}
private function php_handle_col($col, $primary_keys, $table, $old, $new) {
global $wpdb;
$count = 0;
$replacer = new SearchReplacer($old, $new, true);
$table_sql = $this->esc_sql_ident($table);
$col_sql = $this->esc_sql_ident($col);
$base_key_condition = "$col_sql" . $wpdb->prepare(
' LIKE BINARY %s',
'%' . $this->esc_like($old) . '%'
);
$where_key = "WHERE $base_key_condition";
$escaped_primary_keys = $this->esc_sql_ident($primary_keys);
$primary_keys_sql = implode(',', $escaped_primary_keys);
$order_by_keys = array_map(
static function ($key) {
return "{$key} ASC";
},
$escaped_primary_keys
);
$order_by_sql = 'ORDER BY ' . implode(',', $order_by_keys);
$limit = 1000;
// 2 errors:
// - WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
// - WordPress.CodeAnalysis.AssignmentInCondition -- no reason to do copy-paste for a single valid assignment in while
// phpcs:ignore
while ($rows = $wpdb->get_results("SELECT {$primary_keys_sql} FROM {$table_sql} {$where_key} {$order_by_sql} LIMIT {$limit}")) {
foreach ($rows as $keys) {
$where_sql = '';
foreach ((array) $keys as $k => $v) {
if ('' !== $where_sql) {
$where_sql .= ' AND ';
}
$where_sql .= $this->esc_sql_ident($k) . ' = ' . $this->esc_sql_value($v);
}
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
$col_value = $wpdb->get_var(
"SELECT {$col_sql} FROM {$table_sql} WHERE {$where_sql}"
);
if ('' === $col_value) {
continue;
}
$value = $replacer->run($col_value);
if ($value === $col_value) {
continue;
}
$count++;
if (! $this->dry_run) {
$update_where = array();
foreach ((array) $keys as $k => $v) {
$update_where[$k] = $v;
}
$wpdb->update($table, [$col => $value], $update_where);
}
}
// Because we are ordering by primary keys from least to greatest,
// we can exclude previous chunks from consideration by adding greater-than conditions
// to insist the next chunk's keys must be greater than the last of this chunk's keys.
$last_row = end($rows);
$next_key_conditions = array();
// NOTE: For a composite key (X, Y, Z), selecting the next rows requires the following conditions:
// ( X = lastX AND Y = lastY AND Z > lastZ ) OR
// ( X = lastX AND Y > lastY ) OR
// ( X > lastX )
for ($last_key_index = count($primary_keys) - 1; $last_key_index >= 0; $last_key_index--) {
$next_key_subconditions = array();
for ($i = 0; $i <= $last_key_index; $i++) {
$k = $primary_keys[$i];
$v = $last_row->{$k};
if ($i < $last_key_index) {
$next_key_subconditions[] = $this->esc_sql_ident($k) . ' = ' . $this->esc_sql_value($v);
} else {
$next_key_subconditions[] = $this->esc_sql_ident($k) . ' > ' . $this->esc_sql_value($v);
}
}
$next_key_conditions[] = '( ' . implode(' AND ', $next_key_subconditions) . ' )';
}
$where_key_conditions = array();
if ($base_key_condition) {
$where_key_conditions[] = $base_key_condition;
}
$where_key_conditions[] = '( ' . implode(' OR ', $next_key_conditions) . ' )';
$where_key = 'WHERE ' . implode(' AND ', $where_key_conditions);
}
return $count;
}
/**
* 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.
*/
private function esc_sql_value($values) {
$quote = static function ($v) {
// Don't quote integer values to avoid MySQL's implicit type conversion.
if (preg_match('/^[+-]?[0-9]{1,20}$/', $v)) { // MySQL BIGINT UNSIGNED max 18446744073709551615 (20 digits).
return esc_sql($v);
}
// Put any string values between single quotes.
return "'" . esc_sql($v) . "'";
};
if (is_array($values)) {
return array_map($quote, $values);
}
return $quote($values);
}
}