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
					
				| 
						 | 
					@ -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 New Issue