diff --git a/lib/Search/FilterStringParser.php b/lib/Search/FilterStringParser.php index 3d484e268..ab2afd012 100644 --- a/lib/Search/FilterStringParser.php +++ b/lib/Search/FilterStringParser.php @@ -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; + } } diff --git a/tests/unit/Search/FilterStringParserTest.php b/tests/unit/Search/FilterStringParserTest.php new file mode 100644 index 000000000..91c94163c --- /dev/null +++ b/tests/unit/Search/FilterStringParserTest.php @@ -0,0 +1,133 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +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_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); + } +}