BookStack/app/Search/SearchOptions.php
Dan Brown 8964575973
Search: Added support for escaped exact terms
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
2023-09-19 20:09:33 +01:00

174 lines
5.2 KiB
PHP

<?php
namespace BookStack\Search;
use Illuminate\Http\Request;
class SearchOptions
{
public array $searches = [];
public array $exacts = [];
public array $tags = [];
public array $filters = [];
/**
* Create a new instance from a search string.
*/
public static function fromString(string $search): self
{
$decoded = static::decode($search);
$instance = new SearchOptions();
foreach ($decoded as $type => $value) {
$instance->$type = $value;
}
return $instance;
}
/**
* Create a new instance from a request.
* Will look for a classic string term and use that
* Otherwise we'll use the details from an advanced search form.
*/
public static function fromRequest(Request $request): self
{
if (!$request->has('search') && !$request->has('term')) {
return static::fromString('');
}
if ($request->has('term')) {
return static::fromString($request->get('term'));
}
$instance = new SearchOptions();
$inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']);
$parsedStandardTerms = static::parseStandardTermString($inputs['search'] ?? '');
$instance->searches = array_filter($parsedStandardTerms['terms']);
$instance->exacts = array_filter($parsedStandardTerms['exacts']);
array_push($instance->exacts, ...array_filter($inputs['exact'] ?? []));
$instance->tags = array_filter($inputs['tags'] ?? []);
foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) {
if (empty($filterVal)) {
continue;
}
$instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal;
}
if (isset($inputs['types']) && count($inputs['types']) < 4) {
$instance->filters['type'] = implode('|', $inputs['types']);
}
return $instance;
}
/**
* Decode a search string into an array of terms.
*/
protected static function decode(string $searchString): array
{
$terms = [
'searches' => [],
'exacts' => [],
'tags' => [],
'filters' => [],
];
$patterns = [
'exacts' => '/"(.*?)(?<!\\\)"/',
'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/',
];
// Parse special terms
foreach ($patterns as $termType => $pattern) {
$matches = [];
preg_match_all($pattern, $searchString, $matches);
if (count($matches) > 0) {
$terms[$termType] = $matches[1];
$searchString = preg_replace($pattern, '', $searchString);
}
}
// 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']);
array_push($terms['exacts'], ...$parsedStandardTerms['exacts']);
// Split filter values out
$splitFilters = [];
foreach ($terms['filters'] as $filter) {
$explodedFilter = explode(':', $filter, 2);
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
}
$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
* 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>}
*/
protected static function parseStandardTermString(string $termString): array
{
$terms = explode(' ', $termString);
$indexDelimiters = SearchIndex::$delimiters;
$parsed = [
'terms' => [],
'exacts' => [],
];
foreach ($terms as $searchTerm) {
if ($searchTerm === '') {
continue;
}
$becomeExact = (strpbrk($searchTerm, $indexDelimiters) !== false);
$parsed[$becomeExact ? 'exacts' : 'terms'][] = $searchTerm;
}
return $parsed;
}
/**
* Encode this instance to a search string.
*/
public function toString(): string
{
$parts = $this->searches;
foreach ($this->exacts as $term) {
$escaped = str_replace('"', '\"', $term);
$parts[] = '"' . $escaped . '"';
}
foreach ($this->tags as $term) {
$parts[] = "[{$term}]";
}
foreach ($this->filters as $filterName => $filterVal) {
$parts[] = '{' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
}
return implode(' ', $parts);
}
}