]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/SearchOptions.php
Improved shelf book management interface
[bookstack] / app / Entities / Tools / SearchOptions.php
1 <?php
2
3 namespace BookStack\Entities\Tools;
4
5 use Illuminate\Http\Request;
6
7 class SearchOptions
8 {
9     /**
10      * @var array
11      */
12     public $searches = [];
13
14     /**
15      * @var array
16      */
17     public $exacts = [];
18
19     /**
20      * @var array
21      */
22     public $tags = [];
23
24     /**
25      * @var array
26      */
27     public $filters = [];
28
29     /**
30      * Create a new instance from a search string.
31      */
32     public static function fromString(string $search): self
33     {
34         $decoded = static::decode($search);
35         $instance = new SearchOptions();
36         foreach ($decoded as $type => $value) {
37             $instance->$type = $value;
38         }
39
40         return $instance;
41     }
42
43     /**
44      * Create a new instance from a request.
45      * Will look for a classic string term and use that
46      * Otherwise we'll use the details from an advanced search form.
47      */
48     public static function fromRequest(Request $request): self
49     {
50         if (!$request->has('search') && !$request->has('term')) {
51             return static::fromString('');
52         }
53
54         if ($request->has('term')) {
55             return static::fromString($request->get('term'));
56         }
57
58         $instance = new SearchOptions();
59         $inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']);
60
61         $parsedStandardTerms = static::parseStandardTermString($inputs['search'] ?? '');
62         $instance->searches = $parsedStandardTerms['terms'];
63         $instance->exacts = $parsedStandardTerms['exacts'];
64
65         array_push($instance->exacts, ...array_filter($inputs['exact'] ?? []));
66
67         $instance->tags = array_filter($inputs['tags'] ?? []);
68
69         foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) {
70             if (empty($filterVal)) {
71                 continue;
72             }
73             $instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal;
74         }
75
76         if (isset($inputs['types']) && count($inputs['types']) < 4) {
77             $instance->filters['type'] = implode('|', $inputs['types']);
78         }
79
80         return $instance;
81     }
82
83     /**
84      * Decode a search string into an array of terms.
85      */
86     protected static function decode(string $searchString): array
87     {
88         $terms = [
89             'searches' => [],
90             'exacts'   => [],
91             'tags'     => [],
92             'filters'  => [],
93         ];
94
95         $patterns = [
96             'exacts'  => '/"(.*?)"/',
97             'tags'    => '/\[(.*?)\]/',
98             'filters' => '/\{(.*?)\}/',
99         ];
100
101         // Parse special terms
102         foreach ($patterns as $termType => $pattern) {
103             $matches = [];
104             preg_match_all($pattern, $searchString, $matches);
105             if (count($matches) > 0) {
106                 $terms[$termType] = $matches[1];
107                 $searchString = preg_replace($pattern, '', $searchString);
108             }
109         }
110
111         // Parse standard terms
112         $parsedStandardTerms = static::parseStandardTermString($searchString);
113         array_push($terms['searches'], ...$parsedStandardTerms['terms']);
114         array_push($terms['exacts'], ...$parsedStandardTerms['exacts']);
115
116         // Split filter values out
117         $splitFilters = [];
118         foreach ($terms['filters'] as $filter) {
119             $explodedFilter = explode(':', $filter, 2);
120             $splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
121         }
122         $terms['filters'] = $splitFilters;
123
124         return $terms;
125     }
126
127     /**
128      * Parse a standard search term string into individual search terms and
129      * extract any exact terms searches to be made.
130      *
131      * @return array{terms: array<string>, exacts: array<string>}
132      */
133     protected static function parseStandardTermString(string $termString): array
134     {
135         $terms = explode(' ', $termString);
136         $indexDelimiters = SearchIndex::$delimiters;
137         $parsed = [
138             'terms'  => [],
139             'exacts' => [],
140         ];
141
142         foreach ($terms as $searchTerm) {
143             if ($searchTerm === '') {
144                 continue;
145             }
146
147             $parsedList = (strpbrk($searchTerm, $indexDelimiters) === false) ? 'terms' : 'exacts';
148             $parsed[$parsedList][] = $searchTerm;
149         }
150
151         return $parsed;
152     }
153
154     /**
155      * Encode this instance to a search string.
156      */
157     public function toString(): string
158     {
159         $string = implode(' ', $this->searches ?? []);
160
161         foreach ($this->exacts as $term) {
162             $string .= ' "' . $term . '"';
163         }
164
165         foreach ($this->tags as $term) {
166             $string .= " [{$term}]";
167         }
168
169         foreach ($this->filters as $filterName => $filterVal) {
170             $string .= ' {' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
171         }
172
173         return $string;
174     }
175 }