diff --git a/app/Search/SearchOptions.php b/app/Search/SearchOptions.php index af146d5fd..d38fc8d57 100644 --- a/app/Search/SearchOptions.php +++ b/app/Search/SearchOptions.php @@ -78,7 +78,7 @@ class SearchOptions ]; $patterns = [ - 'exacts' => '/"(.*?)(? '/"((?:\\\\.|[^"\\\\])*)"/', 'tags' => '/\[(.*?)\]/', 'filters' => '/\{(.*?)\}/', ]; @@ -93,9 +93,9 @@ class SearchOptions } } - // Unescape exacts + // Unescape exacts and backslash escapes foreach ($terms['exacts'] as $index => $exact) { - $terms['exacts'][$index] = str_replace('\"', '"', $exact); + $terms['exacts'][$index] = static::decodeEscapes($exact); } // Parse standard terms @@ -118,6 +118,28 @@ class SearchOptions return $terms; } + /** + * Decode backslash escaping within the input string. + */ + protected static function decodeEscapes(string $input): string + { + $decoded = ""; + $escaping = false; + + foreach (str_split($input) as $char) { + if ($escaping) { + $decoded .= $char; + $escaping = false; + } else if ($char === '\\') { + $escaping = true; + } else { + $decoded .= $char; + } + } + + return $decoded; + } + /** * Parse a standard search term string into individual search terms and * convert any required terms to exact matches. This is done since some @@ -156,7 +178,8 @@ class SearchOptions $parts = $this->searches; foreach ($this->exacts as $term) { - $escaped = str_replace('"', '\"', $term); + $escaped = str_replace('\\', '\\\\', $term); + $escaped = str_replace('"', '\"', $escaped); $parts[] = '"' . $escaped . '"'; } diff --git a/tests/Commands/ResetMfaCommandTest.php b/tests/Commands/ResetMfaCommandTest.php index 85f8f6430..39c8c689b 100644 --- a/tests/Commands/ResetMfaCommandTest.php +++ b/tests/Commands/ResetMfaCommandTest.php @@ -11,7 +11,7 @@ class ResetMfaCommandTest extends TestCase public function test_command_requires_email_or_id_option() { $this->artisan('bookstack:reset-mfa') - ->expectsOutput('Either a --id= or --email= option must be provided.') + ->expectsOutputToContain('Either a --id= or --email= option must be provided.') ->assertExitCode(1); } diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index a070ce3fa..fbb47226e 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -466,10 +466,10 @@ class EntitySearchTest extends TestCase $search = $this->asEditor()->get('/search?term=' . urlencode('\\\\cat\\dog')); $search->assertSee($page->getUrl(), false); - $search = $this->asEditor()->get('/search?term=' . urlencode('"\\dog\\"')); + $search = $this->asEditor()->get('/search?term=' . urlencode('"\\dog\\\\"')); $search->assertSee($page->getUrl(), false); - $search = $this->asEditor()->get('/search?term=' . urlencode('"\\badger\\"')); + $search = $this->asEditor()->get('/search?term=' . urlencode('"\\badger\\\\"')); $search->assertDontSee($page->getUrl(), false); $search = $this->asEditor()->get('/search?term=' . urlencode('[\\Categorylike%\\fluffy]')); diff --git a/tests/Entity/SearchOptionsTest.php b/tests/Entity/SearchOptionsTest.php index 8bc9d02e4..ea4d727a4 100644 --- a/tests/Entity/SearchOptionsTest.php +++ b/tests/Entity/SearchOptionsTest.php @@ -20,9 +20,9 @@ class SearchOptionsTest extends TestCase public function test_from_string_properly_parses_escaped_quotes() { - $options = SearchOptions::fromString('"\"cat\"" surprise "\"\"" "\"donkey" "\""'); + $options = SearchOptions::fromString('"\"cat\"" surprise "\"\"" "\"donkey" "\"" "\\\\"'); - $this->assertEquals(['"cat"', '""', '"donkey', '"'], $options->exacts); + $this->assertEquals(['"cat"', '""', '"donkey', '"', '\\'], $options->exacts); } public function test_to_string_includes_all_items_in_the_correct_format() @@ -40,13 +40,13 @@ class SearchOptionsTest extends TestCase } } - public function test_to_string_escapes_quotes_as_expected() + public function test_to_string_escapes_as_expected() { $options = new SearchOptions(); - $options->exacts = ['"cat"', '""', '"donkey', '"']; + $options->exacts = ['"cat"', '""', '"donkey', '"', '\\', '\\"']; $output = $options->toString(); - $this->assertEquals('"\"cat\"" "\"\"" "\"donkey" "\""', $output); + $this->assertEquals('"\"cat\"" "\"\"" "\"donkey" "\"" "\\\\" "\\\\\""', $output); } public function test_correct_filter_values_are_set_from_string()