Skip to content

Commit 1501796

Browse files
authored
Merge pull request #40 from wp-cli/esc_sql_ident
Cater for reserved word column/table names in db search.
2 parents 229c00e + 43357cc commit 1501796

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
@@ -915,3 +915,33 @@ Feature: Search through the database
915915
"""
916916
Warning: Unrecognized percent color code '%x' for 'match_color'.
917917
"""
918+
919+
Scenario: Search should cater for field/table names that use reserved words or unusual characters
920+
Given a WP install
921+
And a esc_sql_ident.sql file:
922+
"""
923+
CREATE TABLE `TABLE` (`KEY` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `VALUES` TEXT, `back``tick` TEXT, `single'double"quote` TEXT, PRIMARY KEY (`KEY`) );
924+
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' );
925+
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' );
926+
"""
927+
928+
When I run `wp db query "SOURCE esc_sql_ident.sql;"`
929+
Then STDERR should be empty
930+
931+
When I run `wp db search 'v_v' TABLE`
932+
Then STDOUT should be:
933+
"""
934+
TABLE:VALUES
935+
1:v"v`v'v\v_v1
936+
TABLE:VALUES
937+
2:v"v`v'v\v_v2
938+
TABLE:back`tick
939+
1:v"v`v'v\v_v1
940+
TABLE:back`tick
941+
2:v"v`v'v\v_v2
942+
TABLE:single'double"quote
943+
1:v"v`v'v\v_v1
944+
TABLE:single'double"quote
945+
2:v"v`v'v\v_v2
946+
"""
947+
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)