Skip to content

Commit

Permalink
Add escaping in the query builder
Browse files Browse the repository at this point in the history
  • Loading branch information
MacFJA committed Feb 17, 2021
1 parent 75a1459 commit 97669be
Show file tree
Hide file tree
Showing 15 changed files with 457 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Detection of syntax error from Redis response
- Allow multiple level of fuzziness
- Escape values in the query builder

## [1.1.0]

Expand Down
119 changes: 119 additions & 0 deletions src/Helper/EscapeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);

/*
* Copyright MacFJA
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

namespace MacFJA\RediSearch\Helper;

use function preg_quote;
use function preg_replace;
use function sprintf;
use function str_split;

class EscapeHelper
{
private const NOT_ESCAPED_CHAR_REGEX_PATTERN = '/(?<!\\\\)((?:\\\\\\\\)*)(%s)/';

private const CHAR_AT_WORD_BEGIN_REGEX_PATTERN = '/(^|\s+)(%s)/';

public static function escapeFieldName(string $text): string
{
$text = self::escapeCommon($text);

return self::escapeCharAtWordBegin($text, '-');
}

public static function escapeWord(string $text): string
{
return self::escapeCommon($text);
}

public static function escapeExactMatch(string $text): string
{
return self::escapeCommon($text);
}

public static function escapeFuzzy(string $text): string
{
return self::escapeCommon($text);
}

public static function escapeNegation(string $text): string
{
$text = self::escapeCommon($text);
$text = self::escapeCharAtWordBegin($text, '-');

for ($number = 0; $number < 10; $number++) {
$text = self::escapeCharAtWordBegin($text, (string) $number);
}

return $text;
}

public static function escapeOptional(string $text): string
{
return self::escapeCommon($text);
}

/**
* @codeCoverageIgnore
*/
private static function escapeCharAtWordBegin(string $inText, string $char): string
{
return preg_replace(
sprintf(self::CHAR_AT_WORD_BEGIN_REGEX_PATTERN, preg_quote($char, '/')),
'$1\\\\$2',
$inText
) ?? $inText;
}

/**
* @codeCoverageIgnore
*/
private static function escapeChar(string $inText, string $char): string
{
return preg_replace(
sprintf(self::NOT_ESCAPED_CHAR_REGEX_PATTERN, preg_quote($char, '/')),
'$1\\\\$2',
$inText
) ?? $inText;
}

/**
* @codeCoverageIgnore
*/
private static function escapeCommon(string $inText): string
{
$anywhere = str_split(',.<>{}[]"\':;!@#$%^&*()-+=~|');

foreach ($anywhere as $char) {
$inText = self::escapeChar($inText, $char);
}

return self::unescapeNumber($inText);
}

/**
* @codeCoverageIgnore
*/
private static function unescapeNumber(string $inText): string
{
return preg_replace('/(?:\\s|^)\\\\(-\\d)/', '$1', $inText) ?? $inText;
}
}
34 changes: 34 additions & 0 deletions src/Search/Exception/OutOfRangeLevenshteinDistanceException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

/*
* Copyright MacFJA
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

namespace MacFJA\RediSearch\Search\Exception;

use OutOfRangeException;
use function sprintf;
use Throwable;

class OutOfRangeLevenshteinDistanceException extends OutOfRangeException
{
public function __construct(int $providedDistance, int $code = 0, ?Throwable $previous = null)
{
parent::__construct(sprintf('The Levenshtein distance should be between 1 and 3 (%d provided)', $providedDistance), $code, $previous);
}
}
3 changes: 2 additions & 1 deletion src/Search/QueryBuilder/ExactMatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

namespace MacFJA\RediSearch\Search\QueryBuilder;

use MacFJA\RediSearch\Helper\EscapeHelper;
use function sprintf;

class ExactMatch implements PartialQuery
Expand All @@ -35,7 +36,7 @@ public function __construct(string $match)

public function render(): string
{
return sprintf('"%s"', $this->match);
return sprintf('"%s"', EscapeHelper::escapeExactMatch($this->match));
}

public function includeSpace(): bool
Expand Down
3 changes: 2 additions & 1 deletion src/Search/QueryBuilder/FuzzyWord.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

namespace MacFJA\RediSearch\Search\QueryBuilder;

use MacFJA\RediSearch\Helper\EscapeHelper;
use MacFJA\RediSearch\Search\Exception\OutOfRangeLevenshteinDistanceException;
use function sprintf;
use function str_repeat;
Expand All @@ -44,7 +45,7 @@ public function __construct(string $word, int $levenshteinDistance = 1)

public function render(): string
{
return sprintf('%1$s%2$s%1$s', str_repeat('%%', $this->levenshteinDistance), $this->word);
return sprintf('%1$s%2$s%1$s', str_repeat('%%', $this->levenshteinDistance), EscapeHelper::escapeFuzzy($this->word));
}

public function includeSpace(): bool
Expand Down
3 changes: 2 additions & 1 deletion src/Search/QueryBuilder/GeoFacet.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

use function in_array;
use MacFJA\RediSearch\Helper\DataHelper;
use MacFJA\RediSearch\Helper\EscapeHelper;
use MacFJA\RediSearch\Search\Exception\UnknownUnitException;
use MacFJA\RediSearch\Search\GeoFilter;
use function sprintf;
Expand Down Expand Up @@ -59,7 +60,7 @@ public function __construct(string $fieldName, float $lon, float $lat, int $radi

public function render(): string
{
return sprintf('@%s:[%f %f %f %s]', $this->fieldName, $this->lon, $this->lat, $this->radius, $this->unit);
return sprintf('@%s:[%f %f %f %s]', EscapeHelper::escapeFieldName($this->fieldName), $this->lon, $this->lat, $this->radius, $this->unit);
}

public function includeSpace(): bool
Expand Down
6 changes: 5 additions & 1 deletion src/Search/QueryBuilder/Negation.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@

namespace MacFJA\RediSearch\Search\QueryBuilder;

use function ctype_digit;
use function sprintf;
use function substr;

class Negation implements PartialQuery
{
Expand All @@ -39,8 +41,10 @@ public function __construct(PartialQuery $expression)

public function render(): string
{
$withParentheses = $this->expression->includeSpace() || ctype_digit(substr($this->expression->render(), 0, 1));

return sprintf(
$this->expression->includeSpace() ? self::WITH_SPACE_PATTERN : self::WITHOUT_SPACE_PATTERN,
true === $withParentheses ? self::WITH_SPACE_PATTERN : self::WITHOUT_SPACE_PATTERN,
$this->expression->render()
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Search/QueryBuilder/NumericFacet.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
namespace MacFJA\RediSearch\Search\QueryBuilder;

use function is_numeric;
use MacFJA\RediSearch\Helper\EscapeHelper;
use function sprintf;

class NumericFacet implements PartialQuery
Expand Down Expand Up @@ -87,7 +88,7 @@ public function render(): string
$max = ($this->isMaxInclusive ? '' : '(').$this->max;
}

return sprintf('@%s:[%s %s]', $this->fieldName, $min, $max);
return sprintf('@%s:[%s %s]', EscapeHelper::escapeFieldName($this->fieldName), $min, $max);
}

public function includeSpace(): bool
Expand Down
17 changes: 17 additions & 0 deletions src/Search/QueryBuilder/OrGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
namespace MacFJA\RediSearch\Search\QueryBuilder;

use function array_map;
use function count;
use function implode;
use function sprintf;
use function substr;
use function usort;

class OrGroup implements GroupPartialQuery
Expand Down Expand Up @@ -61,4 +63,19 @@ public function priority(): int
{
return self::PRIORITY_NORMAL;
}

public static function renderNoParentheses(PartialQuery ...$queries): string
{
if (0 === count($queries)) {
return '';
}

$group = new self();
foreach ($queries as $query) {
$group->addExpression($query);
}
$rendered = $group->render();

return substr($rendered, 1, -1) ?: $rendered;
}
}
48 changes: 48 additions & 0 deletions src/Search/QueryBuilder/RawExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

/*
* Copyright MacFJA
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

namespace MacFJA\RediSearch\Search\QueryBuilder;

class RawExpression implements PartialQuery
{
/** @var string */
private $content;

public function __construct(string $content)
{
$this->content = $content;
}

public function render(): string
{
return $this->content;
}

public function includeSpace(): bool
{
return true;
}

public function priority(): int
{
return self::PRIORITY_NORMAL;
}
}
9 changes: 7 additions & 2 deletions src/Search/QueryBuilder/TagFacet.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

namespace MacFJA\RediSearch\Search\QueryBuilder;

use function implode;
use function array_map;
use MacFJA\RediSearch\Helper\EscapeHelper;
use function sprintf;

class TagFacet implements PartialQuery
Expand All @@ -40,7 +41,11 @@ public function __construct(string $field, string ...$orValues)

public function render(): string
{
return sprintf('@%s:{%s}', $this->field, implode(' | ', $this->orValues));
$terms = OrGroup::renderNoParentheses(...array_map(function (string $orValue) {
return new Word($orValue);
}, $this->orValues));

return sprintf('@%s:{%s}', EscapeHelper::escapeFieldName($this->field), $terms);
}

public function includeSpace(): bool
Expand Down
13 changes: 9 additions & 4 deletions src/Search/QueryBuilder/TextFacet.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

namespace MacFJA\RediSearch\Search\QueryBuilder;

use function array_map;
use function count;
use function implode;
use MacFJA\RediSearch\Helper\EscapeHelper;
use function reset;
use function sprintf;
use function strpos;
Expand All @@ -48,15 +49,19 @@ public function __construct(string $field, string ...$orValues)
public function render(): string
{
if (count($this->orValues) > 1) {
return sprintf(self::WITH_SPACE_PATTERN, $this->field, implode(' | ', $this->orValues));
$terms = OrGroup::renderNoParentheses(...array_map(function (string $orValue) {
return new Word($orValue);
}, $this->orValues));

return sprintf(self::WITH_SPACE_PATTERN, EscapeHelper::escapeFieldName($this->field), $terms);
}

$value = reset($this->orValues) ?: '';
if (false === strpos($value, ' ')) {
return sprintf(self::WITHOUT_SPACE_PATTERN, $this->field, $value);
return sprintf(self::WITHOUT_SPACE_PATTERN, EscapeHelper::escapeFieldName($this->field), EscapeHelper::escapeWord($value));
}

return sprintf(self::WITH_SPACE_PATTERN, $this->field, $value);
return sprintf(self::WITH_SPACE_PATTERN, EscapeHelper::escapeFieldName($this->field), EscapeHelper::escapeWord($value));
}

public function includeSpace(): bool
Expand Down
Loading

0 comments on commit 97669be

Please sign in to comment.