Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
7.63% covered (danger)
7.63%
9 / 118
14.29% covered (danger)
14.29%
2 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Params
7.63% covered (danger)
7.63%
9 / 118
14.29% covered (danger)
14.29%
2 / 14
1788.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addFacet
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 resetFacetConfig
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getFullFacetSettings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDateFacetSettings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFacetLabel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getCheckboxFacets
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getBackendParameters
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 getBackendFacetParameters
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 createBackendFilterParameters
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
156
 formatFilterListEntry
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 initFacetList
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 initAdvancedFacets
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 initHomePageFacets
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * Summon Search Parameters
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2011.
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_Summon
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Page
28 */
29
30namespace VuFind\Search\Summon;
31
32use SerialsSolutions_Summon_Query as SummonQuery;
33use VuFind\Solr\Utils as SolrUtils;
34use VuFindSearch\ParamBag;
35
36/**
37 * Summon Search Parameters
38 *
39 * @category VuFind
40 * @package  Search_Summon
41 * @author   Demian Katz <demian.katz@villanova.edu>
42 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
43 * @link     https://vufind.org Main Page
44 */
45class Params extends \VuFind\Search\Base\Params
46{
47    use \VuFind\Search\Params\FacetLimitTrait;
48
49    /**
50     * Settings for all the facets
51     *
52     * @var array
53     */
54    protected $fullFacetSettings = [];
55
56    /**
57     * Settings for the date facet only
58     *
59     * @var array
60     */
61    protected $dateFacetSettings = [];
62
63    /**
64     * Config sections to search for facet labels if no override configuration
65     * is set.
66     *
67     * @var array
68     */
69    protected $defaultFacetLabelSections
70        = ['Advanced_Facets', 'HomePage_Facets', 'FacetsTop', 'Facets'];
71
72    /**
73     * Config sections to search for checkbox facet labels if no override
74     * configuration is set.
75     *
76     * @var array
77     */
78    protected $defaultFacetLabelCheckboxSections = ['CheckboxFacets'];
79
80    /**
81     * Constructor
82     *
83     * @param \VuFind\Search\Base\Options  $options      Options to use
84     * @param \VuFind\Config\PluginManager $configLoader Config loader
85     */
86    public function __construct($options, \VuFind\Config\PluginManager $configLoader)
87    {
88        parent::__construct($options, $configLoader);
89        $config = $configLoader->get($options->getFacetsIni());
90        $this->initFacetLimitsFromConfig($config->Facet_Settings ?? null);
91    }
92
93    /**
94     * Add a field to facet on.
95     *
96     * @param string $newField Field name
97     * @param string $newAlias Optional on-screen display label
98     * @param bool   $ored     Should we treat this as an ORed facet?
99     *
100     * @return void
101     */
102    public function addFacet($newField, $newAlias = null, $ored = false)
103    {
104        // Save the full field name (which may include extra parameters);
105        // we'll need these to do the proper search using the Summon class:
106        if (strstr($newField, 'PublicationDate')) {
107            // Special case -- we don't need to send this to the Summon API,
108            // but we do need to set a flag so VuFind knows to display the
109            // date facet control.
110            $this->dateFacetSettings[] = 'PublicationDate';
111        } else {
112            $this->fullFacetSettings[] = $newField;
113        }
114
115        // Field name may have parameters attached -- remove them:
116        $parts = explode(',', $newField);
117        parent::addFacet($parts[0], $newAlias, $ored);
118    }
119
120    /**
121     * Reset the current facet configuration.
122     *
123     * @return void
124     */
125    public function resetFacetConfig()
126    {
127        parent::resetFacetConfig();
128        $this->dateFacetSettings = [];
129        $this->fullFacetSettings = [];
130    }
131
132    /**
133     * Get the full facet settings stored by addFacet -- these may include extra
134     * parameters needed by the search results class.
135     *
136     * @return array
137     */
138    public function getFullFacetSettings()
139    {
140        return $this->fullFacetSettings;
141    }
142
143    /**
144     * Get the date facet settings stored by addFacet.
145     *
146     * @return array
147     */
148    public function getDateFacetSettings()
149    {
150        return $this->dateFacetSettings;
151    }
152
153    /**
154     * Get a user-friendly string to describe the provided facet field.
155     *
156     * @param string $field   Facet field name.
157     * @param string $value   Facet value.
158     * @param string $default Default field name (null for default behavior).
159     *
160     * @return string         Human-readable description of field.
161     */
162    public function getFacetLabel($field, $value = null, $default = null)
163    {
164        // The default use of "Other" for undefined facets doesn't work well with
165        // checkbox facets -- we'll use field names as the default within the Summon
166        // search object.
167        return parent::getFacetLabel($field, $value, $default ?: $field);
168    }
169
170    /**
171     * Get information on the current state of the boolean checkbox facets.
172     *
173     * @param array $include        List of checkbox filters to return (null for all)
174     * @param bool  $includeDynamic Should we include dynamically-generated
175     * checkboxes that are not part of the include list above?
176     *
177     * @return array
178     */
179    public function getCheckboxFacets(
180        array $include = null,
181        bool $includeDynamic = true
182    ) {
183        // Grab checkbox facet details using the standard method:
184        $facets = parent::getCheckboxFacets($include, $includeDynamic);
185
186        // Special case -- if we have a "holdings only" or "expand query" facet,
187        // we want this to always appear, even on the "no results" screen, since
188        // setting this facet actually EXPANDS rather than reduces the result set.
189        foreach ($facets as $i => $facet) {
190            [$field] = explode(':', $facet['filter']);
191            if ($field == 'holdingsOnly' || $field == 'queryExpansion') {
192                $facets[$i]['alwaysVisible'] = true;
193            }
194        }
195
196        // Return modified list:
197        return $facets;
198    }
199
200    /**
201     * Create search backend parameters for advanced features.
202     *
203     * @return ParamBag
204     */
205    public function getBackendParameters()
206    {
207        $backendParams = new ParamBag();
208
209        $options = $this->getOptions();
210
211        $sort = $this->getSort();
212        if ($sort) {
213            // If we have an empty search with relevance sort, see if there is
214            // an override configured:
215            if (
216                $sort == 'relevance' && $this->getQuery()->getAllTerms() == ''
217                && ($relOv = $this->getOptions()->getEmptySearchRelevanceOverride())
218            ) {
219                $sort = $relOv;
220            }
221        }
222
223        // The "relevance" sort option is a VuFind reserved word; we need to make
224        // this null in order to achieve the desired effect with Summon:
225        $finalSort = ($sort == 'relevance') ? null : $sort;
226        $backendParams->set('sort', $finalSort);
227
228        $backendParams->set('didYouMean', $options->spellcheckEnabled());
229
230        // Get the language setting:
231        $lang = $this->getOptions()->getTranslatorLocale();
232        $backendParams->set('language', substr($lang, 0, 2));
233
234        if ($options->highlightEnabled()) {
235            $backendParams->set('highlight', true);
236            $backendParams->set('highlightStart', '{{{{START_HILITE}}}}');
237            $backendParams->set('highlightEnd', '{{{{END_HILITE}}}}');
238        }
239        if ($maxTopics = $options->getMaxTopicRecommendations()) {
240            $backendParams->set('maxTopics', $maxTopics);
241        }
242        $backendParams->set('facets', $this->getBackendFacetParameters());
243        $this->createBackendFilterParameters($backendParams);
244
245        return $backendParams;
246    }
247
248    /**
249     * Set up facets based on VuFind settings.
250     *
251     * @return array
252     */
253    protected function getBackendFacetParameters()
254    {
255        $finalFacets = [];
256        foreach ($this->getFullFacetSettings() as $facet) {
257            // See if parameters are included as part of the facet name;
258            // if not, override them with defaults.
259            $parts = explode(',', $facet);
260            $facetName = $parts[0];
261            $defaultMode = ($this->getFacetOperator($facet) == 'OR') ? 'or' : 'and';
262            $facetMode = $parts[1] ?? $defaultMode;
263            $facetPage = $parts[2] ?? 1;
264            $facetLimit = $parts[3] ?? $this->getFacetLimitForField($facetName);
265            $facetParams = "{$facetMode},{$facetPage},{$facetLimit}";
266            $finalFacets[] = "{$facetName},{$facetParams}";
267        }
268        return $finalFacets;
269    }
270
271    /**
272     * Set up filters based on VuFind settings.
273     *
274     * @param ParamBag $params Parameter collection to update
275     *
276     * @return void
277     */
278    public function createBackendFilterParameters(ParamBag $params)
279    {
280        // Which filters should be applied to our query?
281        $filterList = $this->getFilterList();
282        if (!empty($filterList)) {
283            $orFacets = [];
284
285            // Loop through all filters and add appropriate values to request:
286            foreach ($filterList as $filterArray) {
287                foreach ($filterArray as $filt) {
288                    $safeValue = SummonQuery::escapeParam($filt['value']);
289                    // Special case -- "holdings only" is a separate parameter from
290                    // other facets.
291                    if ($filt['field'] == 'holdingsOnly') {
292                        $params->set(
293                            'holdings',
294                            strtolower(trim($safeValue)) == 'true'
295                        );
296                    } elseif ($filt['field'] == 'queryExpansion') {
297                        // Special case -- "query expansion" is a separate parameter
298                        // from other facets.
299                        $params->set(
300                            'expand',
301                            strtolower(trim($safeValue)) == 'true'
302                        );
303                    } elseif ($filt['field'] == 'openAccessFilter') {
304                        // Special case -- "open access filter" is a separate
305                        // parameter from other facets.
306                        $params->set(
307                            'openAccessFilter',
308                            strtolower(trim($safeValue)) == 'true'
309                        );
310                    } elseif ($filt['field'] == 'excludeNewspapers') {
311                        // Special case -- support a checkbox for excluding
312                        // newspapers:
313                        $params
314                            ->add('filters', 'ContentType,Newspaper Article,true');
315                    } elseif ($range = SolrUtils::parseRange($filt['value'])) {
316                        // Special case -- range query (translate [x TO y] syntax):
317                        $from = SummonQuery::escapeParam($range['from']);
318                        $to = SummonQuery::escapeParam($range['to']);
319                        $params
320                            ->add('rangeFilters', "{$filt['field']},{$from}:{$to}");
321                    } elseif ($filt['operator'] == 'OR') {
322                        // Special case -- OR facets:
323                        $orFacets[$filt['field']] ??= [];
324                        $orFacets[$filt['field']][] = $safeValue;
325                    } else {
326                        // Standard case:
327                        $fq = "{$filt['field']},{$safeValue}";
328                        if ($filt['operator'] == 'NOT') {
329                            $fq .= ',true';
330                        }
331                        $params->add('filters', $fq);
332                    }
333                }
334
335                // Deal with OR facets:
336                foreach ($orFacets as $field => $values) {
337                    $params->add(
338                        'groupFilters',
339                        $field . ',or,' . implode(',', $values)
340                    );
341                }
342            }
343        }
344    }
345
346    /**
347     * Format a single filter for use in getFilterList().
348     *
349     * @param string $field     Field name
350     * @param string $value     Field value
351     * @param string $operator  Operator (AND/OR/NOT)
352     * @param bool   $translate Should we translate the label?
353     *
354     * @return array
355     */
356    protected function formatFilterListEntry($field, $value, $operator, $translate)
357    {
358        $filter = parent::formatFilterListEntry(
359            $field,
360            $value,
361            $operator,
362            $translate
363        );
364
365        // Convert range queries to a language-non-specific format:
366        $caseInsensitiveRegex = '/^\(\[(.*) TO (.*)\] OR \[(.*) TO (.*)\]\)$/';
367        if (preg_match('/^\[(.*) TO (.*)\]$/', $value, $matches)) {
368            // Simple case: [X TO Y]
369            $filter['displayText'] = $matches[1] . '-' . $matches[2];
370        } elseif (preg_match($caseInsensitiveRegex, $value, $matches)) {
371            // Case insensitive case: [x TO y] OR [X TO Y]; convert
372            // only if values in both ranges match up!
373            if (
374                strtolower($matches[3]) == strtolower($matches[1])
375                && strtolower($matches[4]) == strtolower($matches[2])
376            ) {
377                $filter['displayText'] = $matches[1] . '-' . $matches[2];
378            }
379        }
380
381        return $filter;
382    }
383
384    /**
385     * Initialize facet settings for the specified configuration sections.
386     *
387     * @param string $facetList     Config section containing fields to activate
388     * @param string $facetSettings Config section containing related settings
389     * @param string $cfgFile       Name of configuration to load (null to load
390     * default facets configuration).
391     *
392     * @return bool                 True if facets set, false if no settings found
393     */
394    protected function initFacetList($facetList, $facetSettings, $cfgFile = null)
395    {
396        $config = $this->configLoader
397            ->get($cfgFile ?? $this->getOptions()->getFacetsIni());
398        // Special case -- when most settings are in Results_Settings, the limits
399        // can be found in Facet_Settings.
400        $limitSection = ($facetSettings === 'Results_Settings')
401            ? 'Facet_Settings' : $facetSettings;
402        $this->initFacetLimitsFromConfig($config->$limitSection ?? null);
403        return parent::initFacetList($facetList, $facetSettings, $cfgFile);
404    }
405
406    /**
407     * Initialize facet settings for the advanced search screen.
408     *
409     * @return void
410     */
411    public function initAdvancedFacets()
412    {
413        // If no configuration was found, set up defaults instead:
414        if (!$this->initFacetList('Advanced_Facets', 'Advanced_Facet_Settings')) {
415            $defaults = ['Language' => 'Language', 'ContentType' => 'Format'];
416            foreach ($defaults as $key => $value) {
417                $this->addFacet($key, $value);
418            }
419        }
420    }
421
422    /**
423     * Initialize facet settings for the home page.
424     *
425     * @return void
426     */
427    public function initHomePageFacets()
428    {
429        // Load Advanced settings if HomePage settings are missing (legacy support):
430        if (!$this->initFacetList('HomePage_Facets', 'HomePage_Facet_Settings')) {
431            $this->initAdvancedFacets();
432        }
433    }
434}