Search: Added further backslash handling

Added due to now not being able to perform an exact search where
contains a trailing backslash.
Now all backslashes in exact terms are consided escape chars
and require escaping themselves.
Potential breaking change due to search syntax handling change.

Related to #4535.
This commit is contained in:
Dan Brown 2023-09-23 13:41:10 +01:00
parent fb417828a4
commit f77bb01b51
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
4 changed files with 35 additions and 12 deletions

View file

@ -78,7 +78,7 @@ class SearchOptions
]; ];
$patterns = [ $patterns = [
'exacts' => '/"(.*?)(?<!\\\)"/', 'exacts' => '/"((?:\\\\.|[^"\\\\])*)"/',
'tags' => '/\[(.*?)\]/', 'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/', 'filters' => '/\{(.*?)\}/',
]; ];
@ -93,9 +93,9 @@ class SearchOptions
} }
} }
// Unescape exacts // Unescape exacts and backslash escapes
foreach ($terms['exacts'] as $index => $exact) { foreach ($terms['exacts'] as $index => $exact) {
$terms['exacts'][$index] = str_replace('\"', '"', $exact); $terms['exacts'][$index] = static::decodeEscapes($exact);
} }
// Parse standard terms // Parse standard terms
@ -118,6 +118,28 @@ class SearchOptions
return $terms; 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 * Parse a standard search term string into individual search terms and
* convert any required terms to exact matches. This is done since some * convert any required terms to exact matches. This is done since some
@ -156,7 +178,8 @@ class SearchOptions
$parts = $this->searches; $parts = $this->searches;
foreach ($this->exacts as $term) { foreach ($this->exacts as $term) {
$escaped = str_replace('"', '\"', $term); $escaped = str_replace('\\', '\\\\', $term);
$escaped = str_replace('"', '\"', $escaped);
$parts[] = '"' . $escaped . '"'; $parts[] = '"' . $escaped . '"';
} }

View file

@ -11,7 +11,7 @@ class ResetMfaCommandTest extends TestCase
public function test_command_requires_email_or_id_option() public function test_command_requires_email_or_id_option()
{ {
$this->artisan('bookstack:reset-mfa') $this->artisan('bookstack:reset-mfa')
->expectsOutput('Either a --id=<number> or --email=<email> option must be provided.') ->expectsOutputToContain('Either a --id=<number> or --email=<email> option must be provided.')
->assertExitCode(1); ->assertExitCode(1);
} }

View file

@ -466,10 +466,10 @@ class EntitySearchTest extends TestCase
$search = $this->asEditor()->get('/search?term=' . urlencode('\\\\cat\\dog')); $search = $this->asEditor()->get('/search?term=' . urlencode('\\\\cat\\dog'));
$search->assertSee($page->getUrl(), false); $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->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->assertDontSee($page->getUrl(), false);
$search = $this->asEditor()->get('/search?term=' . urlencode('[\\Categorylike%\\fluffy]')); $search = $this->asEditor()->get('/search?term=' . urlencode('[\\Categorylike%\\fluffy]'));

View file

@ -20,9 +20,9 @@ class SearchOptionsTest extends TestCase
public function test_from_string_properly_parses_escaped_quotes() 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() 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 = new SearchOptions();
$options->exacts = ['"cat"', '""', '"donkey', '"']; $options->exacts = ['"cat"', '""', '"donkey', '"', '\\', '\\"'];
$output = $options->toString(); $output = $options->toString();
$this->assertEquals('"\"cat\"" "\"\"" "\"donkey" "\""', $output); $this->assertEquals('"\"cat\"" "\"\"" "\"donkey" "\"" "\\\\" "\\\\\""', $output);
} }
public function test_correct_filter_values_are_set_from_string() public function test_correct_filter_values_are_set_from_string()