Unverified Commit ea7657f0 authored by Marco Pivetta's avatar Marco Pivetta Committed by GitHub

Merge pull request #3013 from greg0ire/escape_like_literals

Escape LIKE metacharacters
parents 69742aee f397fe04
...@@ -42,6 +42,11 @@ use Doctrine\DBAL\Schema\TableDiff; ...@@ -42,6 +42,11 @@ use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\TransactionIsolationLevel; use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types; use Doctrine\DBAL\Types;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Type;
use function addcslashes;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function strlen;
/** /**
* Base class for all DatabasePlatforms. The DatabasePlatforms are the central * Base class for all DatabasePlatforms. The DatabasePlatforms are the central
...@@ -3578,4 +3583,26 @@ abstract class AbstractPlatform ...@@ -3578,4 +3583,26 @@ abstract class AbstractPlatform
{ {
return "'"; return "'";
} }
/**
* Escapes metacharacters in a string intended to be used with a LIKE
* operator.
*
* @param string $inputString a literal, unquoted string
* @param string $escapeChar should be reused by the caller in the LIKE
* expression.
*/
final public function escapeStringForLike(string $inputString, string $escapeChar) : string
{
return preg_replace(
'~([' . preg_quote($this->getLikeWildcardCharacters() . $escapeChar, '~') . '])~u',
addcslashes($escapeChar, '\\') . '$1',
$inputString
);
}
protected function getLikeWildcardCharacters() : string
{
return '%_';
}
} }
...@@ -116,4 +116,9 @@ class SQLServer2008Platform extends SQLServer2005Platform ...@@ -116,4 +116,9 @@ class SQLServer2008Platform extends SQLServer2005Platform
{ {
return Keywords\SQLServer2008Keywords::class; return Keywords\SQLServer2008Keywords::class;
} }
protected function getLikeWildcardCharacters() : string
{
return parent::getLikeWildcardCharacters() . '[]^';
}
} }
...@@ -20,6 +20,9 @@ ...@@ -20,6 +20,9 @@
namespace Doctrine\DBAL\Query\Expression; namespace Doctrine\DBAL\Query\Expression;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
use function func_get_arg;
use function func_num_args;
use function sprintf;
/** /**
* ExpressionBuilder class is responsible to dynamically create SQL query parts. * ExpressionBuilder class is responsible to dynamically create SQL query parts.
...@@ -254,9 +257,10 @@ class ExpressionBuilder ...@@ -254,9 +257,10 @@ class ExpressionBuilder
* *
* @return string * @return string
*/ */
public function like($x, $y) public function like($x, $y/*, ?string $escapeChar = null */)
{ {
return $this->comparison($x, 'LIKE', $y); return $this->comparison($x, 'LIKE', $y) .
(func_num_args() >= 3 ? sprintf(' ESCAPE %s', func_get_arg(2)) : '');
} }
/** /**
...@@ -267,9 +271,10 @@ class ExpressionBuilder ...@@ -267,9 +271,10 @@ class ExpressionBuilder
* *
* @return string * @return string
*/ */
public function notLike($x, $y) public function notLike($x, $y/*, ?string $escapeChar = null */)
{ {
return $this->comparison($x, 'NOT LIKE', $y); return $this->comparison($x, 'NOT LIKE', $y) .
(func_num_args() >= 3 ? sprintf(' ESCAPE %s', func_get_arg(2)) : '');
} }
/** /**
......
<?php
namespace Doctrine\Tests\DBAL\Functional;
use Doctrine\Tests\DbalFunctionalTestCase;
use function sprintf;
use function str_replace;
final class LikeWildcardsEscapingTest extends DbalFunctionalTestCase
{
public function testFetchLikeExpressionResult() : void
{
$string = '_25% off_ your next purchase \o/ [$̲̅(̲̅5̲̅)̲̅$̲̅] (^̮^)';
$escapeChar = '!';
$databasePlatform = $this->_conn->getDatabasePlatform();
$stmt = $this->_conn->prepare(str_replace(
'1',
sprintf(
"(CASE WHEN '%s' LIKE '%s' ESCAPE '%s' THEN 1 ELSE 0 END)",
$string,
$databasePlatform->escapeStringForLike($string, $escapeChar),
$escapeChar
),
$databasePlatform->getDummySelectSQL()
));
$stmt->execute();
$this->assertTrue((bool) $stmt->fetchColumn());
}
}
...@@ -1468,4 +1468,12 @@ abstract class AbstractPlatformTestCase extends \Doctrine\Tests\DbalTestCase ...@@ -1468,4 +1468,12 @@ abstract class AbstractPlatformTestCase extends \Doctrine\Tests\DbalTestCase
array(array('precision' => 8, 'scale' => 2), 'DOUBLE PRECISION'), array(array('precision' => 8, 'scale' => 2), 'DOUBLE PRECISION'),
); );
} }
public function testItEscapesStringsForLike() : void
{
self::assertSame(
'\_25\% off\_ your next purchase \\\\o/',
$this->_platform->escapeStringForLike('_25% off_ your next purchase \o/', '\\')
);
}
} }
...@@ -219,4 +219,33 @@ class ExpressionBuilderTest extends \Doctrine\Tests\DbalTestCase ...@@ -219,4 +219,33 @@ class ExpressionBuilderTest extends \Doctrine\Tests\DbalTestCase
{ {
self::assertEquals('u.groups NOT IN (:values)', $this->expr->notIn('u.groups', ':values')); self::assertEquals('u.groups NOT IN (:values)', $this->expr->notIn('u.groups', ':values'));
} }
public function testLikeWithoutEscape()
{
self::assertEquals("a.song LIKE 'a virgin'", $this->expr->like('a.song', "'a virgin'"));
}
public function testLikeWithEscape()
{
self::assertEquals(
"a.song LIKE 'a virgin' ESCAPE '💩'",
$this->expr->like('a.song', "'a virgin'", "'💩'")
);
}
public function testNotLikeWithoutEscape()
{
self::assertEquals(
"s.last_words NOT LIKE 'this'",
$this->expr->notLike('s.last_words', "'this'")
);
}
public function testNotLikeWithEscape()
{
self::assertEquals(
"p.description NOT LIKE '20💩%' ESCAPE '💩'",
$this->expr->notLike('p.description', "'20💩%'", "'💩'")
);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment