| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							|  |  |  | use Illuminate\Http\Request; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class SearchOptions | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2022-08-16 18:27:22 +08:00
										 |  |  |     public array $searches = []; | 
					
						
							|  |  |  |     public array $exacts = []; | 
					
						
							|  |  |  |     public array $tags = []; | 
					
						
							|  |  |  |     public array $filters = []; | 
					
						
							| 
									
										
										
										
											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
										 |  |  |     { | 
					
						
							|  |  |  |         $decoded = static::decode($search); | 
					
						
							| 
									
										
										
										
											2021-11-06 00:27:59 +08:00
										 |  |  |         $instance = new SearchOptions(); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         foreach ($decoded as $type => $value) { | 
					
						
							|  |  |  |             $instance->$type = $value; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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(); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         $inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']); | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         $parsedStandardTerms = static::parseStandardTermString($inputs['search'] ?? ''); | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |         $instance->searches = array_filter($parsedStandardTerms['terms']); | 
					
						
							|  |  |  |         $instance->exacts = array_filter($parsedStandardTerms['exacts']); | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         array_push($instance->exacts, ...array_filter($inputs['exact'] ?? [])); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         $instance->tags = array_filter($inputs['tags'] ?? []); | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) { | 
					
						
							|  |  |  |             if (empty($filterVal)) { | 
					
						
							|  |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             $instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-11-13 02:03:44 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         if (isset($inputs['types']) && count($inputs['types']) < 4) { | 
					
						
							|  |  |  |             $instance->filters['type'] = implode('|', $inputs['types']); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         return $instance; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Decode a search string into an array of terms. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected static function decode(string $searchString): array | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $terms = [ | 
					
						
							|  |  |  |             'searches' => [], | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'exacts'   => [], | 
					
						
							|  |  |  |             'tags'     => [], | 
					
						
							|  |  |  |             'filters'  => [], | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $patterns = [ | 
					
						
							| 
									
										
										
										
											2023-09-23 20:41:10 +08:00
										 |  |  |             'exacts'  => '/"((?:\\\\.|[^"\\\\])*)"/', | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'tags'    => '/\[(.*?)\]/', | 
					
						
							|  |  |  |             'filters' => '/\{(.*?)\}/', | 
					
						
							| 
									
										
										
										
											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) { | 
					
						
							|  |  |  |                 $terms[$termType] = $matches[1]; | 
					
						
							|  |  |  |                 $searchString = preg_replace($pattern, '', $searchString); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-23 20:41:10 +08:00
										 |  |  |         // Unescape exacts and backslash escapes
 | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |         foreach ($terms['exacts'] as $index => $exact) { | 
					
						
							| 
									
										
										
										
											2023-09-23 20:41:10 +08:00
										 |  |  |             $terms['exacts'][$index] = static::decodeEscapes($exact); | 
					
						
							| 
									
										
										
										
											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); | 
					
						
							|  |  |  |         array_push($terms['searches'], ...$parsedStandardTerms['terms']); | 
					
						
							|  |  |  |         array_push($terms['exacts'], ...$parsedStandardTerms['exacts']); | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // 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; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |         // Filter down terms where required
 | 
					
						
							|  |  |  |         $terms['exacts'] = array_filter($terms['exacts']); | 
					
						
							|  |  |  |         $terms['searches'] = array_filter($terms['searches']); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         return $terms; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $this->filters[$filterName] = $filterValue; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Encode this instance to a search string. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function toString(): string | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |         $parts = $this->searches; | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         foreach ($this->exacts as $term) { | 
					
						
							| 
									
										
										
										
											2023-09-23 20:41:10 +08:00
										 |  |  |             $escaped = str_replace('\\', '\\\\', $term); | 
					
						
							|  |  |  |             $escaped = str_replace('"', '\"', $escaped); | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |             $parts[] = '"' . $escaped . '"'; | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($this->tags as $term) { | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |             $parts[] = "[{$term}]"; | 
					
						
							| 
									
										
										
										
											2020-06-27 20:29:00 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($this->filters as $filterName => $filterVal) { | 
					
						
							| 
									
										
										
										
											2023-09-20 03:09:33 +08:00
										 |  |  |             $parts[] = '{' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}'; | 
					
						
							| 
									
										
										
										
											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
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-03-08 06:24:05 +08:00
										 |  |  | } |