];
$patterns = [
- 'exacts' => '/"(.*?)(?<!\\\)"/',
+ 'exacts' => '/"((?:\\\\.|[^"\\\\])*)"/',
'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/',
];
}
}
- // 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
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
$parts = $this->searches;
foreach ($this->exacts as $term) {
- $escaped = str_replace('"', '\"', $term);
+ $escaped = str_replace('\\', '\\\\', $term);
+ $escaped = str_replace('"', '\"', $escaped);
$parts[] = '"' . $escaped . '"';
}
public function test_command_requires_email_or_id_option()
{
$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);
}
$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]'));
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_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()