]> BookStack Code Mirror - bookstack/commitdiff
Search: Added support for escaped exact terms
authorDan Brown <redacted>
Tue, 19 Sep 2023 19:09:33 +0000 (20:09 +0100)
committerDan Brown <redacted>
Tue, 19 Sep 2023 19:09:33 +0000 (20:09 +0100)
Also prevented use of empty exact matches.
Prevents issues when attempting to use exact search terms in inputs for
just search terms, and use of single " chars within search terms since
these would get auto-promoted to exacts.

For #4535

app/Search/SearchOptions.php
tests/Entity/SearchOptionsTest.php

index 0bf9c3116b67d91e092ee3fa82abda3e01441fa0..af146d5fd6e5c19db5802e336611ec74e7cfa8ac 100644 (file)
@@ -44,8 +44,8 @@ class SearchOptions
         $inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']);
 
         $parsedStandardTerms = static::parseStandardTermString($inputs['search'] ?? '');
-        $instance->searches = $parsedStandardTerms['terms'];
-        $instance->exacts = $parsedStandardTerms['exacts'];
+        $instance->searches = array_filter($parsedStandardTerms['terms']);
+        $instance->exacts = array_filter($parsedStandardTerms['exacts']);
 
         array_push($instance->exacts, ...array_filter($inputs['exact'] ?? []));
 
@@ -78,7 +78,7 @@ class SearchOptions
         ];
 
         $patterns = [
-            'exacts'  => '/"(.*?)"/',
+            'exacts'  => '/"(.*?)(?<!\\\)"/',
             'tags'    => '/\[(.*?)\]/',
             'filters' => '/\{(.*?)\}/',
         ];
@@ -93,6 +93,11 @@ class SearchOptions
             }
         }
 
+        // Unescape exacts
+        foreach ($terms['exacts'] as $index => $exact) {
+            $terms['exacts'][$index] = str_replace('\"', '"', $exact);
+        }
+
         // Parse standard terms
         $parsedStandardTerms = static::parseStandardTermString($searchString);
         array_push($terms['searches'], ...$parsedStandardTerms['terms']);
@@ -106,12 +111,19 @@ class SearchOptions
         }
         $terms['filters'] = $splitFilters;
 
+        // Filter down terms where required
+        $terms['exacts'] = array_filter($terms['exacts']);
+        $terms['searches'] = array_filter($terms['searches']);
+
         return $terms;
     }
 
     /**
      * Parse a standard search term string into individual search terms and
-     * extract any exact terms searches to be made.
+     * convert any required terms to exact matches. This is done since some
+     * characters will never be in the standard index, since we use them as
+     * delimiters, and therefore we convert a term to be exact if it
+     * contains one of those delimiter characters.
      *
      * @return array{terms: array<string>, exacts: array<string>}
      */
@@ -129,8 +141,8 @@ class SearchOptions
                 continue;
             }
 
-            $parsedList = (strpbrk($searchTerm, $indexDelimiters) === false) ? 'terms' : 'exacts';
-            $parsed[$parsedList][] = $searchTerm;
+            $becomeExact = (strpbrk($searchTerm, $indexDelimiters) !== false);
+            $parsed[$becomeExact ? 'exacts' : 'terms'][] = $searchTerm;
         }
 
         return $parsed;
@@ -141,20 +153,21 @@ class SearchOptions
      */
     public function toString(): string
     {
-        $string = implode(' ', $this->searches ?? []);
+        $parts = $this->searches;
 
         foreach ($this->exacts as $term) {
-            $string .= ' "' . $term . '"';
+            $escaped = str_replace('"', '\"', $term);
+            $parts[] = '"' . $escaped . '"';
         }
 
         foreach ($this->tags as $term) {
-            $string .= " [{$term}]";
+            $parts[] = "[{$term}]";
         }
 
         foreach ($this->filters as $filterName => $filterVal) {
-            $string .= ' {' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
+            $parts[] = '{' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
         }
 
-        return $string;
+        return implode(' ', $parts);
     }
 }
index cac9c67f145f5780c6be173849162693bf295120..8bc9d02e4c792df0da3949391830e68523d3ab3e 100644 (file)
@@ -3,6 +3,7 @@
 namespace Tests\Entity;
 
 use BookStack\Search\SearchOptions;
+use Illuminate\Http\Request;
 use Tests\TestCase;
 
 class SearchOptionsTest extends TestCase
@@ -17,6 +18,13 @@ class SearchOptionsTest extends TestCase
         $this->assertEquals(['is_tree' => ''], $options->filters);
     }
 
+    public function test_from_string_properly_parses_escaped_quotes()
+    {
+        $options = SearchOptions::fromString('"\"cat\"" surprise "\"\"" "\"donkey" "\""');
+
+        $this->assertEquals(['"cat"', '""', '"donkey', '"'], $options->exacts);
+    }
+
     public function test_to_string_includes_all_items_in_the_correct_format()
     {
         $expected = 'cat "dog" [tag=good] {is_tree}';
@@ -32,6 +40,15 @@ class SearchOptionsTest extends TestCase
         }
     }
 
+    public function test_to_string_escapes_quotes_as_expected()
+    {
+        $options = new SearchOptions();
+        $options->exacts = ['"cat"', '""', '"donkey', '"'];
+
+        $output = $options->toString();
+        $this->assertEquals('"\"cat\"" "\"\"" "\"donkey" "\""', $output);
+    }
+
     public function test_correct_filter_values_are_set_from_string()
     {
         $opts = SearchOptions::fromString('{is_tree} {name:dan} {cat:happy}');
@@ -42,4 +59,22 @@ class SearchOptionsTest extends TestCase
             'cat'     => 'happy',
         ], $opts->filters);
     }
+    public function test_it_cannot_parse_out_empty_exacts()
+    {
+        $options = SearchOptions::fromString('"" test ""');
+
+        $this->assertEmpty($options->exacts);
+        $this->assertCount(1, $options->searches);
+    }
+
+    public function test_from_request_properly_parses_exacts_from_search_terms()
+    {
+        $request = new Request([
+            'search' => 'biscuits "cheese" "" "baked beans"'
+        ]);
+
+        $options = SearchOptions::fromRequest($request);
+        $this->assertEquals(["biscuits"], $options->searches);
+        $this->assertEquals(['"cheese"', '""', '"baked',  'beans"'], $options->exacts);
+    }
 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.