diff --git a/src/Search/Comb/Comb.php b/src/Search/Comb/Comb.php index 6817e01cc27..7e2693f0e5e 100644 --- a/src/Search/Comb/Comb.php +++ b/src/Search/Comb/Comb.php @@ -710,7 +710,7 @@ private function searchOverData($params, $raw_query) */ private function removeDisallowedMatches($params) { - $disallowed = '#'.implode('|', $params['disallowed']).'#iu'; + $disallowed = $this->pattern($params['disallowed']); $new_data = []; // this only applies to boolean mode @@ -740,7 +740,7 @@ private function removeDisallowedMatches($params) // check for required, one word at a time so each is a single linear scan foreach ($params['required'] as $word) { - if (! preg_match('#'.$word.'#iu', $record)) { + if (! preg_match($this->pattern([$word]), $record)) { // a required word is missing, we don't want this throw new CombException(''); } @@ -755,6 +755,11 @@ private function removeDisallowedMatches($params) return $new_data; } + private function pattern(array $words): string + { + return '#'.implode('|', array_map(fn (string $word) => preg_quote($word, '#'), $words)).'#iu'; + } + /** * Removes properties that are on the exclusion list. * diff --git a/tests/Search/CombTest.php b/tests/Search/CombTest.php index 95205b9eb09..4df7a7f01c4 100644 --- a/tests/Search/CombTest.php +++ b/tests/Search/CombTest.php @@ -311,6 +311,38 @@ public function it_filters_out_results_with_disallowed_words_where_results_are_a $this->assertEquals(['Chicken & Sweetcorn Soup'], collect($results['data'] ?? [])->pluck('data.title')->all()); } + #[Test] + #[DataProvider('regexMetacharactersProvider')] + public function it_treats_regex_metacharacters_in_boolean_words_literally($term, $expected) + { + $comb = new Comb([ + ['title' => 'node.js guide'], + ['title' => 'nodeXjs guide'], + ['title' => 'c++ guide'], + ['title' => 'c guide'], + ]); + + try { + $results = $comb->lookUp($term); + } catch (NoResultsFound $e) { + $results = []; + } + + $this->assertEquals($expected, collect($results['data'] ?? [])->pluck('data.title')->all()); + } + + public static function regexMetacharactersProvider() + { + return [ + 'required word with a dot' => ['+node.js', ['node.js guide']], + 'disallowed word with a dot' => ['guide -node.js', ['nodeXjs guide', 'c++ guide', 'c guide']], + 'required word with plus signs' => ['+c++', ['c++ guide']], + 'disallowed word with plus signs' => ['guide -c++', ['node.js guide', 'nodeXjs guide', 'c guide']], + 'required word with a trailing backslash' => ['+foo\\', []], + 'disallowed word with a trailing backslash' => ['guide -foo\\', ['node.js guide', 'nodeXjs guide', 'c++ guide', 'c guide']], + ]; + } + #[Test] #[DataProvider('requiredWordsProvider')] public function it_filters_out_results_without_required_words($term, $expected)