Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
70.43% |
331 / 470 |
|
60.64% |
57 / 94 |
CRAP | |
0.00% |
0 / 1 |
Params | |
70.43% |
331 / 470 |
|
60.64% |
57 / 94 |
1756.89 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
getOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getQueryAdapter | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setQueryAdapter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__clone | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getSearchClassId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
initFromRequest | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
initShards | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
5.93 | |||
initLimit | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
initPage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
initSearch | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
initBasicSearch | |
63.64% |
7 / 11 |
|
0.00% |
0 / 1 |
6.20 | |||
setBasicSearch | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
convertToAdvancedSearch | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
initAdvancedSearch | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
5.03 | |||
initSort | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setLastView | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initView | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
6.97 | |||
getDefaultSort | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getLimit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLimit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSort | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setSort | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
7.02 | |||
getSearchHandler | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getSearchType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getView | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setView | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDisplayQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
parseFilter | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
5.02 | |||
parseFilterAndPrefix | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getAliasesForFacetField | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
5.20 | |||
hasFilter | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
addFilter | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isAdvancedFilter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
removeFilter | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
removeAllFilters | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
addFacet | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getFacetOperator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
addCheckboxFacet | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getFacetLabel | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
7 | |||
getFacetConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
resetFacetConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRawFilters | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFilterList | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
7 | |||
getFiltersAsQueryParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFacetValueRawDisplayText | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
2.26 | |||
translateFacetValue | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
formatFilterListEntry | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
parseOperatorAndFieldName | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getCheckboxFacetValues | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getCheckboxFacets | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
7.39 | |||
getRawCheckboxFacets | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatFilterArrayAsQueryParams | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
initRangeFilters | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
formatYearForDateRange | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
formatDateForFullDateRange | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
formatValueForNumericRange | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
20 | |||
buildGenericRangeFilter | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
initGenericRangeFilters | |
14.29% |
2 / 14 |
|
0.00% |
0 / 1 |
60.01 | |||
buildNumericRangeFilter | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
buildDateRangeFilter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildFullDateRangeFilter | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
initDateFilters | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
initFullDateFilters | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
initNumericRangeFilters | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
initFilters | |
64.29% |
9 / 14 |
|
0.00% |
0 / 1 |
9.23 | |||
initHiddenFilters | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
getHiddenFilters | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHiddenFiltersAsQueryParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasHiddenFilter | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
addHiddenFilter | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
addHiddenFilterForField | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDisplayQueryWithReplacedTerm | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getViewList | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getLimitList | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getSortList | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
minify | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
deminify | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
getSavedSearchContextParameters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setQueryIDs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQueryIDLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSelectedShards | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
translate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setOverrideQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOverrideQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQuery | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
setQuery | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
initFacetList | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
hasDefaultsApplied | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
initCheckboxFacets | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
4.32 | |||
supportsFacetFiltering | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * Abstract parameters search model. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Search_Base |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
27 | * @author Juha Luoma <juha.luoma@helsinki.fi> |
28 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
29 | * @link https://vufind.org Main Page |
30 | */ |
31 | |
32 | namespace VuFind\Search\Base; |
33 | |
34 | use VuFind\I18n\TranslatableString; |
35 | use VuFind\Search\Minified; |
36 | use VuFind\Search\QueryAdapter; |
37 | use VuFind\Search\QueryAdapterInterface; |
38 | use VuFind\Solr\Utils as SolrUtils; |
39 | use VuFindSearch\Backend\Solr\LuceneSyntaxHelper; |
40 | use VuFindSearch\Query\AbstractQuery; |
41 | use VuFindSearch\Query\Query; |
42 | use VuFindSearch\Query\QueryGroup; |
43 | |
44 | use function call_user_func; |
45 | use function count; |
46 | use function get_class; |
47 | use function in_array; |
48 | use function intval; |
49 | use function is_array; |
50 | use function is_callable; |
51 | use function is_object; |
52 | use function strlen; |
53 | |
54 | /** |
55 | * Abstract parameters search model. |
56 | * |
57 | * This abstract class defines the parameters methods for modeling a search in VuFind |
58 | * |
59 | * @category VuFind |
60 | * @package Search_Base |
61 | * @author Demian Katz <demian.katz@villanova.edu> |
62 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
63 | * @author Juha Luoma <juha.luoma@helsinki.fi> |
64 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
65 | * @link https://vufind.org Main Page |
66 | */ |
67 | class Params |
68 | { |
69 | /** |
70 | * Internal representation of user query. |
71 | * |
72 | * @var Query |
73 | */ |
74 | protected $query; |
75 | |
76 | /** |
77 | * Page number |
78 | * |
79 | * @var int |
80 | */ |
81 | protected $page = 1; |
82 | |
83 | /** |
84 | * Sort setting |
85 | * |
86 | * @var string |
87 | */ |
88 | protected $sort = null; |
89 | |
90 | /** |
91 | * Override special RSS sort feature? |
92 | * |
93 | * @var bool |
94 | */ |
95 | protected $skipRssSort = false; |
96 | |
97 | /** |
98 | * Result limit |
99 | * |
100 | * @var int |
101 | */ |
102 | protected $limit = 20; |
103 | |
104 | /** |
105 | * Search type (basic or advanced) |
106 | * |
107 | * @var string |
108 | */ |
109 | protected $searchType = 'basic'; |
110 | |
111 | /** |
112 | * Shards |
113 | * |
114 | * @var array |
115 | */ |
116 | protected $selectedShards = []; |
117 | |
118 | /** |
119 | * View |
120 | * |
121 | * @var string |
122 | */ |
123 | protected $view = null; |
124 | |
125 | /** |
126 | * Previously-used view (loaded in from session) |
127 | * |
128 | * @var string |
129 | */ |
130 | protected $lastView = null; |
131 | |
132 | /** |
133 | * Search options |
134 | * |
135 | * @var Options |
136 | */ |
137 | protected $options; |
138 | |
139 | /** |
140 | * Main facet configuration |
141 | * |
142 | * @var array |
143 | */ |
144 | protected $facetConfig = []; |
145 | |
146 | /** |
147 | * Extra facet labels |
148 | * |
149 | * @var array |
150 | */ |
151 | protected $extraFacetLabels = []; |
152 | |
153 | /** |
154 | * Config sections to search for facet labels if no override configuration |
155 | * is set. |
156 | * |
157 | * @var array |
158 | */ |
159 | protected $defaultFacetLabelSections = ['ExtraFacetLabels']; |
160 | |
161 | /** |
162 | * Config sections to search for checkbox facet labels if no override |
163 | * configuration is set. |
164 | * |
165 | * @var array |
166 | */ |
167 | protected $defaultFacetLabelCheckboxSections = []; |
168 | |
169 | /** |
170 | * Checkbox facet configuration |
171 | * |
172 | * @var array |
173 | */ |
174 | protected $checkboxFacets = []; |
175 | |
176 | /** |
177 | * Applied filters |
178 | * |
179 | * @var array |
180 | */ |
181 | protected $filterList = []; |
182 | |
183 | /** |
184 | * Pre-assigned filters |
185 | * |
186 | * @var array |
187 | */ |
188 | protected $hiddenFilters = []; |
189 | |
190 | /** |
191 | * Facets in "OR" mode |
192 | * |
193 | * @var array |
194 | */ |
195 | protected $orFacets = []; |
196 | |
197 | /** |
198 | * Override Query |
199 | */ |
200 | protected $overrideQuery = false; |
201 | |
202 | /** |
203 | * Are default filters applied? |
204 | * |
205 | * @var bool |
206 | */ |
207 | protected $defaultsApplied = false; |
208 | |
209 | /** |
210 | * Map of facet field aliases. |
211 | * |
212 | * @var array |
213 | */ |
214 | protected $facetAliases = []; |
215 | |
216 | /** |
217 | * Search context parameters. |
218 | * |
219 | * @var array |
220 | */ |
221 | protected $searchContextParameters = []; |
222 | |
223 | /** |
224 | * Config loader |
225 | * |
226 | * @var \VuFind\Config\PluginManager |
227 | */ |
228 | protected $configLoader; |
229 | |
230 | /** |
231 | * Query adapter |
232 | * |
233 | * @var ?QueryAdapterInterface |
234 | */ |
235 | protected $queryAdapter = null; |
236 | |
237 | /** |
238 | * Default query adapter class |
239 | * |
240 | * @var string |
241 | */ |
242 | protected $queryAdapterClass = QueryAdapter::class; |
243 | |
244 | /** |
245 | * Constructor |
246 | * |
247 | * @param \VuFind\Search\Base\Options $options Options to use |
248 | * @param \VuFind\Config\PluginManager $configLoader Config loader |
249 | */ |
250 | public function __construct($options, \VuFind\Config\PluginManager $configLoader) |
251 | { |
252 | $this->setOptions($options); |
253 | |
254 | $this->configLoader = $configLoader; |
255 | |
256 | // Make sure we have some sort of query object: |
257 | $this->query = new Query(); |
258 | |
259 | // Set up facet label settings, to be used as fallbacks if specific facets |
260 | // are not already configured: |
261 | $config = $configLoader->get($options->getFacetsIni()); |
262 | $sections = $config->FacetLabels->labelSections |
263 | ?? $this->defaultFacetLabelSections; |
264 | foreach ($sections as $section) { |
265 | foreach ($config->$section ?? [] as $field => $label) { |
266 | $this->extraFacetLabels[$field] = $label; |
267 | } |
268 | } |
269 | |
270 | // Activate all relevant checkboxes, also important for labeling: |
271 | $checkboxSections = $config->FacetLabels->checkboxSections |
272 | ?? $this->defaultFacetLabelCheckboxSections; |
273 | foreach ($checkboxSections as $checkboxSection) { |
274 | $this->initCheckboxFacets($checkboxSection); |
275 | } |
276 | } |
277 | |
278 | /** |
279 | * Get the search options object. |
280 | * |
281 | * @return \VuFind\Search\Base\Options |
282 | */ |
283 | public function getOptions() |
284 | { |
285 | return $this->options; |
286 | } |
287 | |
288 | /** |
289 | * Set the search options object. |
290 | * |
291 | * @param \VuFind\Search\Base\Options $options Options to use |
292 | * |
293 | * @return void |
294 | */ |
295 | public function setOptions(Options $options) |
296 | { |
297 | $this->options = $options; |
298 | } |
299 | |
300 | /** |
301 | * Get query adapter |
302 | * |
303 | * @return QueryAdapterInterface |
304 | */ |
305 | public function getQueryAdapter(): QueryAdapterInterface |
306 | { |
307 | if (null === $this->queryAdapter) { |
308 | $this->queryAdapter = new ($this->queryAdapterClass)(); |
309 | } |
310 | return $this->queryAdapter; |
311 | } |
312 | |
313 | /** |
314 | * Set query adapter |
315 | * |
316 | * @param QueryAdapterInterface $queryAdapter Query adapter |
317 | * |
318 | * @return void |
319 | */ |
320 | public function setQueryAdapter(QueryAdapterInterface $queryAdapter) |
321 | { |
322 | $this->queryAdapter = $queryAdapter; |
323 | } |
324 | |
325 | /** |
326 | * Copy constructor |
327 | * |
328 | * @return void |
329 | */ |
330 | public function __clone() |
331 | { |
332 | if (is_object($this->options)) { |
333 | $this->options = clone $this->options; |
334 | } |
335 | if (is_object($this->query)) { |
336 | $this->query = clone $this->query; |
337 | } |
338 | } |
339 | |
340 | /** |
341 | * Get the identifier used for naming the various search classes in this family. |
342 | * |
343 | * @return string |
344 | */ |
345 | public function getSearchClassId() |
346 | { |
347 | return $this->getOptions()->getSearchClassId(); |
348 | } |
349 | |
350 | /** |
351 | * Pull the search parameters |
352 | * |
353 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
354 | * request. |
355 | * |
356 | * @return void |
357 | */ |
358 | public function initFromRequest($request) |
359 | { |
360 | // We should init view first, since RSS view may cause certain variant |
361 | // behaviors: |
362 | $this->initView($request); |
363 | $this->initLimit($request); |
364 | $this->initPage($request); |
365 | $this->initShards($request); |
366 | // We have to initialize sort after search, since the search options may |
367 | // affect the default sort option. |
368 | $this->initSearch($request); |
369 | $this->initSort($request); |
370 | $this->initFilters($request); |
371 | $this->initHiddenFilters($request); |
372 | } |
373 | |
374 | /** |
375 | * Pull shard parameters from the request or set defaults |
376 | * |
377 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
378 | * request. |
379 | * |
380 | * @return void |
381 | */ |
382 | protected function initShards($request) |
383 | { |
384 | $legalShards = array_keys($this->getOptions()->getShards()); |
385 | $requestShards = $request->get('shard', []); |
386 | if (!is_array($requestShards)) { |
387 | $requestShards = [$requestShards]; |
388 | } |
389 | |
390 | // If a shard selection list is found as an incoming parameter, |
391 | // we should save valid values for future reference: |
392 | foreach ($requestShards as $current) { |
393 | if (in_array($current, $legalShards)) { |
394 | $this->selectedShards[] = $current; |
395 | } |
396 | } |
397 | |
398 | // If we got this far and still have no selections established, revert to |
399 | // defaults: |
400 | if (empty($this->selectedShards)) { |
401 | $this->selectedShards = $this->getOptions()->getDefaultSelectedShards(); |
402 | } |
403 | } |
404 | |
405 | /** |
406 | * Pull the page size parameter or set to default |
407 | * |
408 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
409 | * request. |
410 | * |
411 | * @return void |
412 | */ |
413 | protected function initLimit($request) |
414 | { |
415 | // Check for a limit parameter in the url. |
416 | $defaultLimit = $this->getOptions()->getDefaultLimit(); |
417 | if (($limit = intval($request->get('limit'))) != $defaultLimit) { |
418 | // make sure the url parameter is a valid limit -- either |
419 | // one of the explicitly allowed values, or at least smaller |
420 | // than the largest allowed. (This leniency is useful in |
421 | // combination with combined search, where it is often useful |
422 | // to reduce the size of result lists without actually enabling |
423 | // the user's ability to select a reduced list size). |
424 | $legalOptions = $this->getOptions()->getLimitOptions(); |
425 | if ( |
426 | in_array($limit, $legalOptions) |
427 | || ($limit > 0 && $limit < max($legalOptions)) |
428 | ) { |
429 | $this->limit = $limit; |
430 | return; |
431 | } |
432 | } |
433 | |
434 | // Increase default limit for RSS mode: |
435 | if ($this->getView() == 'rss' && $defaultLimit < 50) { |
436 | $defaultLimit = 50; |
437 | } |
438 | |
439 | // If we got this far, setting was missing or invalid; load the default |
440 | $this->limit = $defaultLimit; |
441 | } |
442 | |
443 | /** |
444 | * Pull the page parameter |
445 | * |
446 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
447 | * request. |
448 | * |
449 | * @return void |
450 | */ |
451 | protected function initPage($request) |
452 | { |
453 | $this->page = intval($request->get('page')); |
454 | if ($this->page < 1) { |
455 | $this->page = 1; |
456 | } |
457 | } |
458 | |
459 | /** |
460 | * Initialize the object's search settings from a request object. |
461 | * |
462 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
463 | * request. |
464 | * |
465 | * @return void |
466 | */ |
467 | protected function initSearch($request) |
468 | { |
469 | // Try to initialize a basic search; if that fails, try for an advanced |
470 | // search next! |
471 | if (!$this->initBasicSearch($request)) { |
472 | $this->initAdvancedSearch($request); |
473 | } |
474 | } |
475 | |
476 | /** |
477 | * Support method for initSearch() -- handle basic settings. |
478 | * |
479 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
480 | * request. |
481 | * |
482 | * @return bool True if search settings were found, false if not. |
483 | */ |
484 | protected function initBasicSearch($request) |
485 | { |
486 | // If no lookfor parameter was found, we have no search terms to |
487 | // add to our array! |
488 | if (null === ($lookfor = $request->get('lookfor'))) { |
489 | return false; |
490 | } |
491 | |
492 | // If lookfor is an array, we may be dealing with a legacy Advanced |
493 | // Search URL. If there's only one parameter, we can flatten it, |
494 | // but otherwise we should treat it as an error -- no point in going |
495 | // to great lengths for compatibility. |
496 | if (is_array($lookfor)) { |
497 | if (count($lookfor) > 1) { |
498 | throw new \Exception('Unsupported search URL.'); |
499 | } |
500 | $lookfor = $lookfor[0]; |
501 | } |
502 | |
503 | // Flatten type arrays for backward compatibility: |
504 | $handler = $request->get('type'); |
505 | if (is_array($handler)) { |
506 | $handler = $handler[0]; |
507 | } |
508 | |
509 | // Set the search: |
510 | $this->setBasicSearch($lookfor, $handler); |
511 | return true; |
512 | } |
513 | |
514 | /** |
515 | * Set a basic search query: |
516 | * |
517 | * @param string $lookfor The search query |
518 | * @param string $handler The search handler (null for default) |
519 | * |
520 | * @return void |
521 | */ |
522 | public function setBasicSearch($lookfor, $handler = null) |
523 | { |
524 | $this->searchType = 'basic'; |
525 | |
526 | if (empty($handler)) { |
527 | $handler = $this->getOptions()->getDefaultHandler(); |
528 | } |
529 | |
530 | $this->query = new Query($lookfor, $handler); |
531 | } |
532 | |
533 | /** |
534 | * Convert a basic query into an advanced query: |
535 | * |
536 | * @return void |
537 | */ |
538 | public function convertToAdvancedSearch() |
539 | { |
540 | if ($this->searchType === 'basic') { |
541 | $this->query = new QueryGroup( |
542 | 'AND', |
543 | [new QueryGroup('AND', [$this->query])] |
544 | ); |
545 | $this->searchType = 'advanced'; |
546 | } |
547 | if ($this->searchType !== 'advanced') { |
548 | throw new \Exception( |
549 | 'Unsupported search type: ' . $this->searchType |
550 | ); |
551 | } |
552 | } |
553 | |
554 | /** |
555 | * Support method for initSearch() -- handle advanced settings. Advanced |
556 | * searches have numeric subscripts on the lookfor and type parameters -- |
557 | * this is how they are distinguished from basic searches. |
558 | * |
559 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
560 | * request. |
561 | * |
562 | * @return void |
563 | */ |
564 | protected function initAdvancedSearch($request) |
565 | { |
566 | $this->query = $this->getQueryAdapter()->fromRequest( |
567 | $request, |
568 | $this->getOptions()->getDefaultHandler() |
569 | ); |
570 | |
571 | $this->searchType = $this->query instanceof Query ? 'basic' : 'advanced'; |
572 | |
573 | // If we ended up with a basic search, it's probably the result of |
574 | // submitting an empty form, and more processing may be needed: |
575 | if ($this->searchType == 'basic') { |
576 | // Set a default handler if necessary: |
577 | if ($this->query->getHandler() === null) { |
578 | $this->query->setHandler($this->getOptions()->getDefaultHandler()); |
579 | } |
580 | // If the user submitted the advanced search form, we want to treat |
581 | // the search as advanced even if it evaluated to a basic search. |
582 | if ($request->offsetExists('lookfor0')) { |
583 | $this->convertToAdvancedSearch(); |
584 | } |
585 | } |
586 | } |
587 | |
588 | /** |
589 | * Get the value for which type of sorting to use |
590 | * |
591 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
592 | * request. |
593 | * |
594 | * @return void |
595 | */ |
596 | protected function initSort($request) |
597 | { |
598 | // Check for special parameter only relevant in RSS mode: |
599 | if ($request->get('skip_rss_sort', 'unset') != 'unset') { |
600 | $this->skipRssSort = true; |
601 | } |
602 | $this->setSort($request->get('sort')); |
603 | } |
604 | |
605 | /** |
606 | * Set the last value of the view parameter (if available in session). |
607 | * |
608 | * @param string $view Last valid view parameter value |
609 | * |
610 | * @return void |
611 | */ |
612 | public function setLastView($view) |
613 | { |
614 | $this->lastView = $view; |
615 | } |
616 | |
617 | /** |
618 | * Get the value for which results view to use |
619 | * |
620 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
621 | * request. |
622 | * |
623 | * @return void |
624 | */ |
625 | protected function initView($request) |
626 | { |
627 | // Check for a view parameter in the url. |
628 | $view = $request->get('view'); |
629 | $validViews = array_keys($this->getOptions()->getViewOptions()); |
630 | if ($view == 'rss') { |
631 | // RSS is a special case that does not require config validation |
632 | $this->setView('rss'); |
633 | } elseif (!empty($view) && in_array($view, $validViews)) { |
634 | // make sure the url parameter is a valid view |
635 | $this->setView($view); |
636 | } elseif ( |
637 | !empty($this->lastView) |
638 | && in_array($this->lastView, $validViews) |
639 | ) { |
640 | // if there is nothing in the URL, see if we had a previous value |
641 | // injected based on session information. |
642 | $this->setView($this->lastView); |
643 | } else { |
644 | // otherwise load the default |
645 | $this->setView($this->getOptions()->getDefaultView()); |
646 | } |
647 | } |
648 | |
649 | /** |
650 | * Return the default sorting value |
651 | * |
652 | * @return string |
653 | */ |
654 | public function getDefaultSort() |
655 | { |
656 | return $this->getOptions() |
657 | ->getDefaultSortByHandler($this->getSearchHandler()); |
658 | } |
659 | |
660 | /** |
661 | * Return the current limit value |
662 | * |
663 | * @return int |
664 | */ |
665 | public function getLimit() |
666 | { |
667 | return $this->limit; |
668 | } |
669 | |
670 | /** |
671 | * Change the value of the limit |
672 | * |
673 | * @param int $l New limit value. |
674 | * |
675 | * @return void |
676 | */ |
677 | public function setLimit($l) |
678 | { |
679 | $this->limit = $l; |
680 | } |
681 | |
682 | /** |
683 | * Change the page |
684 | * |
685 | * @param int $p New page value. |
686 | * |
687 | * @return void |
688 | */ |
689 | public function setPage($p) |
690 | { |
691 | $this->page = $p; |
692 | } |
693 | |
694 | /** |
695 | * Get the page value |
696 | * |
697 | * @return int |
698 | */ |
699 | public function getPage() |
700 | { |
701 | return $this->page; |
702 | } |
703 | |
704 | /** |
705 | * Return the sorting value |
706 | * |
707 | * @return string |
708 | */ |
709 | public function getSort() |
710 | { |
711 | return $this->sort; |
712 | } |
713 | |
714 | /** |
715 | * Set the sorting value (note: sort will be set to default if an illegal |
716 | * or empty value is passed in). |
717 | * |
718 | * @param string $sort New sort value (null for default) |
719 | * @param bool $force Set sort value without validating it? |
720 | * |
721 | * @return void |
722 | */ |
723 | public function setSort($sort, $force = false) |
724 | { |
725 | // Skip validation if requested: |
726 | if ($force) { |
727 | $this->sort = $sort; |
728 | return; |
729 | } |
730 | |
731 | // Validate and assign the sort value: |
732 | $valid = array_keys($this->getOptions()->getSortOptions()); |
733 | |
734 | $matchedHiddenPatterns = array_filter( |
735 | $this->getOptions()->getHiddenSortOptions(), |
736 | function ($pattern) use ($sort) { |
737 | return preg_match('/' . $pattern . '/', $sort); |
738 | } |
739 | ); |
740 | |
741 | if (!empty($sort) && (in_array($sort, $valid) || count($matchedHiddenPatterns) > 0)) { |
742 | $this->sort = $sort; |
743 | } else { |
744 | $this->sort = $this->getDefaultSort(); |
745 | } |
746 | |
747 | // In RSS mode, we may want to adjust sort settings: |
748 | if (!$this->skipRssSort && $this->getView() == 'rss') { |
749 | $this->sort = $this->getOptions()->getRssSort($this->sort); |
750 | } |
751 | } |
752 | |
753 | /** |
754 | * Return the selected search handler (null for complex searches which have no |
755 | * single handler) |
756 | * |
757 | * @return string|null |
758 | */ |
759 | public function getSearchHandler() |
760 | { |
761 | // We can only definitively name a handler if we have a basic search: |
762 | $q = $this->getQuery(); |
763 | return $q instanceof Query ? $q->getHandler() : null; |
764 | } |
765 | |
766 | /** |
767 | * Return the search type (i.e. basic or advanced) |
768 | * |
769 | * @return string |
770 | */ |
771 | public function getSearchType() |
772 | { |
773 | return $this->searchType; |
774 | } |
775 | |
776 | /** |
777 | * Return the value for which search view we use |
778 | * |
779 | * @return string |
780 | */ |
781 | public function getView() |
782 | { |
783 | return $this->view ?? $this->getOptions()->getDefaultView(); |
784 | } |
785 | |
786 | /** |
787 | * Set the value for which search view we use |
788 | * |
789 | * @param String $v New view setting |
790 | * |
791 | * @return void |
792 | */ |
793 | public function setView($v) |
794 | { |
795 | $this->view = $v; |
796 | } |
797 | |
798 | /** |
799 | * Build a string for onscreen display showing the |
800 | * query used in the search (not the filters). |
801 | * |
802 | * @return string user friendly version of 'query' |
803 | */ |
804 | public function getDisplayQuery() |
805 | { |
806 | // Set up callbacks: |
807 | $translate = [$this, 'translate']; |
808 | $showField = [$this->getOptions(), 'getHumanReadableFieldName']; |
809 | |
810 | // Build display query: |
811 | return $this->getQueryAdapter()->display($this->getQuery(), $translate, $showField); |
812 | } |
813 | |
814 | /** |
815 | * Parse apart the field and value from a URL filter string. |
816 | * |
817 | * @param string $filter A filter string from url : "field:value" |
818 | * |
819 | * @return array Array with elements 0 = field, 1 = value. |
820 | */ |
821 | public function parseFilter($filter) |
822 | { |
823 | // Special case: complex filters cannot be split into field/value |
824 | // since they have multiple parts (e.g. field1:a OR field2:b). Use |
825 | // a fake "#" field to collect these types of filters. |
826 | if ($this->isAdvancedFilter($filter) == true) { |
827 | return ['#', $filter]; |
828 | } |
829 | |
830 | // Split the string and assign the parts to $field and $value |
831 | $temp = explode(':', $filter, 2); |
832 | $field = array_shift($temp); |
833 | $value = count($temp) > 0 ? $temp[0] : ''; |
834 | |
835 | // Remove quotes from the value if there are any |
836 | if (str_starts_with($value, '"')) { |
837 | $value = substr($value, 1); |
838 | } |
839 | if (str_ends_with($value, '"')) { |
840 | $value = substr($value, 0, -1); |
841 | } |
842 | // One last little clean on whitespace |
843 | $value = trim($value); |
844 | |
845 | // Send back the results: |
846 | return [$field, $value]; |
847 | } |
848 | |
849 | /** |
850 | * Parse apart any prefix, field and value from a URL filter string. |
851 | * |
852 | * @param string $filter A filter string from url : "field:value" |
853 | * |
854 | * @return array Array with elements 0 = prefix, 1 = field, 2 = value. |
855 | */ |
856 | public function parseFilterAndPrefix($filter) |
857 | { |
858 | [$field, $value] = $this->parseFilter($filter); |
859 | $prefix = substr($field, 0, 1); |
860 | if (in_array($prefix, ['-', '~'])) { |
861 | $field = substr($field, 1); |
862 | } else { |
863 | $prefix = ''; |
864 | } |
865 | return [$prefix, $field, $value]; |
866 | } |
867 | |
868 | /** |
869 | * Given a facet field, return an array containing all aliases of that |
870 | * field. |
871 | * |
872 | * @param string $field Field to look up |
873 | * |
874 | * @return array |
875 | */ |
876 | public function getAliasesForFacetField($field) |
877 | { |
878 | // Account for field prefixes used for Boolean logic: |
879 | $prefix = substr($field, 0, 1); |
880 | if ($prefix === '-' || $prefix === '~') { |
881 | $rawField = substr($field, 1); |
882 | } else { |
883 | $prefix = ''; |
884 | $rawField = $field; |
885 | } |
886 | $fieldsToCheck = [$field]; |
887 | foreach ($this->facetAliases as $k => $v) { |
888 | if ($v === $rawField) { |
889 | $fieldsToCheck[] = $prefix . $k; |
890 | } |
891 | } |
892 | return $fieldsToCheck; |
893 | } |
894 | |
895 | /** |
896 | * Does the object already contain the specified filter? |
897 | * |
898 | * @param string $filter A filter string from url : "field:value" |
899 | * |
900 | * @return bool |
901 | */ |
902 | public function hasFilter($filter) |
903 | { |
904 | // Extract field and value from URL string: |
905 | [$field, $value] = $this->parseFilter($filter); |
906 | |
907 | // Check all of the relevant fields for matches: |
908 | foreach ($this->getAliasesForFacetField($field) as $current) { |
909 | if ( |
910 | isset($this->filterList[$current]) |
911 | && in_array($value, $this->filterList[$current]) |
912 | ) { |
913 | return true; |
914 | } |
915 | } |
916 | return false; |
917 | } |
918 | |
919 | /** |
920 | * Take a filter string and add it into the protected |
921 | * array checking for duplicates. |
922 | * |
923 | * @param string $newFilter A filter string from url : "field:value" |
924 | * |
925 | * @return void |
926 | */ |
927 | public function addFilter($newFilter) |
928 | { |
929 | // Check for duplicates -- if it's not in the array, we can add it |
930 | if (!$this->hasFilter($newFilter)) { |
931 | // Extract field and value from filter string: |
932 | [$field, $value] = $this->parseFilter($newFilter); |
933 | $this->filterList[$field][] = $value; |
934 | } |
935 | } |
936 | |
937 | /** |
938 | * Detects if a filter is advanced (true) or simple (false). An advanced |
939 | * filter is currently defined as one surrounded by parentheses (possibly |
940 | * with a leading exclusion operator), while a simple filter is of the form |
941 | * field:value. Advanced filters are used to express more complex queries, |
942 | * such as combining multiple values from multiple fields using boolean |
943 | * operators. |
944 | * |
945 | * @param string $filter A filter string |
946 | * |
947 | * @return bool |
948 | */ |
949 | public function isAdvancedFilter($filter) |
950 | { |
951 | return str_starts_with($filter, '(') || str_starts_with($filter, '-('); |
952 | } |
953 | |
954 | /** |
955 | * Remove a filter from the list. |
956 | * |
957 | * @param string $oldFilter A filter string from url : "field:value" |
958 | * |
959 | * @return void |
960 | */ |
961 | public function removeFilter($oldFilter) |
962 | { |
963 | // Extract field and value from URL string: |
964 | [$field, $value] = $this->parseFilter($oldFilter); |
965 | |
966 | // Make sure the field exists |
967 | if (isset($this->filterList[$field])) { |
968 | // Assume by default that we will not need to rebuild the array: |
969 | $rebuildArray = false; |
970 | |
971 | // Loop through all filters on the field |
972 | foreach ($this->filterList[$field] as $i => $currentFilter) { |
973 | // Does it contain the value we don't want? |
974 | if ($currentFilter == $value) { |
975 | // If so remove it. |
976 | unset($this->filterList[$field][$i]); |
977 | |
978 | // Flag that we now need to rebuild the array: |
979 | $rebuildArray = true; |
980 | } |
981 | } |
982 | |
983 | // If necessary, rebuild the array to remove gaps in the key sequence: |
984 | if ($rebuildArray) { |
985 | $this->filterList[$field] = array_values($this->filterList[$field]); |
986 | if (!$this->filterList[$field]) { |
987 | unset($this->filterList[$field]); |
988 | } |
989 | } |
990 | } |
991 | } |
992 | |
993 | /** |
994 | * Remove all filters from the list. |
995 | * |
996 | * @param string $field Name of field to remove filters from (null to remove |
997 | * all filters from all fields) |
998 | * |
999 | * @return void |
1000 | */ |
1001 | public function removeAllFilters($field = null) |
1002 | { |
1003 | if ($field == null) { |
1004 | $this->filterList = []; |
1005 | } else { |
1006 | foreach (['', '-', '~'] as $prefix) { |
1007 | if (isset($this->filterList[$prefix . $field])) { |
1008 | unset($this->filterList[$prefix . $field]); |
1009 | } |
1010 | } |
1011 | } |
1012 | } |
1013 | |
1014 | /** |
1015 | * Add a field to facet on. |
1016 | * |
1017 | * @param string $newField Field name |
1018 | * @param string $newAlias Optional on-screen display label |
1019 | * @param bool $ored Should we treat this as an ORed facet? |
1020 | * |
1021 | * @return void |
1022 | */ |
1023 | public function addFacet($newField, $newAlias = null, $ored = false) |
1024 | { |
1025 | if ($newAlias == null) { |
1026 | $newAlias = $newField; |
1027 | } |
1028 | $this->facetConfig[$newField] = $newAlias; |
1029 | if ($ored) { |
1030 | $this->orFacets[] = $newField; |
1031 | } |
1032 | } |
1033 | |
1034 | /** |
1035 | * Get facet operator for the specified field |
1036 | * |
1037 | * @param string $field Field name |
1038 | * |
1039 | * @return string |
1040 | */ |
1041 | public function getFacetOperator($field) |
1042 | { |
1043 | return in_array($field, $this->orFacets) ? 'OR' : 'AND'; |
1044 | } |
1045 | |
1046 | /** |
1047 | * Add a checkbox facet. When the checkbox is checked, the specified filter |
1048 | * will be applied to the search. When the checkbox is not checked, no filter |
1049 | * will be applied. |
1050 | * |
1051 | * @param string $filter [field]:[value] pair to associate with checkbox |
1052 | * @param string $desc Description to associate with the checkbox |
1053 | * @param bool $dynamic Is this being added dynamically (true) or in response |
1054 | * to a user configuration (false)? |
1055 | * |
1056 | * @return void |
1057 | */ |
1058 | public function addCheckboxFacet($filter, $desc, $dynamic = false) |
1059 | { |
1060 | // Extract the facet field name from the filter, then add the |
1061 | // relevant information to the array. |
1062 | [$fieldName] = explode(':', $filter); |
1063 | $this->checkboxFacets[$fieldName][$filter] |
1064 | = compact('desc', 'filter', 'dynamic'); |
1065 | } |
1066 | |
1067 | /** |
1068 | * Get a user-friendly string to describe the provided facet field. |
1069 | * |
1070 | * @param string $field Facet field name. |
1071 | * @param string $value Facet value. |
1072 | * @param string $default Default field name (null for default behavior). |
1073 | * |
1074 | * @return string Human-readable description of field. |
1075 | */ |
1076 | public function getFacetLabel($field, $value = null, $default = null) |
1077 | { |
1078 | if ( |
1079 | !isset($this->facetConfig[$field]) |
1080 | && !isset($this->extraFacetLabels[$field]) |
1081 | && isset($this->facetAliases[$field]) |
1082 | ) { |
1083 | $field = $this->facetAliases[$field]; |
1084 | } |
1085 | $checkboxFacet = $this->checkboxFacets[$field]["$field:$value"] ?? null; |
1086 | if (null !== $checkboxFacet) { |
1087 | return $checkboxFacet['desc']; |
1088 | } |
1089 | if (isset($this->facetConfig[$field])) { |
1090 | return $this->facetConfig[$field]; |
1091 | } |
1092 | return $this->extraFacetLabels[$field] |
1093 | ?? ($default ?: 'unrecognized_facet_label'); |
1094 | } |
1095 | |
1096 | /** |
1097 | * Get the current facet configuration. |
1098 | * |
1099 | * @return array |
1100 | */ |
1101 | public function getFacetConfig() |
1102 | { |
1103 | return $this->facetConfig; |
1104 | } |
1105 | |
1106 | /** |
1107 | * Reset the current facet configuration. |
1108 | * |
1109 | * @return void |
1110 | */ |
1111 | public function resetFacetConfig() |
1112 | { |
1113 | $this->facetConfig = []; |
1114 | } |
1115 | |
1116 | /** |
1117 | * Get the raw filter list. |
1118 | * |
1119 | * @return array |
1120 | */ |
1121 | public function getRawFilters() |
1122 | { |
1123 | return $this->filterList; |
1124 | } |
1125 | |
1126 | /** |
1127 | * Return an array structure containing information about all current filters. |
1128 | * |
1129 | * @param bool $excludeCheckboxFilters Should we exclude checkbox filters from |
1130 | * the list (to be used as a complement to getCheckboxFacets()). |
1131 | * |
1132 | * @return array Field, values and translation status |
1133 | */ |
1134 | public function getFilterList($excludeCheckboxFilters = false) |
1135 | { |
1136 | // If we don't have any filters, return right away to avoid further |
1137 | // processing: |
1138 | if (!$this->filterList) { |
1139 | return []; |
1140 | } |
1141 | |
1142 | // Get a list of checkbox filters to skip if necessary: |
1143 | $skipList = $excludeCheckboxFilters |
1144 | ? $this->getCheckboxFacetValues() : []; |
1145 | |
1146 | $list = []; |
1147 | $translatedFacets = $this->getOptions()->getTranslatedFacets(); |
1148 | // Loop through all the current filter fields |
1149 | foreach ($this->filterList as $field => $values) { |
1150 | [$operator, $field] = $this->parseOperatorAndFieldName($field); |
1151 | $translate = in_array($field, $translatedFacets); |
1152 | // and each value currently used for that field |
1153 | foreach ($values as $value) { |
1154 | // Add to the list unless it's in the list of fields to skip: |
1155 | if ( |
1156 | !isset($skipList[$field]) |
1157 | || !in_array($value, $skipList[$field]) |
1158 | ) { |
1159 | $facetLabel = $this->getFacetLabel($field, $value); |
1160 | $list[$facetLabel][] = $this->formatFilterListEntry( |
1161 | $field, |
1162 | $value, |
1163 | $operator, |
1164 | $translate |
1165 | ); |
1166 | } |
1167 | } |
1168 | } |
1169 | return $list; |
1170 | } |
1171 | |
1172 | /** |
1173 | * Get the filter list as a query parameter array. |
1174 | * |
1175 | * Returns an array of strings that parseFilter can parse. |
1176 | * |
1177 | * @return array |
1178 | */ |
1179 | public function getFiltersAsQueryParams(): array |
1180 | { |
1181 | return $this->formatFilterArrayAsQueryParams($this->getRawFilters()); |
1182 | } |
1183 | |
1184 | /** |
1185 | * Get a display text for a facet field. |
1186 | * |
1187 | * @param string $field Facet field |
1188 | * @param string $value Facet value |
1189 | * |
1190 | * @return string |
1191 | */ |
1192 | public function getFacetValueRawDisplayText(string $field, string $value): string |
1193 | { |
1194 | // Check for delimited facets -- if $field is a delimited facet field, |
1195 | // process $displayText accordingly: |
1196 | $delimitedFacetFields = $this->getOptions()->getDelimitedFacets(true); |
1197 | if (isset($delimitedFacetFields[$field])) { |
1198 | $parts = explode($delimitedFacetFields[$field], $value); |
1199 | return end($parts); |
1200 | } |
1201 | |
1202 | return $value; |
1203 | } |
1204 | |
1205 | /** |
1206 | * Translate a facet value. |
1207 | * |
1208 | * @param string $field Field name |
1209 | * @param string|TranslatableString $text Field value (processed by |
1210 | * getFacetValueRawDisplayText) |
1211 | * |
1212 | * @return string |
1213 | */ |
1214 | public function translateFacetValue(string $field, $text): string |
1215 | { |
1216 | $domain = $this->getOptions()->getTextDomainForTranslatedFacet($field); |
1217 | $translateFormat = $this->getOptions()->getFormatForTranslatedFacet($field); |
1218 | $translated = $this->translate([$domain, $text]); |
1219 | return $translateFormat |
1220 | ? $this->translate( |
1221 | $translateFormat, |
1222 | [ |
1223 | '%%raw%%' => $text, |
1224 | '%%translated%%' => $translated, |
1225 | ] |
1226 | ) : $translated; |
1227 | } |
1228 | |
1229 | /** |
1230 | * Format a single filter for use in getFilterList(). |
1231 | * |
1232 | * @param string $field Field name |
1233 | * @param string $value Field value |
1234 | * @param string $operator Operator (AND/OR/NOT) |
1235 | * @param bool $translate Should we translate the label? |
1236 | * |
1237 | * @return array |
1238 | */ |
1239 | protected function formatFilterListEntry($field, $value, $operator, $translate) |
1240 | { |
1241 | $rawDisplayText = $this->getFacetValueRawDisplayText($field, $value); |
1242 | $displayText = $translate |
1243 | ? $this->translateFacetValue($field, $rawDisplayText) |
1244 | : $rawDisplayText; |
1245 | |
1246 | return compact('value', 'displayText', 'field', 'operator'); |
1247 | } |
1248 | |
1249 | /** |
1250 | * Parse the operator and field name from a prefixed field string. |
1251 | * |
1252 | * @param string $field Prefixed string |
1253 | * |
1254 | * @return array (0 = operator, 1 = field name) |
1255 | */ |
1256 | protected function parseOperatorAndFieldName($field) |
1257 | { |
1258 | $firstChar = substr($field, 0, 1); |
1259 | if ($firstChar == '-') { |
1260 | $operator = 'NOT'; |
1261 | $field = substr($field, 1); |
1262 | } elseif ($firstChar == '~') { |
1263 | $operator = 'OR'; |
1264 | $field = substr($field, 1); |
1265 | } else { |
1266 | $operator = 'AND'; |
1267 | } |
1268 | return [$operator, $field]; |
1269 | } |
1270 | |
1271 | /** |
1272 | * Get a formatted list of checkbox filter values ($field => array of values). |
1273 | * |
1274 | * @return array |
1275 | */ |
1276 | protected function getCheckboxFacetValues() |
1277 | { |
1278 | $list = []; |
1279 | foreach ($this->getRawCheckboxFacets() as $facets) { |
1280 | foreach ($facets as $current) { |
1281 | [$field, $value] = $this->parseFilter($current['filter']); |
1282 | if (!isset($list[$field])) { |
1283 | $list[$field] = []; |
1284 | } |
1285 | $list[$field][] = $value; |
1286 | } |
1287 | } |
1288 | return $list; |
1289 | } |
1290 | |
1291 | /** |
1292 | * Get information on the current state of the boolean checkbox facets. |
1293 | * |
1294 | * @param array $include List of checkbox filters to return (null for all) |
1295 | * @param bool $includeDynamic Should we include dynamically-generated |
1296 | * checkboxes that are not part of the include list above? |
1297 | * |
1298 | * @return array |
1299 | */ |
1300 | public function getCheckboxFacets( |
1301 | array $include = null, |
1302 | bool $includeDynamic = true |
1303 | ) { |
1304 | // Build up an array of checkbox facets with status booleans and |
1305 | // toggle URLs. |
1306 | $result = []; |
1307 | foreach ($this->getRawCheckboxFacets() as $facets) { |
1308 | foreach ($facets as $facet) { |
1309 | // If the current filter is not on the include list, skip it (but |
1310 | // accept everything if the include list is null). |
1311 | if ( |
1312 | ($include !== null && !in_array($facet['filter'], $include)) |
1313 | && !($includeDynamic && $facet['dynamic']) |
1314 | ) { |
1315 | continue; |
1316 | } |
1317 | $facet['selected'] = $this->hasFilter($facet['filter']); |
1318 | // Is this checkbox always visible, even if non-selected on the |
1319 | // "no results" screen? By default, no (may be overridden by |
1320 | // child classes). |
1321 | $facet['alwaysVisible'] = false; |
1322 | $result[] = $facet; |
1323 | } |
1324 | } |
1325 | return $result; |
1326 | } |
1327 | |
1328 | /** |
1329 | * Return checkbox facets without any processing |
1330 | * |
1331 | * @return array |
1332 | */ |
1333 | protected function getRawCheckboxFacets(): array |
1334 | { |
1335 | return $this->checkboxFacets; |
1336 | } |
1337 | |
1338 | /** |
1339 | * Format a raw filter array as a query parameter array. |
1340 | * |
1341 | * Returns an array of strings that parseFilter can parse. |
1342 | * |
1343 | * @param array $filterArray Filter array |
1344 | * |
1345 | * @return array |
1346 | */ |
1347 | protected function formatFilterArrayAsQueryParams(array $filterArray): array |
1348 | { |
1349 | $result = []; |
1350 | foreach ($filterArray as $field => $values) { |
1351 | foreach ($values as $current) { |
1352 | $result[] = "$field:\"$current\""; |
1353 | } |
1354 | } |
1355 | return $result; |
1356 | } |
1357 | |
1358 | /** |
1359 | * Initialize all range filters. |
1360 | * |
1361 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
1362 | * request. |
1363 | * |
1364 | * @return void |
1365 | */ |
1366 | protected function initRangeFilters($request) |
1367 | { |
1368 | $this->initDateFilters($request); |
1369 | $this->initFullDateFilters($request); |
1370 | $this->initGenericRangeFilters($request); |
1371 | $this->initNumericRangeFilters($request); |
1372 | } |
1373 | |
1374 | /** |
1375 | * Support method for initDateFilters() -- normalize a year for use in a |
1376 | * year-based date range. |
1377 | * |
1378 | * @param ?string $year Value to check for valid year. |
1379 | * @param bool $rangeEnd Is this the end of a range? |
1380 | * |
1381 | * @return string Formatted year. |
1382 | * |
1383 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1384 | */ |
1385 | protected function formatYearForDateRange($year, $rangeEnd = false) |
1386 | { |
1387 | // Make sure parameter is set and numeric; default to wildcard otherwise: |
1388 | $year = ($year && preg_match('/\d{2,4}/', $year)) ? $year : '*'; |
1389 | |
1390 | // Pad to four digits: |
1391 | if (strlen($year) == 2) { |
1392 | $year = '19' . $year; |
1393 | } elseif (strlen($year) == 3) { |
1394 | $year = '0' . $year; |
1395 | } |
1396 | |
1397 | return $year; |
1398 | } |
1399 | |
1400 | /** |
1401 | * Support method for initFullDateFilters() -- normalize a date for use in a |
1402 | * year/month/day date range. |
1403 | * |
1404 | * @param ?string $date Value to check for valid date. |
1405 | * @param bool $rangeEnd Is this the end of a range? |
1406 | * |
1407 | * @return string Formatted date. |
1408 | */ |
1409 | protected function formatDateForFullDateRange($date, $rangeEnd = false) |
1410 | { |
1411 | // Make sure date is valid; default to wildcard otherwise: |
1412 | $date = $date ? SolrUtils::sanitizeDate($date, $rangeEnd) : null; |
1413 | return $date ?? '*'; |
1414 | } |
1415 | |
1416 | /** |
1417 | * Support method for initNumericRangeFilters() -- normalize a number for use in |
1418 | * a numeric range. |
1419 | * |
1420 | * @param ?string $num Value to format into a number. |
1421 | * @param bool $rangeEnd Is this the end of a range? |
1422 | * |
1423 | * @return string Formatted number. |
1424 | * |
1425 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1426 | */ |
1427 | protected function formatValueForNumericRange($num, $rangeEnd = false) |
1428 | { |
1429 | // empty strings, null values and non-numeric values are treated as wildcards: |
1430 | if ($num === '' || $num === null || !is_numeric($num)) { |
1431 | return '*'; |
1432 | } |
1433 | // If we got this far, it's a number! |
1434 | return $num; |
1435 | } |
1436 | |
1437 | /** |
1438 | * Support method for initGenericRangeFilters() -- build a filter query based on |
1439 | * a range of values. |
1440 | * |
1441 | * @param string $field field to use for filtering. |
1442 | * @param string $from start of range. |
1443 | * @param string $to end of range. |
1444 | * @param bool $cs Should ranges be case-sensitive? |
1445 | * |
1446 | * @return string filter query. |
1447 | */ |
1448 | protected function buildGenericRangeFilter($field, $from, $to, $cs = true) |
1449 | { |
1450 | // Assume Solr syntax -- this should be overridden in child classes where |
1451 | // other indexing methodologies are used. |
1452 | $range = "{$field}:[{$from} TO {$to}]"; |
1453 | if (!$cs) { |
1454 | // Flip values if out of order: |
1455 | if (strcmp(strtolower($from), strtolower($to)) > 0) { |
1456 | $range = "{$field}:[{$to} TO {$from}]"; |
1457 | } |
1458 | $helper = new LuceneSyntaxHelper(false, false); |
1459 | $range = $helper->capitalizeRanges($range); |
1460 | } |
1461 | return $range; |
1462 | } |
1463 | |
1464 | /** |
1465 | * Support method for initFilters() -- initialize range filters. Factored |
1466 | * out as a separate method so that it can be more easily overridden by child |
1467 | * classes. |
1468 | * |
1469 | * @param \Laminas\Stdlib\Parameters $request Parameter object |
1470 | * representing user request. |
1471 | * @param string $requestParam Name of parameter |
1472 | * containing names of range filter fields. |
1473 | * @param callable $valueFilter Optional callback to |
1474 | * process values in the range. |
1475 | * @param callable $filterGenerator Optional callback to create |
1476 | * a filter query from the range values. |
1477 | * |
1478 | * @return void |
1479 | */ |
1480 | protected function initGenericRangeFilters( |
1481 | $request, |
1482 | $requestParam = 'genericrange', |
1483 | $valueFilter = null, |
1484 | $filterGenerator = null |
1485 | ) { |
1486 | $rangeFacets = $request->get($requestParam); |
1487 | if (!empty($rangeFacets)) { |
1488 | $ranges = is_array($rangeFacets) ? $rangeFacets : [$rangeFacets]; |
1489 | foreach ($ranges as $range) { |
1490 | // Load start and end of range: |
1491 | $from = $request->get($range . 'from'); |
1492 | $to = $request->get($range . 'to'); |
1493 | |
1494 | // Apply filtering/validation if necessary: |
1495 | if (is_callable($valueFilter)) { |
1496 | $from = call_user_func($valueFilter, $from, false); |
1497 | $to = call_user_func($valueFilter, $to, true); |
1498 | } |
1499 | |
1500 | // Build filter only if necessary: |
1501 | if (!empty($range) && ($from != '*' || $to != '*')) { |
1502 | $rangeFacet = is_callable($filterGenerator) |
1503 | ? call_user_func($filterGenerator, $range, $from, $to) |
1504 | : $this->buildGenericRangeFilter($range, $from, $to, false); |
1505 | $this->addFilter($rangeFacet); |
1506 | } |
1507 | } |
1508 | } |
1509 | } |
1510 | |
1511 | /** |
1512 | * Support method for initNumericRangeFilters() -- build a filter query based on |
1513 | * a range of numbers. |
1514 | * |
1515 | * @param string $field field to use for filtering. |
1516 | * @param string $from number for start of range. |
1517 | * @param string $to number for end of range. |
1518 | * |
1519 | * @return string filter query. |
1520 | */ |
1521 | protected function buildNumericRangeFilter($field, $from, $to) |
1522 | { |
1523 | // Make sure that $to is less than $from: |
1524 | if ($to != '*' && $from != '*' && $to < $from) { |
1525 | $tmp = $to; |
1526 | $to = $from; |
1527 | $from = $tmp; |
1528 | } |
1529 | |
1530 | return $this->buildGenericRangeFilter($field, $from, $to); |
1531 | } |
1532 | |
1533 | /** |
1534 | * Support method for initDateFilters() -- build a filter query based on a range |
1535 | * of 4-digit years. |
1536 | * |
1537 | * @param string $field field to use for filtering. |
1538 | * @param string $from year for start of range. |
1539 | * @param string $to year for end of range. |
1540 | * |
1541 | * @return string filter query. |
1542 | */ |
1543 | protected function buildDateRangeFilter($field, $from, $to) |
1544 | { |
1545 | // Dates work just like numbers: |
1546 | return $this->buildNumericRangeFilter($field, $from, $to); |
1547 | } |
1548 | |
1549 | /** |
1550 | * Support method for initFullDateFilters() -- build a filter query based on a |
1551 | * range of dates. |
1552 | * |
1553 | * @param string $field field to use for filtering. |
1554 | * @param string $from year for start of range. |
1555 | * @param string $to year for end of range. |
1556 | * |
1557 | * @return string filter query. |
1558 | */ |
1559 | protected function buildFullDateRangeFilter($field, $from, $to) |
1560 | { |
1561 | // Make sure that $to is less than $from: |
1562 | if ($to != '*' && $from != '*' && strtotime($to) < strtotime($from)) { |
1563 | $tmp = $to; |
1564 | $to = $from; |
1565 | $from = $tmp; |
1566 | } |
1567 | |
1568 | return $this->buildGenericRangeFilter($field, $from, $to); |
1569 | } |
1570 | |
1571 | /** |
1572 | * Support method for initFilters() -- initialize year-based date filters. |
1573 | * Factored out as a separate method so that it can be more easily overridden |
1574 | * by child classes. |
1575 | * |
1576 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
1577 | * request. |
1578 | * |
1579 | * @return void |
1580 | */ |
1581 | protected function initDateFilters($request) |
1582 | { |
1583 | $this->initGenericRangeFilters( |
1584 | $request, |
1585 | 'daterange', |
1586 | [$this, 'formatYearForDateRange'], |
1587 | [$this, 'buildDateRangeFilter'] |
1588 | ); |
1589 | } |
1590 | |
1591 | /** |
1592 | * Support method for initFilters() -- initialize year/month/day-based date |
1593 | * filters. Factored out as a separate method so that it can be more easily |
1594 | * overridden by child classes. |
1595 | * |
1596 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
1597 | * request. |
1598 | * |
1599 | * @return void |
1600 | */ |
1601 | protected function initFullDateFilters($request) |
1602 | { |
1603 | $this->initGenericRangeFilters( |
1604 | $request, |
1605 | 'fulldaterange', |
1606 | [$this, 'formatDateForFullDateRange'], |
1607 | [$this, 'buildFullDateRangeFilter'] |
1608 | ); |
1609 | } |
1610 | |
1611 | /** |
1612 | * Support method for initFilters() -- initialize numeric range filters. Factored |
1613 | * out as a separate method so that it can be more easily overridden by child |
1614 | * classes. |
1615 | * |
1616 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
1617 | * request. |
1618 | * |
1619 | * @return void |
1620 | */ |
1621 | protected function initNumericRangeFilters($request) |
1622 | { |
1623 | $this->initGenericRangeFilters( |
1624 | $request, |
1625 | 'numericrange', |
1626 | [$this, 'formatValueForNumericRange'], |
1627 | [$this, 'buildNumericRangeFilter'] |
1628 | ); |
1629 | } |
1630 | |
1631 | /** |
1632 | * Add filters to the object based on values found in the request object. |
1633 | * |
1634 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
1635 | * request. |
1636 | * |
1637 | * @return void |
1638 | */ |
1639 | protected function initFilters($request) |
1640 | { |
1641 | // Handle standard filters: |
1642 | $filter = $request->get('filter'); |
1643 | if (!empty($filter)) { |
1644 | if (is_array($filter)) { |
1645 | foreach ($filter as $current) { |
1646 | $this->addFilter($current); |
1647 | } |
1648 | } else { |
1649 | $this->addFilter($filter); |
1650 | } |
1651 | } |
1652 | |
1653 | // If we don't have the special flag indicating that defaults have |
1654 | // been applied, and if we do have defaults, apply them: |
1655 | if ($request->get('dfApplied')) { |
1656 | $this->defaultsApplied = true; |
1657 | } else { |
1658 | $defaults = $this->getOptions()->getDefaultFilters(); |
1659 | if (!empty($defaults)) { |
1660 | foreach ($defaults as $current) { |
1661 | $this->addFilter($current); |
1662 | } |
1663 | $this->defaultsApplied = true; |
1664 | } |
1665 | } |
1666 | |
1667 | // Handle range filters: |
1668 | $this->initRangeFilters($request); |
1669 | } |
1670 | |
1671 | /** |
1672 | * Add hidden filters to the object based on values found in the request object. |
1673 | * |
1674 | * @param \Laminas\Stdlib\Parameters $request Parameter object representing user |
1675 | * request. |
1676 | * |
1677 | * @return void |
1678 | */ |
1679 | protected function initHiddenFilters($request) |
1680 | { |
1681 | $hiddenFilters = $request->get('hiddenFilters'); |
1682 | if (!empty($hiddenFilters) && is_array($hiddenFilters)) { |
1683 | foreach ($hiddenFilters as $current) { |
1684 | $this->addHiddenFilter($current); |
1685 | } |
1686 | } |
1687 | } |
1688 | |
1689 | /** |
1690 | * Get hidden filters grouped by field like normal filters. |
1691 | * |
1692 | * @return array |
1693 | */ |
1694 | public function getHiddenFilters() |
1695 | { |
1696 | return $this->hiddenFilters; |
1697 | } |
1698 | |
1699 | /** |
1700 | * Get the hidden filter list as a query parameter array. |
1701 | * |
1702 | * Returns an array of strings that parseFilter can parse. |
1703 | * |
1704 | * @return array |
1705 | */ |
1706 | public function getHiddenFiltersAsQueryParams(): array |
1707 | { |
1708 | return $this->formatFilterArrayAsQueryParams($this->getHiddenFilters()); |
1709 | } |
1710 | |
1711 | /** |
1712 | * Does the object already contain the specified hidden filter? |
1713 | * |
1714 | * @param string $filter A filter string from url : "field:value" |
1715 | * |
1716 | * @return bool |
1717 | */ |
1718 | public function hasHiddenFilter($filter) |
1719 | { |
1720 | // Extract field and value from URL string: |
1721 | [$field, $value] = $this->parseFilter($filter); |
1722 | |
1723 | if ( |
1724 | isset($this->hiddenFilters[$field]) |
1725 | && in_array($value, $this->hiddenFilters[$field]) |
1726 | ) { |
1727 | return true; |
1728 | } |
1729 | return false; |
1730 | } |
1731 | |
1732 | /** |
1733 | * Take a filter string and add it into the protected hidden filters |
1734 | * array checking for duplicates. |
1735 | * |
1736 | * @param string $newFilter A filter string from url : "field:value" |
1737 | * |
1738 | * @return void |
1739 | */ |
1740 | public function addHiddenFilter($newFilter) |
1741 | { |
1742 | // Check for duplicates -- if it's not in the array, we can add it |
1743 | if (!$this->hasHiddenFilter($newFilter)) { |
1744 | // Extract field and value from filter string: |
1745 | [$field, $value] = $this->parseFilter($newFilter); |
1746 | if (!empty($field) && '' !== $value) { |
1747 | $this->hiddenFilters[$field][] = $value; |
1748 | } |
1749 | } |
1750 | } |
1751 | |
1752 | /** |
1753 | * Take a filter string and add it into the protected hidden filters |
1754 | * array checking for duplicates. |
1755 | * |
1756 | * @param string $field Field |
1757 | * @param string $value Filter value |
1758 | * |
1759 | * @return void |
1760 | */ |
1761 | public function addHiddenFilterForField(string $field, string $value): void |
1762 | { |
1763 | $this->addHiddenFilter("$field:\"$value\""); |
1764 | } |
1765 | |
1766 | /** |
1767 | * Return a query string for the current search with a search term replaced. |
1768 | * |
1769 | * @param string $oldTerm The old term to replace |
1770 | * @param string $newTerm The new term to search |
1771 | * |
1772 | * @return string query string |
1773 | */ |
1774 | public function getDisplayQueryWithReplacedTerm($oldTerm, $newTerm) |
1775 | { |
1776 | // Stash our old data for a minute |
1777 | $oldTerms = clone $this->query; |
1778 | // Replace the search term |
1779 | $this->query->replaceTerm($oldTerm, $newTerm); |
1780 | // Get the new query string |
1781 | $query = $this->getDisplayQuery(); |
1782 | // Restore the old data |
1783 | $this->query = $oldTerms; |
1784 | // Return the query string |
1785 | return $query; |
1786 | } |
1787 | |
1788 | /** |
1789 | * Basic 'getter' for list of available view options. |
1790 | * |
1791 | * @return array |
1792 | */ |
1793 | public function getViewList() |
1794 | { |
1795 | $list = []; |
1796 | foreach ($this->getOptions()->getViewOptions() as $key => $value) { |
1797 | $list[$key] = [ |
1798 | 'desc' => $value, |
1799 | 'selected' => ($key == $this->getView()), |
1800 | ]; |
1801 | } |
1802 | return $list; |
1803 | } |
1804 | |
1805 | /** |
1806 | * Return a list of urls for possible limits, along with which option |
1807 | * should be currently selected. |
1808 | * |
1809 | * @return array Limit urls, descriptions and selected flags |
1810 | */ |
1811 | public function getLimitList() |
1812 | { |
1813 | // Loop through all the current limits |
1814 | $valid = $this->getOptions()->getLimitOptions(); |
1815 | $defaultLimit = $this->getOptions()->getDefaultLimit(); |
1816 | $list = []; |
1817 | foreach ($valid as $limit) { |
1818 | $list[$limit] = [ |
1819 | 'desc' => $limit, |
1820 | 'selected' => ($limit == $this->getLimit()), |
1821 | 'default' => $limit == $defaultLimit, |
1822 | ]; |
1823 | } |
1824 | return $list; |
1825 | } |
1826 | |
1827 | /** |
1828 | * Return a list of urls for sorting, along with which option |
1829 | * should be currently selected. |
1830 | * |
1831 | * @return array Sort urls, descriptions and selected flags |
1832 | */ |
1833 | public function getSortList() |
1834 | { |
1835 | // Loop through all the current filter fields |
1836 | $valid = $this->getOptions()->getSortOptions(); |
1837 | $defaultSort = $this->getDefaultSort(); |
1838 | $list = []; |
1839 | foreach ($valid as $sort => $desc) { |
1840 | $list[$sort] = [ |
1841 | 'desc' => $desc, |
1842 | 'selected' => ($sort == $this->getSort()), |
1843 | 'default' => $sort == $defaultSort, |
1844 | ]; |
1845 | } |
1846 | return $list; |
1847 | } |
1848 | |
1849 | /** |
1850 | * Store settings to a minified object |
1851 | * |
1852 | * @param Minified $minified Minified Search Object |
1853 | * |
1854 | * @return void |
1855 | */ |
1856 | public function minify(Minified &$minified): void |
1857 | { |
1858 | $minified->ty = $this->getSearchType(); |
1859 | $minified->cl = $this->getSearchClassId(); |
1860 | |
1861 | // Search terms, we'll shorten keys |
1862 | $query = $this->getQuery(); |
1863 | $minified->t = $this->getQueryAdapter()->minify($query); |
1864 | |
1865 | // It would be nice to shorten filter fields too, but |
1866 | // it would be a nightmare to maintain. |
1867 | $minified->f = $this->getRawFilters(); |
1868 | $minified->hf = $this->getHiddenFilters(); |
1869 | |
1870 | $minified->scp = [ |
1871 | 'page' => $this->getPage(), |
1872 | 'limit' => $this->getLimit(), |
1873 | ]; |
1874 | } |
1875 | |
1876 | /** |
1877 | * Restore settings from a minified object found in the database. |
1878 | * |
1879 | * @param \VuFind\Search\Minified $minified Minified Search Object |
1880 | * |
1881 | * @return void |
1882 | */ |
1883 | public function deminify($minified) |
1884 | { |
1885 | // Some values will transfer without changes |
1886 | $this->filterList = $minified->f; |
1887 | $this->hiddenFilters = $minified->hf; |
1888 | $this->searchType = $minified->ty; |
1889 | $this->searchContextParameters = $minified->scp; |
1890 | |
1891 | // Deminified searches will always have defaults already applied; |
1892 | // we don't want to accidentally manipulate them further. |
1893 | $defaults = $this->getOptions()->getDefaultFilters(); |
1894 | if (!empty($defaults)) { |
1895 | $this->defaultsApplied = true; |
1896 | } |
1897 | |
1898 | // Search terms, we need to expand keys |
1899 | $this->query = $this->getQueryAdapter()->deminify($minified->t); |
1900 | } |
1901 | |
1902 | /** |
1903 | * Get remembered search context parameters from saved search. We track these separately since |
1904 | * in some contexts we want to use them (e.g. linking back to a search in breadcrumbs), but in |
1905 | * other contexts we want to ignore them (e.g. comparing two searches to see if they represent |
1906 | * the same query -- because page 1 and page 2 still represent the same overall search). |
1907 | * |
1908 | * @return array |
1909 | */ |
1910 | public function getSavedSearchContextParameters(): array |
1911 | { |
1912 | return $this->searchContextParameters; |
1913 | } |
1914 | |
1915 | /** |
1916 | * Override the normal search behavior with an explicit array of IDs that must |
1917 | * be retrieved. |
1918 | * |
1919 | * @param array $ids Record IDs to load |
1920 | * |
1921 | * @return void |
1922 | * |
1923 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1924 | */ |
1925 | public function setQueryIDs($ids) |
1926 | { |
1927 | // This needs to be defined in child classes: |
1928 | throw new \Exception(get_class($this) . ' does not support setQueryIDs().'); |
1929 | } |
1930 | |
1931 | /** |
1932 | * Get the maximum number of IDs that may be sent to setQueryIDs (-1 for no |
1933 | * limit). |
1934 | * |
1935 | * @return int |
1936 | */ |
1937 | public function getQueryIDLimit() |
1938 | { |
1939 | return -1; |
1940 | } |
1941 | |
1942 | /** |
1943 | * Get an array of the names of all selected shards. These should correspond |
1944 | * with keys in the array returned by the option class's getShards() method. |
1945 | * |
1946 | * @return array |
1947 | */ |
1948 | public function getSelectedShards() |
1949 | { |
1950 | return $this->selectedShards; |
1951 | } |
1952 | |
1953 | /** |
1954 | * Translate a string (or string-castable object) |
1955 | * |
1956 | * @param string|object|array $target String to translate or an array of text |
1957 | * domain and string to translate |
1958 | * @param array $tokens Tokens to inject into the translated |
1959 | * string |
1960 | * @param string $default Default value to use if no translation is |
1961 | * found (null for no default). |
1962 | * |
1963 | * @return string |
1964 | */ |
1965 | public function translate($target, $tokens = [], $default = null) |
1966 | { |
1967 | return $this->getOptions()->translate($target, $tokens, $default); |
1968 | } |
1969 | |
1970 | /** |
1971 | * Set the override query |
1972 | * |
1973 | * @param string $q Override query |
1974 | * |
1975 | * @return void |
1976 | */ |
1977 | public function setOverrideQuery($q) |
1978 | { |
1979 | $this->overrideQuery = $q; |
1980 | } |
1981 | |
1982 | /** |
1983 | * Get the override query |
1984 | * |
1985 | * @return string |
1986 | */ |
1987 | public function getOverrideQuery() |
1988 | { |
1989 | return $this->overrideQuery; |
1990 | } |
1991 | |
1992 | /** |
1993 | * Return search query object. |
1994 | * |
1995 | * @return AbstractQuery |
1996 | */ |
1997 | public function getQuery() |
1998 | { |
1999 | if ($this->overrideQuery) { |
2000 | return new Query($this->overrideQuery); |
2001 | } |
2002 | return $this->query; |
2003 | } |
2004 | |
2005 | /** |
2006 | * Set search query object. |
2007 | * |
2008 | * @param AbstractQuery $query Query |
2009 | * |
2010 | * @return void |
2011 | */ |
2012 | public function setQuery(AbstractQuery $query): void |
2013 | { |
2014 | if ($this->overrideQuery) { |
2015 | $this->overrideQuery = false; |
2016 | } |
2017 | $this->query = $query; |
2018 | } |
2019 | |
2020 | /** |
2021 | * Initialize facet settings for the specified configuration sections. |
2022 | * |
2023 | * @param string $facetList Config section containing fields to activate |
2024 | * @param string $facetSettings Config section containing related settings |
2025 | * @param string $cfgFile Name of configuration to load (null to load |
2026 | * default facets configuration). |
2027 | * |
2028 | * @return bool True if facets set, false if no settings found |
2029 | */ |
2030 | protected function initFacetList($facetList, $facetSettings, $cfgFile = null) |
2031 | { |
2032 | $config = $this->configLoader |
2033 | ->get($cfgFile ?? $this->getOptions()->getFacetsIni()); |
2034 | if (!isset($config->$facetList)) { |
2035 | return false; |
2036 | } |
2037 | if (isset($config->$facetSettings->orFacets)) { |
2038 | $orFields |
2039 | = array_map('trim', explode(',', $config->$facetSettings->orFacets)); |
2040 | } else { |
2041 | $orFields = []; |
2042 | } |
2043 | foreach ($config->$facetList as $key => $value) { |
2044 | $useOr = (isset($orFields[0]) && $orFields[0] == '*') |
2045 | || in_array($key, $orFields); |
2046 | $this->addFacet($key, $value, $useOr); |
2047 | } |
2048 | |
2049 | return true; |
2050 | } |
2051 | |
2052 | /** |
2053 | * Are default filters applied? |
2054 | * |
2055 | * @return bool |
2056 | */ |
2057 | public function hasDefaultsApplied() |
2058 | { |
2059 | return $this->defaultsApplied; |
2060 | } |
2061 | |
2062 | /** |
2063 | * Initialize checkbox facet settings for the specified configuration sections. |
2064 | * |
2065 | * @param string $facetList Config section containing fields to activate |
2066 | * @param string $cfgFile Name of configuration to load (null to load |
2067 | * default facets configuration). |
2068 | * |
2069 | * @return bool True if facets set, false if no settings found |
2070 | */ |
2071 | protected function initCheckboxFacets( |
2072 | $facetList = 'CheckboxFacets', |
2073 | $cfgFile = null |
2074 | ) { |
2075 | $config = $this->configLoader |
2076 | ->get($cfgFile ?? $this->getOptions()->getFacetsIni()); |
2077 | $retVal = false; |
2078 | // If the section is in reverse order, the tilde will flag this: |
2079 | if (str_starts_with($facetList, '~')) { |
2080 | foreach ($config->{substr($facetList, 1)} ?? [] as $value => $key) { |
2081 | $this->addCheckboxFacet($key, $value); |
2082 | $retVal = true; |
2083 | } |
2084 | } else { |
2085 | foreach ($config->$facetList ?? [] as $key => $value) { |
2086 | $this->addCheckboxFacet($key, $value); |
2087 | $retVal = true; |
2088 | } |
2089 | } |
2090 | return $retVal; |
2091 | } |
2092 | |
2093 | /** |
2094 | * Check whether a specific facet supports filtering |
2095 | * |
2096 | * @param string $facet The facet to check |
2097 | * |
2098 | * @return bool |
2099 | */ |
2100 | public function supportsFacetFiltering($facet) |
2101 | { |
2102 | $translatedFacets = $this->getOptions()->getTranslatedFacets(); |
2103 | return method_exists($this, 'setFacetContains') && !in_array($facet, $translatedFacets); |
2104 | } |
2105 | } |