Skip to content

Escape LIKE metacharacters #3013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions lib/Doctrine/DBAL/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
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
Expand Down Expand Up @@ -3578,4 +3583,26 @@ public function getStringLiteralQuoteCharacter()
{
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 '%_';
}
}
5 changes: 5 additions & 0 deletions lib/Doctrine/DBAL/Platforms/SQLServer2008Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,9 @@ protected function getReservedKeywordsClass()
{
return Keywords\SQLServer2008Keywords::class;
}

protected function getLikeWildcardCharacters() : string
{
return parent::getLikeWildcardCharacters() . '[]^';
}
}
13 changes: 9 additions & 4 deletions lib/Doctrine/DBAL/Query/Expression/ExpressionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
namespace Doctrine\DBAL\Query\Expression;

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.
Expand Down Expand Up @@ -254,9 +257,10 @@ public function isNotNull($x)
*
* @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)) : '');
}

/**
Expand All @@ -267,9 +271,10 @@ public function like($x, $y)
*
* @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)) : '');
}

/**
Expand Down
29 changes: 29 additions & 0 deletions tests/Doctrine/Tests/DBAL/Functional/LikeWildcardsEscapingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1468,4 +1468,12 @@ public function getGeneratesFloatDeclarationSQL()
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/', '\\')
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,33 @@ public function testNotInWithPlaceholder()
{
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 '💩'",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classy 💩

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IKR?

$this->expr->notLike('p.description', "'20💩%'", "'💩'")
);
}
}