]> BookStack Code Mirror - bookstack/commitdiff
Search: Added further backslash handling
authorDan Brown <redacted>
Sat, 23 Sep 2023 12:41:10 +0000 (13:41 +0100)
committerDan Brown <redacted>
Sat, 23 Sep 2023 12:41:10 +0000 (13:41 +0100)
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.

app/Search/SearchOptions.php
tests/Commands/ResetMfaCommandTest.php
tests/Entity/EntitySearchTest.php
tests/Entity/SearchOptionsTest.php

index af146d5fd6e5c19db5802e336611ec74e7cfa8ac..d38fc8d5751f3017fe39f1eced968b580e754d18 100644 (file)
@@ -78,7 +78,7 @@ class SearchOptions
         ];
 
         $patterns = [
-            'exacts'  => '/"(.*?)(?<!\\\)"/',
+            '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 . '"';
         }
 
index 85f8f6430a769e77ee9e18ef5ac009526e6ca3d0..39c8c689b0c7944ce45dd76847df7c1831d1987f 100644 (file)
@@ -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=<number> or --email=<email> option must be provided.')
+            ->expectsOutputToContain('Either a --id=<number> or --email=<email> option must be provided.')
             ->assertExitCode(1);
     }
 
index a070ce3fa889994686b5fa8e83b57f4fc3f7499f..fbb47226e6a4864a8ee78c51915e5d1367b92b58 100644 (file)
@@ -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]'));
index 8bc9d02e4c792df0da3949391830e68523d3ab3e..ea4d727a42850002353b686823ce362a048b4395 100644 (file)
@@ -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()