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:
parent
fb417828a4
commit
f77bb01b51
4 changed files with 35 additions and 12 deletions
|
@ -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 . '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]'));
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue