Index: includes/database/database.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/database.inc,v retrieving revision 1.85 diff -u -9 -p -r1.85 database.inc --- includes/database/database.inc 4 Dec 2009 16:31:04 -0000 1.85 +++ includes/database/database.inc 10 Dec 2009 15:34:50 -0000 @@ -814,18 +814,46 @@ abstract class DatabaseConnection extend * * @return * The sanitized table name string. */ public function escapeTable($table) { return preg_replace('/[^A-Za-z0-9_]+/', '', $table); } /** + * Escape characters that work as wildcard characters in a LIKE pattern. + * + * The wildcard characters "%" and "_" as well as backslash are prefixed with + * a backslash. Use this to do a seach for a verbatim string without any + * wildcard behavior. + * + * For example, the following does a case-insensitive query for all rows whose + * name starts with $prefix: + * @code + * $result = db_query( + * 'SELECT * FROM person WHERE name LIKE :pattern', + * array(':pattern' => db_like($prefix) . '%') + * ); + * @endcode + * + * Backslash is defined as escape character for LIKE patterns in + * DatabaseCondition::mapConditionOperator(). + * + * @param $string + * The string to escape. + * @return + * The escaped string. + */ + public function escapeLike($string) { + return addcslashes($string, '\%_'); + } + + /** * Determine if there is an active transaction open. * * @return * TRUE if we're currently in a transaction, FALSE otherwise. */ public function inTransaction() { return ($this->transactionLayers > 0); } @@ -2229,18 +2257,46 @@ function db_is_active() { * The table name to escape. * @return * The escaped table name as a string. */ function db_escape_table($table) { return Database::getConnection()->escapeTable($table); } /** + * Escape characters that work as wildcard characters in a LIKE pattern. + * + * The wildcard characters "%" and "_" as well as backslash are prefixed with + * a backslash. Use this to do a seach for a verbatim string without any + * wildcard behavior. + * + * For example, the following does a case-insensitive query for all rows whose + * name starts with $prefix: + * @code + * $result = db_query( + * 'SELECT * FROM person WHERE name LIKE :pattern', + * array(':pattern' => db_like($prefix) . '%') + * ); + * @endcode + * + * Backslash is defined as escape character for LIKE patterns in + * DatabaseCondition::mapConditionOperator(). + * + * @param $string + * The string to escape. + * @return + * The escaped string. + */ +function db_like($string) { + return Database::getConnection()->escapeLike($string); +} + +/** * Retrieve the name of the currently active database driver, such as * "mysql" or "pgsql". * * @return The name of the currently active database driver. */ function db_driver() { return Database::getConnection()->driver(); } Index: includes/database/query.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/query.inc,v retrieving revision 1.36 diff -u -9 -p -r1.36 query.inc --- includes/database/query.inc 4 Dec 2009 16:31:04 -0000 1.36 +++ includes/database/query.inc 10 Dec 2009 15:34:50 -0000 @@ -1324,25 +1324,26 @@ class DatabaseCondition implements Query */ protected function mapConditionOperator($operator) { // $specials does not use drupal_static as its value never changes. static $specials = array( 'BETWEEN' => array('delimiter' => ' AND '), 'IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'), 'NOT IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'), 'IS NULL' => array('use_value' => FALSE), 'IS NOT NULL' => array('use_value' => FALSE), + // Use backslash for escaping wildcard characters. + 'LIKE' => array('postfix' => " ESCAPE '\\\\'"), // These ones are here for performance reasons. '=' => array(), '<' => array(), '>' => array(), '>=' => array(), '<=' => array(), - 'LIKE' => array(), ); if (isset($specials[$operator])) { $return = $specials[$operator]; } else { // We need to upper case because PHP index matches are case sensitive but // do not need the more expensive drupal_strtoupper because SQL statements are ASCII. $operator = strtoupper($operator); $return = isset($specials[$operator]) ? $specials[$operator] : array(); Index: includes/database/pgsql/database.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/pgsql/database.inc,v retrieving revision 1.30 diff -u -9 -p -r1.30 database.inc --- includes/database/pgsql/database.inc 18 Sep 2009 00:04:21 -0000 1.30 +++ includes/database/pgsql/database.inc 10 Dec 2009 15:34:50 -0000 @@ -114,20 +114,21 @@ class DatabaseConnection_pgsql extends D } public function databaseType() { return 'pgsql'; } public function mapConditionOperator($operator) { static $specials = array( // In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE - // statements, we need to use ILIKE instead. - 'LIKE' => array('operator' => 'ILIKE'), + // statements, we need to use ILIKE instead. Use backslash for escaping + // wildcard characters. + 'LIKE' => array('operator' => 'ILIKE', 'postfix' => " ESCAPE '\\\\'"), ); return isset($specials[$operator]) ? $specials[$operator] : NULL; } } /** * @} End of "ingroup database". */ Index: modules/simpletest/tests/database_test.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/database_test.test,v retrieving revision 1.77 diff -u -9 -p -r1.77 database_test.test --- modules/simpletest/tests/database_test.test 9 Dec 2009 19:01:14 -0000 1.77 +++ modules/simpletest/tests/database_test.test 10 Dec 2009 15:34:51 -0000 @@ -2637,18 +2637,75 @@ class DatabaseAnsiSyntaxTestCase extends function testFieldConcat() { $result = db_query('SELECT :a1 || name || :a2 || age || :a3 FROM {test} WHERE age = :age', array( ':a1' => 'The age of ', ':a2' => ' is ', ':a3' => '.', ':age' => 25, )); $this->assertIdentical($result->fetchField(), 'The age of John is 25.', t('Field ANSI Concat works.')); } + + /** + * Test escaping of LIKE wildcards. + */ + function testLikeEscape() { + db_insert('test') + ->fields(array( + 'name' => 'Ring_', + )) + ->execute(); + + // Match both "Ringo" and "Ring_". + $num_matches = db_select('test', 't') + ->condition('name', 'Ring_', 'LIKE') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertIdentical($num_matches, '2', t('Found 2 records.')); + // Match only "Ring_" using a LIKE expression with no wildcards. + $num_matches = db_select('test', 't') + ->condition('name', db_like('Ring_'), 'LIKE') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertIdentical($num_matches, '1', t('Found 1 record.')); + } + + /** + * Test LIKE query containing a backslash. + */ + function testLikeBackslash() { + db_insert('test') + ->fields(array('name')) + ->values(array( + 'name' => 'abcde\f', + )) + ->values(array( + 'name' => 'abc%\_', + )) + ->execute(); + + // Match both rows using a LIKE expression with two wildcards and a verbatim + // backslash. + $num_matches = db_select('test', 't') + ->condition('name', 'abc%\\\\_', 'LIKE') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertIdentical($num_matches, '2', t('Found 2 records.')); + // Match only the former using a LIKE expression with no wildcards. + $num_matches = db_select('test', 't') + ->condition('name', db_like('abc%\_'), 'LIKE') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertIdentical($num_matches, '1', t('Found 1 record.')); + } } /** * Test invalid data handling. */ class DatabaseInvalidDataTestCase extends DatabaseTestCase { public static function getInfo() { return array( 'name' => 'Invalid data',