| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-16 18:27:22 +08:00
										 |  |  | namespace BookStack\Search; | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  | use BookStack\Search\Options\ExactSearchOption; | 
					
						
							|  |  |  | use BookStack\Search\Options\FilterSearchOption; | 
					
						
							|  |  |  | use BookStack\Search\Options\SearchOption; | 
					
						
							|  |  |  | use BookStack\Search\Options\TagSearchOption; | 
					
						
							|  |  |  | use BookStack\Search\Options\TermSearchOption; | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  | use Illuminate\Http\Request; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class SearchOptions | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2024-10-04 02:27:03 +08:00
										 |  |  |     /** @var SearchOptionSet<TermSearchOption> */ | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |     public SearchOptionSet $searches; | 
					
						
							| 
									
										
										
										
											2024-10-04 02:27:03 +08:00
										 |  |  |     /** @var SearchOptionSet<ExactSearchOption> */ | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |     public SearchOptionSet $exacts; | 
					
						
							| 
									
										
										
										
											2024-10-04 02:27:03 +08:00
										 |  |  |     /** @var SearchOptionSet<TagSearchOption> */ | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |     public SearchOptionSet $tags; | 
					
						
							| 
									
										
										
										
											2024-10-04 02:27:03 +08:00
										 |  |  |     /** @var SearchOptionSet<FilterSearchOption> */ | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |     public SearchOptionSet $filters; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function __construct() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $this->searches = new SearchOptionSet(); | 
					
						
							|  |  |  |         $this->exacts = new SearchOptionSet(); | 
					
						
							|  |  |  |         $this->tags = new SearchOptionSet(); | 
					
						
							|  |  |  |         $this->filters = new SearchOptionSet(); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Create a new instance from a search string. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2021-10-27 05:04:18 +08:00
										 |  |  |     public static function fromString(string $search): self | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |         $instance = new self(); | 
					
						
							|  |  |  |         $instance->addOptionsFromString($search); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         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. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2021-10-27 05:04:18 +08:00
										 |  |  |     public static function fromRequest(Request $request): self | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2020-06-27 20:37:18 +08:00
										 |  |  |         if (!$request->has('search') && !$request->has('term')) { | 
					
						
							|  |  |  |             return static::fromString(''); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         if ($request->has('term')) { | 
					
						
							|  |  |  |             return static::fromString($request->get('term')); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-06 00:27:59 +08:00
										 |  |  |         $instance = new SearchOptions(); | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         $inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags', 'extras']); | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         $parsedStandardTerms = static::parseStandardTermString($inputs['search'] ?? ''); | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |         $inputExacts = array_filter($inputs['exact'] ?? []); | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         $instance->searches = SearchOptionSet::fromValueArray(array_filter($parsedStandardTerms['terms']), TermSearchOption::class); | 
					
						
							|  |  |  |         $instance->exacts = SearchOptionSet::fromValueArray(array_filter($parsedStandardTerms['exacts']), ExactSearchOption::class); | 
					
						
							|  |  |  |         $instance->exacts = $instance->exacts->merge(SearchOptionSet::fromValueArray($inputExacts, ExactSearchOption::class)); | 
					
						
							|  |  |  |         $instance->tags = SearchOptionSet::fromValueArray(array_filter($inputs['tags'] ?? []), TagSearchOption::class); | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         $cleanedFilters = []; | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) { | 
					
						
							|  |  |  |             if (empty($filterVal)) { | 
					
						
							|  |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |             $cleanedFilterVal = $filterVal === 'true' ? '' : $filterVal; | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |             $cleanedFilters[] = new FilterSearchOption($cleanedFilterVal, $filterKey); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         if (isset($inputs['types']) && count($inputs['types']) < 4) { | 
					
						
							| 
									
										
										
										
											2024-10-05 21:47:00 +08:00
										 |  |  |             $cleanedFilters[] = new FilterSearchOption(implode('|', $inputs['types']), 'type'); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         $instance->filters = new SearchOptionSet($cleanedFilters); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Parse and merge in extras if provided
 | 
					
						
							|  |  |  |         if (!empty($inputs['extras'])) { | 
					
						
							|  |  |  |             $extras = static::fromString($inputs['extras']); | 
					
						
							|  |  |  |             $instance->searches = $instance->searches->merge($extras->searches); | 
					
						
							|  |  |  |             $instance->exacts = $instance->exacts->merge($extras->exacts); | 
					
						
							|  |  |  |             $instance->tags = $instance->tags->merge($extras->tags); | 
					
						
							|  |  |  |             $instance->filters = $instance->filters->merge($extras->filters); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         return $instance; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |      * Decode a search string and add its contents to this instance. | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |     protected function addOptionsFromString(string $searchString): void | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         /** @var array<string, SearchOption[]> $terms */ | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         $terms = [ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'exacts'   => [], | 
					
						
							|  |  |  |             'tags'     => [], | 
					
						
							|  |  |  |             'filters'  => [], | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $patterns = [ | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |             'exacts'  => '/-?"((?:\\\\.|[^"\\\\])*)"/', | 
					
						
							|  |  |  |             'tags'    => '/-?\[(.*?)\]/', | 
					
						
							|  |  |  |             'filters' => '/-?\{(.*?)\}/', | 
					
						
							|  |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $constructors = [ | 
					
						
							|  |  |  |             'exacts'   => fn(string $value, bool $negated) => new ExactSearchOption($value, $negated), | 
					
						
							|  |  |  |             'tags'     => fn(string $value, bool $negated) => new TagSearchOption($value, $negated), | 
					
						
							|  |  |  |             'filters'  => fn(string $value, bool $negated) => FilterSearchOption::fromContentString($value, $negated), | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Parse special terms
 | 
					
						
							|  |  |  |         foreach ($patterns as $termType => $pattern) { | 
					
						
							|  |  |  |             $matches = []; | 
					
						
							|  |  |  |             preg_match_all($pattern, $searchString, $matches); | 
					
						
							|  |  |  |             if (count($matches) > 0) { | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |                 foreach ($matches[1] as $index => $value) { | 
					
						
							|  |  |  |                     $negated = str_starts_with($matches[0][$index], '-'); | 
					
						
							|  |  |  |                     $terms[$termType][] = $constructors[$termType]($value, $negated); | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |                 $searchString = preg_replace($pattern, '', $searchString); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-23 20:41:10 +08:00
										 |  |  |         // Unescape exacts and backslash escapes
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         foreach ($terms['exacts'] as $exact) { | 
					
						
							|  |  |  |             $exact->value = static::decodeEscapes($exact->value); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         // Parse standard terms
 | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  |         $parsedStandardTerms = static::parseStandardTermString($searchString); | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |         $this->searches = $this->searches | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |             ->merge(SearchOptionSet::fromValueArray($parsedStandardTerms['terms'], TermSearchOption::class)) | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |             ->filterEmpty(); | 
					
						
							|  |  |  |         $this->exacts = $this->exacts | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |             ->merge(new SearchOptionSet($terms['exacts'])) | 
					
						
							|  |  |  |             ->merge(SearchOptionSet::fromValueArray($parsedStandardTerms['exacts'], ExactSearchOption::class)) | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |             ->filterEmpty(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         // Add tags & filters
 | 
					
						
							|  |  |  |         $this->tags = $this->tags->merge(new SearchOptionSet($terms['tags'])); | 
					
						
							|  |  |  |         $this->filters = $this->filters->merge(new SearchOptionSet($terms['filters'])); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-23 20:41:10 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * 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; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Parse a standard search term string into individual search terms and | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |      * 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. | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  |      * | 
					
						
							|  |  |  |      * @return array{terms: array<string>, exacts: array<string>} | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected static function parseStandardTermString(string $termString): array | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $terms = explode(' ', $termString); | 
					
						
							|  |  |  |         $indexDelimiters = SearchIndex::$delimiters; | 
					
						
							|  |  |  |         $parsed = [ | 
					
						
							| 
									
										
										
										
											2021-11-13 21:28:17 +08:00
										 |  |  |             'terms'  => [], | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  |             'exacts' => [], | 
					
						
							|  |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($terms as $searchTerm) { | 
					
						
							|  |  |  |             if ($searchTerm === '') { | 
					
						
							|  |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |             $becomeExact = (strpbrk($searchTerm, $indexDelimiters) !== false); | 
					
						
							|  |  |  |             $parsed[$becomeExact ? 'exacts' : 'terms'][] = $searchTerm; | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $parsed; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-11 23:55:43 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Set the value of a specific filter in the search options. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function setFilter(string $filterName, string $filterValue = ''): void | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |         $this->filters = $this->filters->merge( | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |             new SearchOptionSet([new FilterSearchOption($filterValue, $filterName)]) | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |         ); | 
					
						
							| 
									
										
										
										
											2023-12-11 23:55:43 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Encode this instance to a search string. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function toString(): string | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         $options = [ | 
					
						
							|  |  |  |             ...$this->searches->all(), | 
					
						
							|  |  |  |             ...$this->exacts->all(), | 
					
						
							|  |  |  |             ...$this->tags->all(), | 
					
						
							|  |  |  |             ...$this->filters->all(), | 
					
						
							|  |  |  |         ]; | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         $parts = array_map(fn(SearchOption $o) => $o->toString(), $options); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |         return implode(' ', $parts); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the search options that don't have UI controls provided for. | 
					
						
							|  |  |  |      * Provided back as a key => value array with the keys being expected | 
					
						
							|  |  |  |      * input names for a search form, and values being the option value. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |     public function getAdditionalOptionsString(): string | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $options = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-05 21:47:00 +08:00
										 |  |  |         // Handle filters without UI support
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         $userFilters = ['updated_by', 'created_by', 'owned_by']; | 
					
						
							| 
									
										
										
										
											2024-10-05 21:47:00 +08:00
										 |  |  |         $unsupportedFilters = ['is_template', 'sort_by']; | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         foreach ($this->filters->all() as $filter) { | 
					
						
							|  |  |  |             if (in_array($filter->getKey(), $userFilters, true) && $filter->value !== null && $filter->value !== 'me') { | 
					
						
							|  |  |  |                 $options[] = $filter; | 
					
						
							| 
									
										
										
										
											2024-10-05 21:47:00 +08:00
										 |  |  |             } else if (in_array($filter->getKey(), $unsupportedFilters, true)) { | 
					
						
							|  |  |  |                 $options[] = $filter; | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         // Negated items
 | 
					
						
							| 
									
										
										
										
											2024-10-04 02:38:07 +08:00
										 |  |  |         array_push($options, ...$this->exacts->negated()->all()); | 
					
						
							|  |  |  |         array_push($options, ...$this->tags->negated()->all()); | 
					
						
							|  |  |  |         array_push($options, ...$this->filters->negated()->all()); | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-03 22:59:50 +08:00
										 |  |  |         return implode(' ', array_map(fn(SearchOption $o) => $o->toString(), $options)); | 
					
						
							| 
									
										
										
										
											2024-10-03 00:31:45 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-03-08 06:24:05 +08:00
										 |  |  | } |