Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
41.62% covered (danger)
41.62%
82 / 197
38.03% covered (danger)
38.03%
27 / 71
CRAP
0.00% covered (danger)
0.00%
0 / 1
Options
41.62% covered (danger)
41.62%
82 / 197
38.03% covered (danger)
38.03%
27 / 71
2984.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
76.92% covered (warning)
76.92%
20 / 26
0.00% covered (danger)
0.00%
0 / 1
5.31
 setConfigLoader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSpecialAdvancedFacets
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAdvancedHandlers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBasicHandlers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHandlerForLabel
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 getLabelForBasicHandler
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultHandler
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getDefaultLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLimitOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFacetsIni
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMainIni
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchIni
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLimitOptions
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 getSortOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHiddenSortOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFacetSortOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultSortByHandler
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getRssSort
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getDefaultView
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getViewOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultFacetDelimiter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultFacetDelimiter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDelimitedFacets
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
7.35
 setDelimitedFacets
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTranslatedFacets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTranslatedFacets
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getTextDomainForTranslatedFacet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFormatForTranslatedFacet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHierarchicalFacets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHierarchicalFacetSeparators
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHierarchicalFacetSortSettings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 spellcheckEnabled
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 highlightEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHumanReadableFieldName
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 disableHighlighting
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 autocompleteEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 autocompleteAutoSubmit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAutocompleteFormattingRules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getListViewOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchAction
n/a
0 / 0
n/a
0 / 0
0
 getSearchHomeAction
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAdvancedSearchAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFacetListAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVersionsAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCitesAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCitedByAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supportsCart
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultFilters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRetainFilterSetting
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 shouldDisplayResetFilters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getShards
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultSelectedShards
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 showShardCheckboxes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVisibleSearchResultLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAPISettings
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getRecommendationSettings
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
110
 getSearchClassId
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 getSearchBoxSearchClassId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supportsFirstLastNavigation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 firstLastNavigationSupported
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 recordFirstLastNavigationEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 supportsScheduledSearch
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadResultsWithJsEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTopPaginatorStyle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSpellingNormalizer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 displayCitationLinksInResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 configureAutocomplete
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 limitOrderOverride
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getFilterHierarchicalFacetsInAdvanced
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHierarchicalExcludeFilters
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getHierarchicalFacetFilters
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * Abstract options 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   Juha Luoma <juha.luoma@helsinki.fi>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org Main Page
29 */
30
31namespace VuFind\Search\Base;
32
33use Laminas\Config\Config;
34use VuFind\I18n\Translator\TranslatorAwareInterface;
35
36use function count;
37use function get_class;
38use function in_array;
39use function intval;
40use function is_array;
41use function is_string;
42
43/**
44 * Abstract options search model.
45 *
46 * This abstract class defines the option methods for modeling a search in VuFind.
47 *
48 * @category VuFind
49 * @package  Search_Base
50 * @author   Demian Katz <demian.katz@villanova.edu>
51 * @author   Juha Luoma <juha.luoma@helsinki.fi>
52 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
53 * @link     https://vufind.org Main Page
54 */
55abstract class Options implements TranslatorAwareInterface
56{
57    use \VuFind\I18n\Translator\TranslatorAwareTrait;
58
59    /**
60     * Available sort options
61     *
62     * @var array
63     */
64    protected $sortOptions = [];
65
66    /**
67     * Allowed hidden sort options
68     *
69     * @var array
70     */
71    protected $hiddenSortOptions = [];
72
73    /**
74     * Available sort options for facets
75     *
76     * @var array
77     */
78    protected $facetSortOptions = [];
79
80    /**
81     * Overall default sort option
82     *
83     * @var string
84     */
85    protected $defaultSort = 'relevance';
86
87    /**
88     * Handler-specific defaults
89     *
90     * @var array
91     */
92    protected $defaultSortByHandler = [];
93
94    /**
95     * RSS-specific sort option
96     *
97     * @var string
98     */
99    protected $rssSort = null;
100
101    /**
102     * Default search handler
103     *
104     * @var string
105     */
106    protected $defaultHandler = null;
107
108    /**
109     * Advanced search handlers
110     *
111     * @var array
112     */
113    protected $advancedHandlers = [];
114
115    /**
116     * Basic search handlers
117     *
118     * @var array
119     */
120    protected $basicHandlers = [];
121
122    /**
123     * Special advanced facet settings
124     *
125     * @var string
126     */
127    protected $specialAdvancedFacets = '';
128
129    /**
130     * Should we retain filters by default?
131     *
132     * @var bool
133     */
134    protected $retainFiltersByDefault;
135
136    /**
137     * Should we display a "Reset Filters" link regardless of retainFiltersByDefault?
138     *
139     * @var bool
140     */
141    protected $alwaysDisplayResetFilters;
142
143    /**
144     * Default filters to apply to new searches
145     *
146     * @var array
147     */
148    protected $defaultFilters = [];
149
150    /**
151     * Default limit option
152     *
153     * @var int
154     */
155    protected $defaultLimit = 20;
156
157    /**
158     * Available limit options
159     *
160     * @var array
161     */
162    protected $limitOptions = [];
163
164    /**
165     * Default view option
166     *
167     * @var string
168     */
169    protected $defaultView = 'list';
170
171    /**
172     * Available view options
173     *
174     * @var array
175     */
176    protected $viewOptions = [];
177
178    /**
179     * Default delimiter used for delimited facets
180     *
181     * @var string
182     */
183    protected $defaultFacetDelimiter;
184
185    /**
186     * Facet settings
187     *
188     * @var array
189     */
190    protected $delimitedFacets = [];
191
192    /**
193     * Convenient field => delimiter lookup array derived from $delimitedFacets.
194     *
195     * @var array
196     */
197    protected $processedDelimitedFacets = null;
198
199    /**
200     * Facet settings
201     *
202     * @var array
203     */
204    protected $translatedFacets = [];
205
206    /**
207     * Text domains for translated facets
208     *
209     * @var array
210     */
211    protected $translatedFacetsTextDomains = [];
212
213    /**
214     * Formats for translated facets
215     *
216     * @var array
217     */
218    protected $translatedFacetsFormats = [];
219
220    /**
221     * Hierarchical facets
222     *
223     * @var array
224     */
225    protected $hierarchicalFacets = [];
226
227    /**
228     * Hierarchical facet separators
229     *
230     * @var array
231     */
232    protected $hierarchicalFacetSeparators = [];
233
234    /**
235     * Hierarchical facet sort settings
236     *
237     * @var array
238     */
239    protected $hierarchicalFacetSortSettings = [];
240
241    /**
242     * Spelling setting
243     *
244     * @var bool
245     */
246    protected $spellcheck = true;
247
248    /**
249     * Available shards
250     *
251     * @var array
252     */
253    protected $shards = [];
254
255    /**
256     * Default selected shards
257     *
258     * @var array
259     */
260    protected $defaultSelectedShards = [];
261
262    /**
263     * Should we present shard checkboxes to the user?
264     *
265     * @var bool
266     */
267    protected $visibleShardCheckboxes = false;
268
269    /**
270     * Highlighting setting
271     *
272     * @var bool
273     */
274    protected $highlight = false;
275
276    /**
277     * Autocomplete setting
278     *
279     * @var bool
280     */
281    protected $autocompleteEnabled = false;
282
283    /**
284     * Autocomplete auto submit setting
285     *
286     * @var bool
287     */
288    protected $autocompleteAutoSubmit = true;
289
290    /**
291     * Autocomplete query formatting rules
292     *
293     * @var array
294     */
295    protected $autocompleteFormattingRules = [];
296
297    /**
298     * Configuration file to read global settings from
299     *
300     * @var string
301     */
302    protected $mainIni = 'config';
303
304    /**
305     * Configuration file to read search settings from
306     *
307     * @var string
308     */
309    protected $searchIni = 'searches';
310
311    /**
312     * Configuration file to read facet settings from
313     *
314     * @var string
315     */
316    protected $facetsIni = 'facets';
317
318    /**
319     * Active list view option (see [List] in searches.ini).
320     *
321     * @var string
322     */
323    protected $listviewOption = 'full';
324
325    /**
326     * Configuration loader
327     *
328     * @var \VuFind\Config\PluginManager
329     */
330    protected $configLoader;
331
332    /**
333     * Maximum number of results (no limit by default)
334     *
335     * @var int
336     */
337    protected $resultLimit = -1;
338
339    /**
340     * Is first/last navigation supported by the backend?
341     *
342     * @var bool
343     */
344    protected $firstLastNavigationSupported = true;
345
346    /**
347     * Is the record page first/last navigation scroller enabled?
348     *
349     * @var bool
350     */
351    protected $recordPageFirstLastNavigation = false;
352
353    /**
354     * Should hierarchicalFacetFilters and hierarchicalExcludeFilters
355     * apply in advanced search
356     *
357     * @var bool
358     */
359    protected $filterHierarchicalFacetsInAdvanced = false;
360
361    /**
362     * Hierarchical exclude filters
363     *
364     * @var array
365     */
366    protected $hierarchicalExcludeFilters = [];
367
368    /**
369     * Hierarchical facet filters
370     *
371     * @var array
372     */
373    protected $hierarchicalFacetFilters = [];
374
375    /**
376     * Top pagination control style (none, simple or full)
377     *
378     * @var string
379     */
380    protected $topPaginatorStyle;
381
382    /**
383     * Is loading of results with JavaScript enabled?
384     *
385     * @var bool
386     */
387    protected $loadResultsWithJs;
388
389    /**
390     * Should we display citation search links in results?
391     *
392     * @var bool
393     */
394    protected $displayCitationLinksInResults;
395
396    /**
397     * Constructor
398     *
399     * @param \VuFind\Config\PluginManager $configLoader Config loader
400     */
401    public function __construct(\VuFind\Config\PluginManager $configLoader)
402    {
403        $this->limitOptions = [$this->defaultLimit];
404        $this->setConfigLoader($configLoader);
405
406        $id = $this->getSearchClassId();
407        $facetSettings = $configLoader->get($this->facetsIni);
408        if (isset($facetSettings->AvailableFacetSortOptions[$id])) {
409            $sortArray = $facetSettings->AvailableFacetSortOptions[$id]->toArray();
410            foreach ($sortArray as $facet => $sortOptions) {
411                $this->facetSortOptions[$facet] = [];
412                foreach (explode(',', $sortOptions) as $fieldAndLabel) {
413                    [$field, $label] = explode('=', $fieldAndLabel);
414                    $this->facetSortOptions[$facet][$field] = $label;
415                }
416            }
417        }
418        $this->filterHierarchicalFacetsInAdvanced
419            = !empty($facetSettings->Advanced_Settings->enable_hierarchical_filters);
420        $this->hierarchicalExcludeFilters
421            = $facetSettings?->HierarchicalExcludeFilters?->toArray() ?? [];
422        $this->hierarchicalFacetFilters
423            = $facetSettings?->HierarchicalFacetFilters?->toArray() ?? [];
424
425        $searchSettings = $configLoader->get($this->searchIni);
426        $this->retainFiltersByDefault = $searchSettings->General->retain_filters_by_default ?? true;
427        $this->alwaysDisplayResetFilters = $searchSettings->General->always_display_reset_filters ?? false;
428        $this->loadResultsWithJs = (bool)($searchSettings->General->load_results_with_js ?? true);
429        $this->topPaginatorStyle = $searchSettings->General->top_paginator
430            ?? ($this->loadResultsWithJs ? 'simple' : false);
431        $this->hiddenSortOptions = $searchSettings?->HiddenSorting?->pattern?->toArray() ?? [];
432        $this->displayCitationLinksInResults
433            = (bool)($searchSettings->Results_Settings->display_citation_links ?? true);
434    }
435
436    /**
437     * Set the config loader
438     *
439     * @param \VuFind\Config\PluginManager $configLoader Config loader
440     *
441     * @return void
442     */
443    public function setConfigLoader(\VuFind\Config\PluginManager $configLoader)
444    {
445        $this->configLoader = $configLoader;
446    }
447
448    /**
449     * Get string listing special advanced facet types.
450     *
451     * @return string
452     */
453    public function getSpecialAdvancedFacets()
454    {
455        return $this->specialAdvancedFacets;
456    }
457
458    /**
459     * Basic 'getter' for advanced search handlers.
460     *
461     * @return array
462     */
463    public function getAdvancedHandlers()
464    {
465        return $this->advancedHandlers;
466    }
467
468    /**
469     * Basic 'getter' for basic search handlers.
470     *
471     * @return array
472     */
473    public function getBasicHandlers()
474    {
475        return $this->basicHandlers;
476    }
477
478    /**
479     * Given a label from the configuration file, return the name of the matching
480     * handler (basic checked first, then advanced); return the default handler
481     * if no match is found.
482     *
483     * @param string $label Label to search for
484     *
485     * @return string
486     */
487    public function getHandlerForLabel($label)
488    {
489        $label = empty($label) ? false : $this->translate($label);
490
491        foreach ($this->getBasicHandlers() as $id => $currentLabel) {
492            if ($this->translate($currentLabel) == $label) {
493                return $id;
494            }
495        }
496        foreach ($this->getAdvancedHandlers() as $id => $currentLabel) {
497            if ($this->translate($currentLabel) == $label) {
498                return $id;
499            }
500        }
501        return $this->getDefaultHandler();
502    }
503
504    /**
505     * Given a basic handler name, return the corresponding label (or false
506     * if none found):
507     *
508     * @param string $handler Handler name to look up.
509     *
510     * @return string
511     */
512    public function getLabelForBasicHandler($handler)
513    {
514        $handlers = $this->getBasicHandlers();
515        return $handlers[$handler] ?? false;
516    }
517
518    /**
519     * Get default search handler.
520     *
521     * @return string
522     */
523    public function getDefaultHandler()
524    {
525        if (!empty($this->defaultHandler)) {
526            return $this->defaultHandler;
527        }
528        return current(array_keys($this->getBasicHandlers()));
529    }
530
531    /**
532     * Get default limit setting.
533     *
534     * @return int
535     */
536    public function getDefaultLimit()
537    {
538        return $this->defaultLimit;
539    }
540
541    /**
542     * Get an array of limit options.
543     *
544     * @return array
545     */
546    public function getLimitOptions()
547    {
548        return $this->limitOptions;
549    }
550
551    /**
552     * Get the name of the ini file used for configuring facet parameters in this
553     * object.
554     *
555     * @return string
556     */
557    public function getFacetsIni()
558    {
559        return $this->facetsIni;
560    }
561
562    /**
563     * Get the name of the ini file used for loading primary settings in this
564     * object.
565     *
566     * @return string
567     */
568    public function getMainIni()
569    {
570        return $this->mainIni;
571    }
572
573    /**
574     * Get the name of the ini file used for configuring search parameters in this
575     * object.
576     *
577     * @return string
578     */
579    public function getSearchIni()
580    {
581        return $this->searchIni;
582    }
583
584    /**
585     * Override the limit options.
586     *
587     * @param array $options New options to set.
588     *
589     * @return void
590     */
591    public function setLimitOptions($options)
592    {
593        if (is_array($options) && !empty($options)) {
594            $this->limitOptions = $options;
595
596            // If the current default limit is no longer legal, pick the
597            // first option in the array as the new default:
598            if (!in_array($this->defaultLimit, $this->limitOptions)) {
599                $this->defaultLimit = $this->limitOptions[0];
600            }
601        }
602    }
603
604    /**
605     * Get an array of sort options.
606     *
607     * @return array
608     */
609    public function getSortOptions()
610    {
611        return $this->sortOptions;
612    }
613
614    /**
615     * Get an array of hidden sort options.
616     *
617     * @return array
618     */
619    public function getHiddenSortOptions()
620    {
621        return $this->hiddenSortOptions;
622    }
623
624    /**
625     * Get an array of sort options for a facet.
626     *
627     * @param string $facet Facet
628     *
629     * @return array
630     */
631    public function getFacetSortOptions($facet = '*')
632    {
633        return $this->facetSortOptions[$facet] ?? $this->facetSortOptions['*'] ?? [];
634    }
635
636    /**
637     * Get the default sort option for the specified search handler.
638     *
639     * @param string $handler Search handler being used
640     *
641     * @return string
642     */
643    public function getDefaultSortByHandler($handler = null)
644    {
645        // Use default handler if none specified:
646        if (empty($handler)) {
647            $handler = $this->getDefaultHandler();
648        }
649        // Send back search-specific sort if available:
650        if (isset($this->defaultSortByHandler[$handler])) {
651            return $this->defaultSortByHandler[$handler];
652        }
653        // If no search-specific sort handler was found, use the overall default:
654        return $this->defaultSort;
655    }
656
657    /**
658     * Return the sorting value for RSS mode
659     *
660     * @param string $sort Sort setting to modify for RSS mode
661     *
662     * @return string
663     */
664    public function getRssSort($sort)
665    {
666        if (empty($this->rssSort)) {
667            return $sort;
668        }
669        if ($sort == 'relevance') {
670            return $this->rssSort;
671        }
672        return $this->rssSort . ',' . $sort;
673    }
674
675    /**
676     * Get default view setting.
677     *
678     * @return int
679     */
680    public function getDefaultView()
681    {
682        return $this->defaultView;
683    }
684
685    /**
686     * Get an array of view options.
687     *
688     * @return array
689     */
690    public function getViewOptions()
691    {
692        return $this->viewOptions;
693    }
694
695    /**
696     * Returns the defaultFacetDelimiter value.
697     *
698     * @return string
699     */
700    public function getDefaultFacetDelimiter()
701    {
702        return $this->defaultFacetDelimiter;
703    }
704
705    /**
706     * Set the defaultFacetDelimiter value.
707     *
708     * @param string $defaultFacetDelimiter A default delimiter to be used with
709     * delimited facets
710     *
711     * @return void
712     */
713    public function setDefaultFacetDelimiter($defaultFacetDelimiter)
714    {
715        $this->defaultFacetDelimiter = $defaultFacetDelimiter;
716        $this->processedDelimitedFacets = null; // clear processed value cache
717    }
718
719    /**
720     * Get a list of delimited facets
721     *
722     * @param bool $processed False = return raw values; true = process values into
723     * field => delimiter associative array.
724     *
725     * @return array
726     */
727    public function getDelimitedFacets($processed = false)
728    {
729        if (!$processed) {
730            return $this->delimitedFacets;
731        }
732        if (null === $this->processedDelimitedFacets) {
733            $this->processedDelimitedFacets = [];
734            $defaultDelimiter = $this->getDefaultFacetDelimiter();
735            foreach ($this->delimitedFacets as $current) {
736                $parts = explode('|', $current, 2);
737                if (count($parts) == 2) {
738                    $this->processedDelimitedFacets[$parts[0]] = $parts[1];
739                } else {
740                    $this->processedDelimitedFacets[$parts[0]] = $defaultDelimiter;
741                }
742            }
743        }
744        return $this->processedDelimitedFacets;
745    }
746
747    /**
748     * Set the delimitedFacets value.
749     *
750     * @param array $delimitedFacets An array of delimited facet names
751     *
752     * @return void
753     */
754    public function setDelimitedFacets($delimitedFacets)
755    {
756        $this->delimitedFacets = $delimitedFacets;
757        $this->processedDelimitedFacets = null; // clear processed value cache
758    }
759
760    /**
761     * Get a list of facets that are subject to translation.
762     *
763     * @return array
764     */
765    public function getTranslatedFacets()
766    {
767        return $this->translatedFacets;
768    }
769
770    /**
771     * Configure facet translation using an array of field names with optional
772     * colon-separated text domains.
773     *
774     * @param array $facets Incoming configuration.
775     *
776     * @return void
777     */
778    public function setTranslatedFacets($facets)
779    {
780        // Reset properties:
781        $this->translatedFacets = $this->translatedFacetsTextDomains
782            = $this->translatedFacetsFormats = [];
783
784        // Fill in new data:
785        foreach ($facets as $current) {
786            $parts = explode(':', $current);
787            $this->translatedFacets[] = $parts[0];
788            if (isset($parts[1])) {
789                $this->translatedFacetsTextDomains[$parts[0]] = $parts[1];
790            }
791            if (isset($parts[2])) {
792                $this->translatedFacetsFormats[$parts[0]] = $parts[2];
793            }
794        }
795    }
796
797    /**
798     * Look up the text domain for use when translating a particular facet
799     * field.
800     *
801     * @param string $field Field name being translated
802     *
803     * @return string
804     */
805    public function getTextDomainForTranslatedFacet($field)
806    {
807        return $this->translatedFacetsTextDomains[$field] ?? 'default';
808    }
809
810    /**
811     * Look up the format for use when translating a particular facet
812     * field.
813     *
814     * @param string $field Field name being translated
815     *
816     * @return string
817     */
818    public function getFormatForTranslatedFacet($field)
819    {
820        return $this->translatedFacetsFormats[$field] ?? null;
821    }
822
823    /**
824     * Get hierarchical facet fields.
825     *
826     * @return array
827     */
828    public function getHierarchicalFacets()
829    {
830        return $this->hierarchicalFacets;
831    }
832
833    /**
834     * Get hierarchical facet separators.
835     *
836     * @return array
837     */
838    public function getHierarchicalFacetSeparators()
839    {
840        return $this->hierarchicalFacetSeparators;
841    }
842
843    /**
844     * Get hierarchical facet sort settings.
845     *
846     * @return array
847     */
848    public function getHierarchicalFacetSortSettings()
849    {
850        return $this->hierarchicalFacetSortSettings;
851    }
852
853    /**
854     * Get current spellcheck setting and (optionally) change it.
855     *
856     * @param bool $bool True to enable, false to disable, null to leave alone
857     *
858     * @return bool
859     */
860    public function spellcheckEnabled($bool = null)
861    {
862        if (null !== $bool) {
863            $this->spellcheck = $bool;
864        }
865        return $this->spellcheck;
866    }
867
868    /**
869     * Is highlighting enabled?
870     *
871     * @return bool
872     */
873    public function highlightEnabled()
874    {
875        return $this->highlight;
876    }
877
878    /**
879     * Translate a field name to a displayable string for rendering a query in
880     * human-readable format:
881     *
882     * @param string $field Field name to display.
883     *
884     * @return string       Human-readable version of field name.
885     */
886    public function getHumanReadableFieldName($field)
887    {
888        if (isset($this->basicHandlers[$field])) {
889            return $this->translate($this->basicHandlers[$field]);
890        } elseif (isset($this->advancedHandlers[$field])) {
891            return $this->translate($this->advancedHandlers[$field]);
892        } else {
893            return $field;
894        }
895    }
896
897    /**
898     * Turn off highlighting.
899     *
900     * @return void
901     */
902    public function disableHighlighting()
903    {
904        $this->highlight = false;
905    }
906
907    /**
908     * Is autocomplete enabled?
909     *
910     * @return bool
911     */
912    public function autocompleteEnabled()
913    {
914        return $this->autocompleteEnabled;
915    }
916
917    /**
918     * Should autocomplete auto submit?
919     *
920     * @return bool
921     */
922    public function autocompleteAutoSubmit()
923    {
924        return $this->autocompleteAutoSubmit;
925    }
926
927    /**
928     * Get autocomplete query formatting rules.
929     *
930     * @return array
931     */
932    public function getAutocompleteFormattingRules(): array
933    {
934        return $this->autocompleteFormattingRules;
935    }
936
937    /**
938     * Get a string of the listviewOption (full or tab).
939     *
940     * @return string
941     */
942    public function getListViewOption()
943    {
944        return $this->listviewOption;
945    }
946
947    /**
948     * Return the route name for the search results action.
949     *
950     * @return string
951     */
952    abstract public function getSearchAction();
953
954    /**
955     * Return the route name for the search home action.
956     *
957     * @return string
958     */
959    public function getSearchHomeAction()
960    {
961        // Assume the home action is the same as the search action, only with
962        // a "-home" suffix in place of the search action.
963        $basicSearch = $this->getSearchAction();
964        return substr($basicSearch, 0, strpos($basicSearch, '-')) . '-home';
965    }
966
967    /**
968     * Return the route name of the action used for performing advanced searches.
969     * Returns false if the feature is not supported.
970     *
971     * @return string|bool
972     */
973    public function getAdvancedSearchAction()
974    {
975        // Assume unsupported by default:
976        return false;
977    }
978
979    /**
980     * Return the route name for the facet list action. Returns false to cover
981     * unimplemented support.
982     *
983     * @return string|bool
984     */
985    public function getFacetListAction()
986    {
987        return false;
988    }
989
990    /**
991     * Return the route name for the versions search action. Returns false to cover
992     * unimplemented support.
993     *
994     * @return string|bool
995     */
996    public function getVersionsAction()
997    {
998        return false;
999    }
1000
1001    /**
1002     * Return the route name for the "cites" search action. Returns false to cover
1003     * unimplemented support.
1004     *
1005     * @return string|bool
1006     */
1007    public function getCitesAction()
1008    {
1009        return false;
1010    }
1011
1012    /**
1013     * Return the route name for the "cited by" search action. Returns false to cover
1014     * unimplemented support.
1015     *
1016     * @return string|bool
1017     */
1018    public function getCitedByAction()
1019    {
1020        return false;
1021    }
1022
1023    /**
1024     * Does this search option support the cart/book bag?
1025     *
1026     * @return bool
1027     */
1028    public function supportsCart()
1029    {
1030        // Assume true by default.
1031        return true;
1032    }
1033
1034    /**
1035     * Get default filters to apply to an empty search.
1036     *
1037     * @return array
1038     */
1039    public function getDefaultFilters()
1040    {
1041        return $this->defaultFilters;
1042    }
1043
1044    /**
1045     * Should filter settings be retained across searches by default?
1046     *
1047     * @return bool
1048     */
1049    public function getRetainFilterSetting()
1050    {
1051        return $this->retainFiltersByDefault;
1052    }
1053
1054    /**
1055     * Should the "Reset Filters" button be displayed?
1056     *
1057     * @return bool
1058     */
1059    public function shouldDisplayResetFilters()
1060    {
1061        return $this->alwaysDisplayResetFilters || $this->getRetainFilterSetting();
1062    }
1063
1064    /**
1065     * Get an associative array of available shards (key = internal VuFind ID for
1066     * this shard; value = details needed to connect to shard; empty for non-sharded
1067     * data sources).
1068     *
1069     * Although this mechanism was originally designed for Solr's sharding
1070     * capabilities, it could also be useful for multi-database search situations
1071     * (i.e. federated search, EBSCO's API, etc., etc.).
1072     *
1073     * @return array
1074     */
1075    public function getShards()
1076    {
1077        return $this->shards;
1078    }
1079
1080    /**
1081     * Get an array of default selected shards (values correspond with keys returned
1082     * by getShards().
1083     *
1084     * @return array
1085     */
1086    public function getDefaultSelectedShards()
1087    {
1088        return $this->defaultSelectedShards;
1089    }
1090
1091    /**
1092     * Should we display shard checkboxes for this object?
1093     *
1094     * @return bool
1095     */
1096    public function showShardCheckboxes()
1097    {
1098        return $this->visibleShardCheckboxes;
1099    }
1100
1101    /**
1102     * If there is a limit to how many search results a user can access, this
1103     * method will return that limit. If there is no limit, this will return -1.
1104     *
1105     * @return int
1106     */
1107    public function getVisibleSearchResultLimit()
1108    {
1109        return intval($this->resultLimit);
1110    }
1111
1112    /**
1113     * Load all API-related settings from the relevant ini file(s).
1114     *
1115     * @return array
1116     */
1117    public function getAPISettings()
1118    {
1119        // Inherit defaults from searches.ini (if that is not already the
1120        // configured search settings file):
1121        $defaultConfig = $this->configLoader->get('searches')->API;
1122        $defaultSettings = $defaultConfig ? $defaultConfig->toArray() : [];
1123        $localIni = $this->getSearchIni();
1124        $localConfig = ($localIni !== 'searches')
1125            ? $this->configLoader->get($localIni)->API : null;
1126        $localSettings = $localConfig ? $localConfig->toArray() : [];
1127        return array_merge($defaultSettings, $localSettings);
1128    }
1129
1130    /**
1131     * Load all recommendation settings from the relevant ini file. Returns an
1132     * associative array where the key is the location of the recommendations (top
1133     * or side) and the value is the settings found in the file (which may be either
1134     * a single string or an array of strings).
1135     *
1136     * @param string $handler Name of handler for which to load specific settings.
1137     *
1138     * @return array associative: location (top/side/etc.) => search settings
1139     */
1140    public function getRecommendationSettings($handler = null)
1141    {
1142        // Load the necessary settings to determine the appropriate recommendations
1143        // module:
1144        $searchSettings = $this->configLoader->get($this->getSearchIni());
1145
1146        // Load a type-specific recommendations setting if possible, or the default
1147        // otherwise:
1148        $recommend = [];
1149
1150        if (
1151            null !== $handler
1152            && isset($searchSettings->TopRecommendations->$handler)
1153        ) {
1154            $recommend['top'] = $searchSettings->TopRecommendations
1155                ->$handler->toArray();
1156        } else {
1157            $recommend['top']
1158                = isset($searchSettings->General->default_top_recommend)
1159                ? $searchSettings->General->default_top_recommend->toArray()
1160                : false;
1161        }
1162        if (
1163            null !== $handler
1164            && isset($searchSettings->SideRecommendations->$handler)
1165        ) {
1166            $recommend['side'] = $searchSettings->SideRecommendations
1167                ->$handler->toArray();
1168        } else {
1169            $recommend['side']
1170                = isset($searchSettings->General->default_side_recommend)
1171                ? $searchSettings->General->default_side_recommend->toArray()
1172                : false;
1173        }
1174        if (
1175            null !== $handler
1176            && isset($searchSettings->NoResultsRecommendations->$handler)
1177        ) {
1178            $recommend['noresults'] = $searchSettings->NoResultsRecommendations
1179                ->$handler->toArray();
1180        } else {
1181            $recommend['noresults']
1182                = isset($searchSettings->General->default_noresults_recommend)
1183                ? $searchSettings->General->default_noresults_recommend
1184                    ->toArray()
1185                : false;
1186        }
1187
1188        return $recommend;
1189    }
1190
1191    /**
1192     * Get the identifier used for naming the various search classes in this family.
1193     *
1194     * @return string
1195     */
1196    public function getSearchClassId()
1197    {
1198        // Parse identifier out of class name of format VuFind\Search\[id]\Options:
1199        $className = get_class($this);
1200        $class = explode('\\', $className);
1201
1202        // Special case: if there's an unexpected number of parts, we may be testing
1203        // with a mock object; if so, that's okay, but anything else is unexpected.
1204        if (count($class) !== 4) {
1205            if (str_starts_with($className, 'Mock_') || str_starts_with($className, 'MockObject_')) {
1206                return 'Mock';
1207            }
1208            throw new \Exception("Unexpected class name: {$className}");
1209        }
1210
1211        return $class[2];
1212    }
1213
1214    /**
1215     * Get the search class ID for identifying search box options; this is normally
1216     * the same as the current search class ID, but some "special purpose" search
1217     * namespaces (e.g. SolrAuthor) need to point to a different ID for search box
1218     * generation
1219     *
1220     * @return string
1221     */
1222    public function getSearchBoxSearchClassId(): string
1223    {
1224        return $this->getSearchClassId();
1225    }
1226
1227    /**
1228     * Should we include first/last options in record page navigation?
1229     *
1230     * @return bool
1231     *
1232     * @deprecated Use recordFirstLastNavigationEnabled instead
1233     */
1234    public function supportsFirstLastNavigation()
1235    {
1236        return $this->recordFirstLastNavigationEnabled();
1237    }
1238
1239    /**
1240     * Is first/last navigation supported by the backend
1241     *
1242     * @return bool
1243     */
1244    public function firstLastNavigationSupported()
1245    {
1246        return $this->firstLastNavigationSupported;
1247    }
1248
1249    /**
1250     * Should we include first/last options in record page navigation?
1251     *
1252     * @return bool
1253     */
1254    public function recordFirstLastNavigationEnabled()
1255    {
1256        return $this->firstLastNavigationSupported() && $this->recordPageFirstLastNavigation;
1257    }
1258
1259    /**
1260     * Does this search backend support scheduled searching?
1261     *
1262     * @return bool
1263     */
1264    public function supportsScheduledSearch()
1265    {
1266        // Unsupported by default!
1267        return false;
1268    }
1269
1270    /**
1271     * Should we load results with JavaScript?
1272     *
1273     * @return bool
1274     */
1275    public function loadResultsWithJsEnabled(): bool
1276    {
1277        return $this->loadResultsWithJs;
1278    }
1279
1280    /**
1281     * Get top paginator style
1282     *
1283     * @return string
1284     */
1285    public function getTopPaginatorStyle(): string
1286    {
1287        return $this->topPaginatorStyle;
1288    }
1289
1290    /**
1291     * Return the callback used for normalization within this backend.
1292     *
1293     * @return callable
1294     */
1295    public function getSpellingNormalizer()
1296    {
1297        return new \VuFind\Normalizer\DefaultSpellingNormalizer();
1298    }
1299
1300    /**
1301     * Should we display citation search links in results?
1302     *
1303     * @return bool
1304     */
1305    public function displayCitationLinksInResults(): bool
1306    {
1307        return $this->displayCitationLinksInResults;
1308    }
1309
1310    /**
1311     * Configure autocomplete preferences from an .ini file.
1312     *
1313     * @param Config $searchSettings Object representation of .ini file
1314     *
1315     * @return void
1316     */
1317    protected function configureAutocomplete(Config $searchSettings = null)
1318    {
1319        // Only change settings from current values if they are defined in .ini:
1320        $this->autocompleteEnabled = $searchSettings->Autocomplete->enabled
1321            ?? $this->autocompleteEnabled;
1322        $this->autocompleteAutoSubmit = $searchSettings->Autocomplete->auto_submit
1323            ?? $this->autocompleteAutoSubmit;
1324        $formattingRules = $searchSettings->Autocomplete->formatting_rule ?? [];
1325        if (!is_string($formattingRules) && count($formattingRules) > 0) {
1326            $this->autocompleteFormattingRules = $formattingRules->toArray();
1327        }
1328    }
1329
1330    /**
1331     * Get advanced search limits that override the natural sorting to
1332     * display at the top.
1333     *
1334     * @param string $limit advanced search limit
1335     *
1336     * @return array
1337     */
1338    public function limitOrderOverride($limit)
1339    {
1340        $facetSettings = $this->configLoader->get($this->getFacetsIni());
1341        $limits = $facetSettings->Advanced_Settings->limitOrderOverride ?? null;
1342        $delimiter = $facetSettings->Advanced_Settings->limitDelimiter ?? '::';
1343        $limitConf = $limits ? $limits->get($limit) : '';
1344        return array_map('trim', explode($delimiter, $limitConf ?? ''));
1345    }
1346
1347    /**
1348     * Are hierarchicalFacetFilters and hierarchicalExcludeFilters enabled in advanced search?
1349     *
1350     * @return bool
1351     */
1352    public function getFilterHierarchicalFacetsInAdvanced(): bool
1353    {
1354        return $this->filterHierarchicalFacetsInAdvanced;
1355    }
1356
1357    /**
1358     * Get hierarchical exclude filters.
1359     *
1360     * @param string|null $field Field to get or null for all values.
1361     *                           Default is null.
1362     *
1363     * @return array
1364     */
1365    public function getHierarchicalExcludeFilters(?string $field = null): array
1366    {
1367        if ($field) {
1368            return $this->hierarchicalExcludeFilters[$field] ?? [];
1369        }
1370        return $this->hierarchicalExcludeFilters;
1371    }
1372
1373    /**
1374     * Get hierarchical facet filters.
1375     *
1376     * @param string|null $field Field to get or null for all values.
1377     *                           Default is null.
1378     *
1379     * @return array
1380     */
1381    public function getHierarchicalFacetFilters(?string $field = null): array
1382    {
1383        if ($field) {
1384            return $this->hierarchicalFacetFilters[$field] ?? [];
1385        }
1386        return $this->hierarchicalFacetFilters;
1387    }
1388}