Skip to content

Commit 43357cc

Browse files
committed
Cater for reserved word column/table names.
1 parent 05f4ba0 commit 43357cc

File tree

2 files changed

+59
-8
lines changed

2 files changed

+59
-8
lines changed

features/db-search.feature

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,3 +901,33 @@ Feature: Search through the database
901901
"""
902902
Warning: Unrecognized percent color code '%x' for 'match_color'.
903903
"""
904+
905+
Scenario: Search should cater for field/table names that use reserved words or unusual characters
906+
Given a WP install
907+
And a esc_sql_ident.sql file:
908+
"""
909+
CREATE TABLE `TABLE` (`KEY` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `VALUES` TEXT, `back``tick` TEXT, `single'double"quote` TEXT, PRIMARY KEY (`KEY`) );
910+
INSERT INTO `TABLE` (`VALUES`, `back``tick`, `single'double"quote`) VALUES ('v"v`v\'v\\v_v1', 'v"v`v\'v\\v_v1', 'v"v`v\'v\\v_v1' );
911+
INSERT INTO `TABLE` (`VALUES`, `back``tick`, `single'double"quote`) VALUES ('v"v`v\'v\\v_v2', 'v"v`v\'v\\v_v2', 'v"v`v\'v\\v_v2' );
912+
"""
913+
914+
When I run `wp db query "SOURCE esc_sql_ident.sql;"`
915+
Then STDERR should be empty
916+
917+
When I run `wp db search 'v_v' TABLE`
918+
Then STDOUT should be:
919+
"""
920+
TABLE:VALUES
921+
1:v"v`v'v\v_v1
922+
TABLE:VALUES
923+
2:v"v`v'v\v_v2
924+
TABLE:back`tick
925+
1:v"v`v'v\v_v1
926+
TABLE:back`tick
927+
2:v"v`v'v\v_v2
928+
TABLE:single'double"quote
929+
1:v"v`v'v\v_v1
930+
TABLE:single'double"quote
931+
2:v"v`v'v\v_v2
932+
"""
933+
And STDERR should be empty

src/DB_Command.php

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -883,20 +883,22 @@ public function search( $args, $assoc_args ) {
883883
}
884884
continue;
885885
}
886+
$table_sql = self::esc_sql_ident( $table );
886887
$column_count += count( $text_columns );
887888
if ( ! $primary_keys ) {
888889
WP_CLI::warning( "No primary key for table '$table'. No row ids will be outputted." );
889890
$primary_key = $primary_key_sql = '';
890891
} else {
891892
$primary_key = array_shift( $primary_keys );
892-
$primary_key_sql = $primary_key . ', ';
893+
$primary_key_sql = self::esc_sql_ident( $primary_key ) . ', ';
893894
}
894895

895896
foreach ( $text_columns as $column ) {
897+
$column_sql = self::esc_sql_ident( $column );
896898
if ( $regex ) {
897-
$results = $wpdb->get_results( "SELECT {$primary_key_sql}{$column} FROM {$table}" );
899+
$results = $wpdb->get_results( "SELECT {$primary_key_sql}{$column_sql} FROM {$table_sql}" );
898900
} else {
899-
$results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_key_sql}{$column} FROM {$table} WHERE {$column} LIKE %s;", $esc_like_search ) );
901+
$results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_key_sql}{$column_sql} FROM {$table_sql} WHERE {$column_sql} LIKE %s;", $esc_like_search ) );
900902
}
901903
if ( $results ) {
902904
$row_count += count( $results );
@@ -966,12 +968,12 @@ public function search( $args, $assoc_args ) {
966968

967969
private static function get_create_query() {
968970

969-
$create_query = sprintf( 'CREATE DATABASE `%s`', DB_NAME );
971+
$create_query = sprintf( 'CREATE DATABASE %s', self::esc_sql_ident( DB_NAME ) );
970972
if ( defined( 'DB_CHARSET' ) && constant( 'DB_CHARSET' ) ) {
971-
$create_query .= sprintf( ' DEFAULT CHARSET `%s`', constant( 'DB_CHARSET' ) );
973+
$create_query .= sprintf( ' DEFAULT CHARSET %s', self::esc_sql_ident( DB_CHARSET ) );
972974
}
973975
if ( defined( 'DB_COLLATE' ) && constant( 'DB_COLLATE' ) ) {
974-
$create_query .= sprintf( ' DEFAULT COLLATE `%s`', constant( 'DB_COLLATE' ) );
976+
$create_query .= sprintf( ' DEFAULT COLLATE %s', self::esc_sql_ident( DB_COLLATE ) );
975977
}
976978
return $create_query;
977979
}
@@ -1005,10 +1007,11 @@ private static function run( $cmd, $assoc_args = array(), $descriptors = null )
10051007
private static function get_columns( $table ) {
10061008
global $wpdb;
10071009

1010+
$table_sql = self::esc_sql_ident( $table );
10081011
$primary_keys = $text_columns = $all_columns = array();
10091012
$suppress_errors = $wpdb->suppress_errors();
1010-
if ( ( $results = $wpdb->get_results( "DESCRIBE $table" ) ) ) {
1011-
foreach ( $wpdb->get_results( "DESCRIBE $table" ) as $col ) {
1013+
if ( ( $results = $wpdb->get_results( "DESCRIBE $table_sql" ) ) ) {
1014+
foreach ( $results as $col ) {
10121015
if ( 'PRI' === $col->Key ) {
10131016
$primary_keys[] = $col->Field;
10141017
}
@@ -1058,6 +1061,24 @@ private static function esc_like( $old ) {
10581061
return $old;
10591062
}
10601063

1064+
/**
1065+
* Escapes (backticks) MySQL identifiers (aka schema object names) - i.e. column names, table names, and database/index/alias/view etc names.
1066+
* See https://dev.mysql.com/doc/refman/5.5/en/identifiers.html
1067+
*
1068+
* @param string|array $idents A single identifier or an array of identifiers.
1069+
* @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings.
1070+
*/
1071+
private static function esc_sql_ident( $idents ) {
1072+
$backtick = function ( $v ) {
1073+
// Escape any backticks in the identifier by doubling.
1074+
return '`' . str_replace( '`', '``', $v ) . '`';
1075+
};
1076+
if ( is_string( $idents ) ) {
1077+
return $backtick( $idents );
1078+
}
1079+
return array_map( $backtick, $idents );
1080+
}
1081+
10611082
/**
10621083
* Gets the color codes from the options if any, and returns the passed in array colorized with 2 elements per entry, a color code (or '') and a reset (or '').
10631084
*

0 commit comments

Comments
 (0)