From 0a22a6618964ec7510e07c7dc3283012a114851b Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 23 Jul 2019 23:55:06 +0200 Subject: [PATCH] Prevent MySQL search operators from taking effect We do not want to inherit MySQL's fulltext query language, so let's just drop all non-word characters from the search term. Fixes #1498. --- .../Search/Gambit/FulltextGambit.php | 7 +-- .../ListDiscussionsControllerTest.php | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/Discussion/Search/Gambit/FulltextGambit.php b/src/Discussion/Search/Gambit/FulltextGambit.php index 15e6f53e0..fe82226dc 100644 --- a/src/Discussion/Search/Gambit/FulltextGambit.php +++ b/src/Discussion/Search/Gambit/FulltextGambit.php @@ -27,9 +27,10 @@ class FulltextGambit implements GambitInterface throw new LogicException('This gambit can only be applied on a DiscussionSearch'); } - // The @ character crashes fulltext searches on InnoDB tables. - // See https://bugs.mysql.com/bug.php?id=74042 - $bit = str_replace('@', '*', $bit); + // Replace all non-word characters with spaces. + // We do this to prevent MySQL fulltext search boolean mode from taking + // effect: https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html + $bit = preg_replace('/[^\p{L}\p{N}_]+/u', ' ', $bit); $query = $search->getQuery(); $grammar = $query->getGrammar(); diff --git a/tests/integration/api/Controller/ListDiscussionsControllerTest.php b/tests/integration/api/Controller/ListDiscussionsControllerTest.php index ca4fe1947..75a0dd3ff 100644 --- a/tests/integration/api/Controller/ListDiscussionsControllerTest.php +++ b/tests/integration/api/Controller/ListDiscussionsControllerTest.php @@ -98,4 +98,52 @@ class ListDiscussionsControllerTest extends ApiControllerTestCase // Order-independent comparison $this->assertEquals(['2', '3'], $ids, 'IDs do not match', 0.0, 10, true); } + + /** + * @test + */ + public function ignores_non_word_characters_when_searching() + { + $this->database()->table('posts')->insert([ + ['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '

not in text

'], + ['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '

lightsail in text

'], + ]); + + $this->database()->table('discussions')->insert([ + ['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 2, 'comment_count' => 1], + ['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 3, 'comment_count' => 1], + ]); + + $response = $this->callWith([], [ + 'filter' => ['q' => 'lightsail+'], + 'include' => 'mostRelevantPost' + ]); + $data = json_decode($response->getBody()->getContents(), true); + $ids = array_map(function ($row) { + return $row['id']; + }, $data['data']); + + // Order-independent comparison + $this->assertEquals(['2', '3'], $ids, 'IDs do not match', 0.0, 10, true); + } + + /** + * @test + */ + public function search_for_special_characters_gives_empty_result() + { + $response = $this->callWith([], [ + 'filter' => ['q' => '*'], + 'include' => 'mostRelevantPost' + ]); + $data = json_decode($response->getBody()->getContents(), true); + $this->assertEquals([], $data['data']); + + $response = $this->callWith([], [ + 'filter' => ['q' => '@'], + 'include' => 'mostRelevantPost' + ]); + $data = json_decode($response->getBody()->getContents(), true); + $this->assertEquals([], $data['data']); + } }