Improve quote handling in search strings

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl
2021-04-13 09:14:49 +02:00
parent e77ca1997b
commit 0f3bbe332b
2 changed files with 161 additions and 12 deletions

View File

@@ -47,11 +47,23 @@ class FilterStringParser {
if (empty($filter)) {
return $query;
}
$tokens = preg_split('/\s(?=([^"]*"[^"]*")*[^"]*$)/', $filter);
foreach ($tokens as $token) {
/**
* Match search tokens that are separated by spaces
* do not match spaces that are surrounded by single or double quotes
* in order to still match quotes
* e.g.:
* - test
* - test:query
* - test:<123
* - test:"1 2 3"
* - test:>="2020-01-01"
*/
$searchQueryExpression = '/((\w+:(<|<=|>|>=)?)?("([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')|[^\s]+)/';
preg_match_all($searchQueryExpression, $filter, $matches, PREG_SET_ORDER, 0);
foreach ($matches as $match) {
$token = $match[0];
if (!$this->parseFilterToken($query, $token)) {
$token = ($token[0] === '"' && $token[mb_strlen($token) - 1] === '"') ? mb_substr($token, 1, -1): $token;
$query->addTextToken($token);
$query->addTextToken($this->removeQuotes($token));
}
}
@@ -81,27 +93,31 @@ class FilterStringParser {
($orEquals ? SearchQuery::COMPARATOR_EQUAL : 0)
);
}
$value = ($value[0] === '"' && $value[mb_strlen($value) - 1] === '"') ? mb_substr($value, 1, -1): $value;
$query->addDuedate(new DateQueryParameter('date', $comparator, $value));
$query->addDuedate(new DateQueryParameter('date', $comparator, $this->removeQuotes($value)));
return true;
case 'title':
$query->addTitle(new StringQueryParameter('title', SearchQuery::COMPARATOR_EQUAL, $param));
$query->addTitle(new StringQueryParameter('title', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
case 'description':
$query->addDescription(new StringQueryParameter('description', SearchQuery::COMPARATOR_EQUAL, $param));
$query->addDescription(new StringQueryParameter('description', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
case 'list':
$query->addStack(new StringQueryParameter('list', SearchQuery::COMPARATOR_EQUAL, $param));
$query->addStack(new StringQueryParameter('list', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
case 'tag':
$query->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, $param));
$query->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
case 'assigned':
$query->addAssigned(new StringQueryParameter('assigned', SearchQuery::COMPARATOR_EQUAL, $param));
$query->addAssigned(new StringQueryParameter('assigned', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
}
return false;
}
protected function removeQuotes(string $token): string {
$token = ($token[0] === '"' && $token[mb_strlen($token) - 1] === '"') ? mb_substr($token, 1, -1): $token;
$token = ($token[0] === '\'' && $token[mb_strlen($token) - 1] === '\'') ? mb_substr($token, 1, -1): $token;
return $token;
}
}

View File

@@ -0,0 +1,133 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search;
use OCA\Deck\Search\Query\DateQueryParameter;
use OCA\Deck\Search\Query\SearchQuery;
use OCA\Deck\Search\Query\StringQueryParameter;
use OCP\IL10N;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
class FilterStringParserTest extends TestCase {
private $l10n;
private $parser;
public function setUp(): void {
$this->l10n = $this->createMock(IL10N::class);
$this->parser = new FilterStringParser($this->l10n);
}
public function testParseEmpty() {
$result = $this->parser->parse(null);
$expected = new SearchQuery();
Assert::assertEquals($expected, $result);
}
public function testParseTextTokens() {
$result = $this->parser->parse('a b c');
$expected = new SearchQuery();
$expected->addTextToken('a');
$expected->addTextToken('b');
$expected->addTextToken('c');
Assert::assertEquals($expected, $result);
}
public function testParseTextToken() {
$result = $this->parser->parse('abc');
$expected = new SearchQuery();
$expected->addTextToken('abc');
Assert::assertEquals($expected, $result);
}
public function testParseTextTokenQuotes() {
$result = $this->parser->parse('a b c "a b c" tag:abc tag:"a b c" tag:\'d e f\'');
$expected = new SearchQuery();
$expected->addTextToken('a');
$expected->addTextToken('b');
$expected->addTextToken('c');
$expected->addTextToken('a b c');
$expected->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, 'abc'));
$expected->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, 'a b c'));
$expected->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, 'd e f'));
Assert::assertEquals($expected, $result);
}
public function testParseTagComparatorNotSupported() {
$result = $this->parser->parse('tag:<"a tag"');
$expected = new SearchQuery();
$expected->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, '<"a tag"'));
Assert::assertEquals($expected, $result);
}
public function testParseTextTokenQuotesSingle() {
$result = $this->parser->parse('a b c \'a b c\'');
$expected = new SearchQuery();
$expected->addTextToken('a');
$expected->addTextToken('b');
$expected->addTextToken('c');
$expected->addTextToken('a b c');
Assert::assertEquals($expected, $result);
}
public function testParseTextTokenQuotesWrong() {
$result = $this->parser->parse('"a b" c"');
$expected = new SearchQuery();
$expected->addTextToken('a b');
$expected->addTextToken('c"');
Assert::assertEquals($expected, $result);
}
public function dataParseDate() {
return [
['date:today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, 'today')], []],
['date:>today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_MORE, 'today')], []],
['date:>=today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_MORE_EQUAL, 'today')], []],
['date:<today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_LESS, 'today')], []],
['date:<=today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_LESS_EQUAL, 'today')], []],
['date:<+today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_LESS, '+today')], []],
['date:<>today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_LESS, '>today')], []],
['date:=today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, '=today')], []],
['date:today todo', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, 'today')], ['todo']],
['date:"last day of next month" todo', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, 'last day of next month')], ['todo']],
['date:"last day of next month" "todo task" task', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, 'last day of next month')], ['todo task', 'task']],
];
}
/**
* @dataProvider dataParseDate
*/
public function testParseDate($query, $dates, array $tokens) {
$result = $this->parser->parse($query);
$expected = new SearchQuery();
foreach ($dates as $date) {
$expected->addDuedate($date);
}
foreach ($tokens as $token) {
$expected->addTextToken($token);
}
Assert::assertEquals($expected, $result);
}
}