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