: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$table = strtolower( $table );
if ( empty( $this->col_meta[ $table ] ) ) {
// If any of the columns don't have one of these collations, it needs more confidence checking.
$safe_collations = array(
foreach ( $this->col_meta[ $table ] as $col ) {
if ( empty( $col->Collation ) ) {
if ( ! in_array( $col->Collation, $safe_collations, true ) ) {
* Strips any invalid characters based on value/charset pairs.
* @param array $data Array of value arrays. Each value array has the keys 'value', 'charset', and 'length'.
* An optional 'ascii' key can be set to false to avoid redundant ASCII checks.
* @return array|WP_Error The $data parameter, with invalid characters removed from each value.
* This works as a passthrough: any additional keys such as 'field' are
* retained in each value array. If we cannot remove invalid characters,
* a WP_Error object is returned.
protected function strip_invalid_text( $data ) {
$db_check_string = false;
foreach ( $data as &$value ) {
$charset = $value['charset'];
if ( is_array( $value['length'] ) ) {
$length = $value['length']['length'];
$truncate_by_byte_length = 'byte' === $value['length']['type'];
* Since we have no length, we'll never truncate. Initialize the variable to false.
* True would take us through an unnecessary (for this case) codepath below.
$truncate_by_byte_length = false;
// There's no charset to work with.
if ( false === $charset ) {
// Column isn't a string.
if ( ! is_string( $value['value'] ) ) {
$needs_validation = true;
// latin1 can store any byte sequence.
( ! isset( $value['ascii'] ) && $this->check_ascii( $value['value'] ) )
$truncate_by_byte_length = true;
$needs_validation = false;
if ( $truncate_by_byte_length ) {
mbstring_binary_safe_encoding();
if ( false !== $length && strlen( $value['value'] ) > $length ) {
$value['value'] = substr( $value['value'], 0, $length );
reset_mbstring_encoding();
if ( ! $needs_validation ) {
// utf8 can be handled by regex, which is a bunch faster than a DB lookup.
if ( ( 'utf8' === $charset || 'utf8mb3' === $charset || 'utf8mb4' === $charset ) && function_exists( 'mb_strlen' ) ) {
(?: [\x00-\x7F] # single-byte sequences 0xxxxxxx
| [\xC2-\xDF][\x80-\xBF] # double-byte sequences 110xxxxx 10xxxxxx
| \xE0[\xA0-\xBF][\x80-\xBF] # triple-byte sequences 1110xxxx 10xxxxxx * 2
| [\xE1-\xEC][\x80-\xBF]{2}
| \xED[\x80-\x9F][\x80-\xBF]
| [\xEE-\xEF][\x80-\xBF]{2}';
if ( 'utf8mb4' === $charset ) {
| \xF0[\x90-\xBF][\x80-\xBF]{2} # four-byte sequences 11110xxx 10xxxxxx * 3
| [\xF1-\xF3][\x80-\xBF]{3}
| \xF4[\x80-\x8F][\x80-\xBF]{2}
$regex .= '){1,40} # ...one or more times
$value['value'] = preg_replace( $regex, '$1', $value['value'] );
if ( false !== $length && mb_strlen( $value['value'], 'UTF-8' ) > $length ) {
$value['value'] = mb_substr( $value['value'], 0, $length, 'UTF-8' );
// We couldn't use any local conversions, send it to the DB.
unset( $value ); // Remove by reference.
if ( $db_check_string ) {
foreach ( $data as $col => $value ) {
if ( ! empty( $value['db'] ) ) {
// We're going to need to truncate by characters or bytes, depending on the length value we have.
if ( isset( $value['length']['type'] ) && 'byte' === $value['length']['type'] ) {
// Using binary causes LEFT() to truncate by bytes.
$charset = $value['charset'];
$connection_charset = $this->charset;
$connection_charset = mysqli_character_set_name( $this->dbh );
if ( is_array( $value['length'] ) ) {
$length = sprintf( '%.0f', $value['length']['length'] );
$queries[ $col ] = $this->prepare( "CONVERT( LEFT( CONVERT( %s USING $charset ), $length ) USING $connection_charset )", $value['value'] );
} elseif ( 'binary' !== $charset ) {
// If we don't have a length, there's no need to convert binary - it will always return the same result.
$queries[ $col ] = $this->prepare( "CONVERT( CONVERT( %s USING $charset ) USING $connection_charset )", $value['value'] );
unset( $data[ $col ]['db'] );
foreach ( $queries as $column => $query ) {
$sql[] = $query . " AS x_$column";
$this->check_current_query = false;
$row = $this->get_row( 'SELECT ' . implode( ', ', $sql ), ARRAY_A );
return new WP_Error( 'wpdb_strip_invalid_text_failure', __( 'Could not strip invalid text.' ) );
foreach ( array_keys( $data ) as $column ) {
if ( isset( $row[ "x_$column" ] ) ) {
$data[ $column ]['value'] = $row[ "x_$column" ];
* Strips any invalid characters from the query.
* @param string $query Query to convert.
* @return string|WP_Error The converted query, or a WP_Error object if the conversion fails.
protected function strip_invalid_text_from_query( $query ) {
// We don't need to check the collation for queries that don't read data.
$trimmed_query = ltrim( $query, "\r\n\t (" );
if ( preg_match( '/^(?:SHOW|DESCRIBE|DESC|EXPLAIN|CREATE)\s/i', $trimmed_query ) ) {
$table = $this->get_table_from_query( $query );
$charset = $this->get_table_charset( $table );
if ( is_wp_error( $charset ) ) {
// We can't reliably strip text from tables containing binary/blob columns.
if ( 'binary' === $charset ) {
$charset = $this->charset;
$data = $this->strip_invalid_text( array( $data ) );
if ( is_wp_error( $data ) ) {
return $data[0]['value'];
* Strips any invalid characters from the string for a given table and column.
* @param string $table Table name.
* @param string $column Column name.
* @param string $value The text to check.
* @return string|WP_Error The converted string, or a WP_Error object if the conversion fails.
public function strip_invalid_text_for_column( $table, $column, $value ) {
if ( ! is_string( $value ) ) {
$charset = $this->get_col_charset( $table, $column );
} elseif ( is_wp_error( $charset ) ) {
'length' => $this->get_col_length( $table, $column ),
$data = $this->strip_invalid_text( $data );
if ( is_wp_error( $data ) ) {
return $data[ $column ]['value'];
* Finds the first table name referenced in a query.
* @param string $query The query to search.
* @return string|false The table name found, or false if a table couldn't be found.
protected function get_table_from_query( $query ) {
// Remove characters that can legally trail the table name.
$query = rtrim( $query, ';/-#' );
// Allow (select...) union [...] style queries. Use the first query's table name.
$query = ltrim( $query, "\r\n\t (" );
// Strip everything between parentheses except nested selects.
$query = preg_replace( '/\((?!\s*select)[^(]*?\)/is', '()', $query );
// Quickly match most common queries.
. '|INSERT(?:\s+LOW_PRIORITY|\s+DELAYED|\s+HIGH_PRIORITY)?(?:\s+IGNORE)?(?:\s+INTO)?'
. '|REPLACE(?:\s+LOW_PRIORITY|\s+DELAYED)?(?:\s+INTO)?'
. '|UPDATE(?:\s+LOW_PRIORITY)?(?:\s+IGNORE)?'
. '|DELETE(?:\s+LOW_PRIORITY|\s+QUICK|\s+IGNORE)*(?:.+?FROM)?'
. ')\s+((?:[0-9a-zA-Z$_.`-]|[\xC2-\xDF][\x80-\xBF])+)/is',
return str_replace( '`', '', $maybe[1] );
// SHOW TABLE STATUS and SHOW TABLES WHERE Name = 'wp_posts'
if ( preg_match( '/^\s*SHOW\s+(?:TABLE\s+STATUS|(?:FULL\s+)?TABLES).+WHERE\s+Name\s*=\s*("|\')((?:[0-9a-zA-Z$_.-]|[\xC2-\xDF][\x80-\xBF])+)\\1/is', $query, $maybe ) ) {
* SHOW TABLE STATUS LIKE and SHOW TABLES LIKE 'wp\_123\_%'
* This quoted LIKE operand seldom holds a full table name.
* It is usually a pattern for matching a prefix so we just
* strip the trailing % and unescape the _ to get 'wp_123_'
* which drop-ins can use for routing these SQL statements.
if ( preg_match( '/^\s*SHOW\s+(?:TABLE\s+STATUS|(?:FULL\s+)?TABLES)\s+(?:WHERE\s+Name\s+)?LIKE\s*("|\')((?:[\\\\0-9a-zA-Z$_.-]|[\xC2-\xDF][\x80-\xBF])+)%?\\1/is', $query, $maybe ) ) {
return str_replace( '\\_', '_', $maybe[2] );
// Big pattern for the rest of the table-related queries.
. '(?:EXPLAIN\s+(?:EXTENDED\s+)?)?SELECT.*?\s+FROM'
. '|DESCRIBE|DESC|EXPLAIN|HANDLER'
. '|(?:LOCK|UNLOCK)\s+TABLE(?:S)?'
. '|(?:RENAME|OPTIMIZE|BACKUP|RESTORE|CHECK|CHECKSUM|ANALYZE|REPAIR).*\s+TABLE'
. '|TRUNCATE(?:\s+TABLE)?'
. '|CREATE(?:\s+TEMPORARY)?\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?'
. '|ALTER(?:\s+IGNORE)?\s+TABLE'
. '|DROP\s+TABLE(?:\s+IF\s+EXISTS)?'
. '|CREATE(?:\s+\w+)?\s+INDEX.*\s+ON'
. '|LOAD\s+DATA.*INFILE.*INTO\s+TABLE'
. '|(?:GRANT|REVOKE).*ON\s+TABLE'
. '|SHOW\s+(?:.*FROM|.*TABLE)'
. ')\s+\(*\s*((?:[0-9a-zA-Z$_.`-]|[\xC2-\xDF][\x80-\xBF])+)\s*\)*/is',
return str_replace( '`', '', $maybe[1] );
* Loads the column metadata from the last query.
protected function load_col_info() {
$num_fields = mysqli_num_fields( $this->result );
for ( $i = 0; $i < $num_fields; $i++ ) {
$this->col_info[ $i ] = mysqli_fetch_field( $this->result );
* Retrieves column metadata from the last query.
* @param string $info_type Optional. Possible values include 'name', 'table', 'def', 'max_length',
* 'not_null', 'primary_key', 'multiple_key', 'unique_key', 'numeric',
* 'blob', 'type', 'unsigned', 'zerofill'. Default 'name'.
* @param int $col_offset Optional. 0: col name. 1: which table the col's in. 2: col's max length.
* 3: if the col is numeric. 4: col's type. Default -1.
* @return mixed Column results.
public function get_col_info( $info_type = 'name', $col_offset = -1 ) {
if ( -1 === $col_offset ) {
foreach ( (array) $this->col_info as $col ) {
$new_array[ $i ] = $col->{$info_type};
return $this->col_info[ $col_offset ]->{$info_type};
* Starts the timer, for debugging purposes.
public function timer_start() {
$this->time_start = microtime( true );
* Stops the debugging timer.
* @return float Total time spent on the query, in seconds.
public function timer_stop() {
return ( microtime( true ) - $this->time_start );
* Wraps errors in a nice header and footer and dies.
* Will not die if wpdb::$show_errors is false.
* @param string $message The error message.
* @param string $error_code Optional. A computer-readable string to identify the error.
* @return void|false Void if the showing of errors is enabled, false if disabled.
public function bail( $message, $error_code = '500' ) {
if ( $this->show_errors ) {
if ( $this->dbh instanceof mysqli ) {
$error = mysqli_error( $this->dbh );
} elseif ( mysqli_connect_errno() ) {
$error = mysqli_connect_error();
$message = '<p><code>' . $error . "</code></p>\n" . $message;
if ( class_exists( 'WP_Error', false ) ) {
$this->error = new WP_Error( $error_code, $message );
* Closes the current database connection.
* @return bool True if the connection was successfully closed,
* false if it wasn't, or if the connection doesn't exist.
public function close() {
$closed = mysqli_close( $this->dbh );
$this->has_connected = false;
* Determines whether MySQL database is at least the required minimum version.
* @global string $wp_version The WordPress version string.
* @global string $required_mysql_version The required MySQL version string.
public function check_database_version() {
global $wp_version, $required_mysql_version;
// Make sure the server has the required MySQL version.
if ( version_compare( $this->db_version(), $required_mysql_version, '<' ) ) {
/* translators: 1: WordPress version number, 2: Minimum required MySQL version number. */
return new WP_Error( 'database_version', sprintf( __( '<strong>Error:</strong> WordPress %1$s requires MySQL %2$s or higher' ), $wp_version, $required_mysql_version ) );
* Determines whether the database supports collation.