Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 166
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchBox
0.00% covered (danger)
0.00%
0 / 166
0.00% covered (danger)
0.00%
0 / 16
4290
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 autocompleteEnabled
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 autocompleteAutoSubmit
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 autocompleteFormattingRulesJson
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 alphaBrowseOptionsEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 combinedHandlersActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOperatorCharacter
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFilterDetails
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
110
 getPlaceholderText
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getKeyboardLayouts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHandlers
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getFilterCount
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getBasicHandlers
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getCombinedHandlerConfig
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 getAlphabrowseHandlers
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 getCombinedHandlers
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
380
1<?php
2
3/**
4 * Search box view helper
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  View_Helpers
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/wiki/development Wiki
28 */
29
30namespace VuFind\View\Helper\Root;
31
32use VuFind\Search\Options\PluginManager as OptionsManager;
33
34use function count;
35use function in_array;
36use function is_array;
37
38/**
39 * Search box view helper
40 *
41 * @category VuFind
42 * @package  View_Helpers
43 * @author   Demian Katz <demian.katz@villanova.edu>
44 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
45 * @link     https://vufind.org/wiki/development Wiki
46 */
47class SearchBox extends \Laminas\View\Helper\AbstractHelper
48{
49    /**
50     * Configuration for search box.
51     *
52     * @var array
53     */
54    protected $config;
55
56    /**
57     * Alphabrowse settings for search box.
58     *
59     * @var array
60     */
61    protected $alphabrowseConfig;
62
63    /**
64     * Placeholders from config.ini
65     *
66     * @var array
67     */
68    protected $placeholders;
69
70    /**
71     * Search options plugin manager
72     *
73     * @var OptionsManager
74     */
75    protected $optionsManager;
76
77    /**
78     * Cache for configurations
79     *
80     * @var array
81     */
82    protected $cachedConfigs = [];
83
84    /**
85     * Constructor
86     *
87     * @param OptionsManager $optionsManager    Search options plugin manager
88     * @param array          $config            Configuration for search box
89     * @param array          $placeholders      Array of placeholders keyed by
90     * backend
91     * @param array          $alphabrowseConfig source => label config for
92     * alphabrowse options to display in combined box (empty for none)
93     */
94    public function __construct(
95        OptionsManager $optionsManager,
96        $config = [],
97        $placeholders = [],
98        $alphabrowseConfig = []
99    ) {
100        $this->optionsManager = $optionsManager;
101        $this->config = $config;
102        $this->alphabrowseConfig = $alphabrowseConfig;
103        $this->placeholders = $placeholders;
104    }
105
106    /**
107     * Is autocomplete enabled for the current context?
108     *
109     * @param string $activeSearchClass Active search class ID
110     *
111     * @return bool
112     */
113    public function autocompleteEnabled($activeSearchClass)
114    {
115        // Simple case -- no combined handlers:
116        if (!$this->combinedHandlersActive()) {
117            $options = $this->optionsManager->get($activeSearchClass);
118            return $options->autocompleteEnabled();
119        }
120
121        // Complex case -- combined handlers:
122        $settings = $this->getCombinedHandlerConfig($activeSearchClass);
123        $typeCount = count($settings['type']);
124        for ($i = 0; $i < $typeCount; $i++) {
125            $type = $settings['type'][$i];
126            $target = $settings['target'][$i];
127
128            if ($type == 'VuFind') {
129                $options = $this->optionsManager->get($target);
130                if ($options->autocompleteEnabled()) {
131                    return true;
132                }
133            }
134        }
135        return false;
136    }
137
138    /**
139     * Is autocomplete enabled for the current context?
140     *
141     * @param string $activeSearchClass Active search class ID
142     *
143     * @return bool
144     */
145    public function autocompleteAutoSubmit($activeSearchClass)
146    {
147        $options = $this->optionsManager->get($activeSearchClass);
148        return $options->autocompleteAutoSubmit();
149    }
150
151    /**
152     * Get JSON-encoded configuration for autocomplete query formatting.
153     *
154     * @param string $activeSearchClass Active search class ID
155     *
156     * @return string
157     */
158    public function autocompleteFormattingRulesJson($activeSearchClass): string
159    {
160        if ($this->combinedHandlersActive()) {
161            $rules = [];
162            $settings = $this->getCombinedHandlerConfig($activeSearchClass);
163            foreach ($settings['target'] ?? [] as $i => $target) {
164                if (($settings['type'][$i] ?? null) === 'VuFind') {
165                    $options = $this->optionsManager->get($target);
166                    $handlerRules = $options->getAutocompleteFormattingRules();
167                    foreach ($handlerRules as $key => $val) {
168                        $rules["VuFind:$target|$key"] = $val;
169                    }
170                }
171            }
172        } else {
173            $options = $this->optionsManager->get($activeSearchClass);
174            $rules = $options->getAutocompleteFormattingRules();
175        }
176        return json_encode($rules);
177    }
178
179    /**
180     * Are alphabrowse options configured to display in the search options
181     * drop-down?
182     *
183     * @return bool
184     */
185    public function alphaBrowseOptionsEnabled()
186    {
187        // Alphabrowse options depend on combined handlers:
188        return $this->combinedHandlersActive() && !empty($this->alphabrowseConfig);
189    }
190
191    /**
192     * Are combined handlers enabled?
193     *
194     * @return bool
195     */
196    public function combinedHandlersActive()
197    {
198        return $this->config['General']['combinedHandlers'] ?? false;
199    }
200
201    /**
202     * Helper method: get special character to represent operator in filter
203     *
204     * @param string $operator Operator
205     *
206     * @return string
207     */
208    protected function getOperatorCharacter($operator)
209    {
210        static $map = ['NOT' => '-', 'OR' => '~'];
211        return $map[$operator] ?? '';
212    }
213
214    /**
215     * Get an array of filter information for use by the "retain filters" feature
216     * of the search box. Returns an array of arrays with 'id' and 'value' keys used
217     * for generating hidden checkboxes.
218     *
219     * @param array $filterList      Standard filter information
220     * @param array $checkboxFilters Checkbox filter information
221     *
222     * @return array
223     */
224    public function getFilterDetails($filterList, $checkboxFilters)
225    {
226        $results = [];
227        foreach ($filterList as $field => $data) {
228            foreach ($data as $value) {
229                $results[] = is_array($value)
230                    ? $this->getOperatorCharacter($value['operator'] ?? '')
231                    . $value['field'] . ':"' . $value['value'] . '"'
232                    : "$field:\"$value\"";
233            }
234        }
235        foreach ($checkboxFilters as $current) {
236            // Check a normalized version of the checkbox facet against the existing
237            // filter list to avoid unnecessary duplication. Note that we don't
238            // actually use this normalized version for anything beyond dupe-checking
239            // in case it breaks advanced syntax.
240            $regex = '/^([^:]*):([^"].*[^"]|[^"]{1,2})$/';
241            $normalized
242                = preg_match($regex, $current['filter'], $match)
243                ? "{$match[1]}:\"{$match[2]}\"" : $current['filter'];
244            if (
245                $current['selected'] && !in_array($normalized, $results)
246                && !in_array($current['filter'], $results)
247            ) {
248                $results[] = $current['filter'];
249            }
250        }
251        $final = [];
252        foreach ($results as $i => $val) {
253            $final[] = ['id' => 'applied_filter_' . ($i + 1), 'value' => $val];
254        }
255        return $final;
256    }
257
258    /**
259     * Get placeholder text from config using the activeSearchClass as key
260     *
261     * @param string $activeSearchClass Active search class ID
262     *
263     * @return string
264     */
265    public function getPlaceholderText($activeSearchClass)
266    {
267        // Searchbox place
268        if (!empty($this->placeholders)) {
269            return $this->placeholders[$activeSearchClass]
270                ?? $this->placeholders['default']
271                ?? null;
272        }
273        return null;
274    }
275
276    /**
277     * Get an array of the configured virtual keyboard layouts
278     *
279     * @return array
280     */
281    public function getKeyboardLayouts()
282    {
283        return $this->config['VirtualKeyboard']['layouts'] ?? [];
284    }
285
286    /**
287     * Get an array of information on search handlers for use in generating a
288     * drop-down or hidden field. Returns an array of arrays with 'value', 'label',
289     * 'indent' and 'selected' keys.
290     *
291     * @param string $activeSearchClass Active search class ID
292     * @param string $activeHandler     Active search handler
293     *
294     * @return array
295     */
296    public function getHandlers($activeSearchClass, $activeHandler)
297    {
298        return $this->combinedHandlersActive()
299            ? $this->getCombinedHandlers($activeSearchClass, $activeHandler)
300            : $this->getBasicHandlers($activeSearchClass, $activeHandler);
301    }
302
303    /**
304     * Get number of active filters
305     *
306     * @param array $checkboxFilters Checkbox filters
307     * @param array $filterList      Other filters
308     *
309     * @return int
310     */
311    public function getFilterCount($checkboxFilters, $filterList)
312    {
313        $result = 0;
314        foreach ($checkboxFilters as $filter) {
315            if ($filter['selected']) {
316                ++$result;
317            }
318        }
319        foreach ($filterList as $filter) {
320            $result += count($filter);
321        }
322        return $result;
323    }
324
325    /**
326     * Support method for getHandlers() -- load basic settings.
327     *
328     * @param string $activeSearchClass Active search class ID
329     * @param string $activeHandler     Active search handler
330     *
331     * @return array
332     */
333    protected function getBasicHandlers($activeSearchClass, $activeHandler)
334    {
335        $handlers = [];
336        $options = $this->optionsManager->get($activeSearchClass);
337        foreach ($options->getBasicHandlers() as $searchVal => $searchDesc) {
338            $handlers[] = [
339                'value' => $searchVal, 'label' => $searchDesc, 'indent' => false,
340                'selected' => ($activeHandler == $searchVal),
341            ];
342        }
343        return $handlers;
344    }
345
346    /**
347     * Support method for getCombinedHandlers() -- retrieve/validate configuration.
348     *
349     * @param string $activeSearchClass Active search class ID
350     *
351     * @return array
352     */
353    protected function getCombinedHandlerConfig($activeSearchClass)
354    {
355        if (!isset($this->cachedConfigs[$activeSearchClass])) {
356            // Load and validate configuration:
357            $settings = $this->config['CombinedHandlers'] ?? [];
358            if (empty($settings)) {
359                throw new \Exception('CombinedHandlers configuration missing.');
360            }
361            $typeCount = count($settings['type']);
362            if (
363                $typeCount != count($settings['target'])
364                || $typeCount != count($settings['label'])
365            ) {
366                throw new \Exception('CombinedHandlers configuration incomplete.');
367            }
368
369            // Fill in missing group settings, if necessary:
370            if (count($settings['group'] ?? []) < $typeCount) {
371                $settings['group'] = array_fill(0, $typeCount, false);
372            }
373
374            // Add configuration for the current search class if it is not already
375            // present:
376            if (!in_array($activeSearchClass, $settings['target'])) {
377                $settings['type'][] = 'VuFind';
378                $settings['target'][] = $activeSearchClass;
379                $settings['label'][] = $activeSearchClass;
380                $settings['group'][]
381                    = $this->config['General']['defaultGroupLabel'] ?? false;
382            }
383
384            $this->cachedConfigs[$activeSearchClass] = $settings;
385        }
386
387        return $this->cachedConfigs[$activeSearchClass];
388    }
389
390    /**
391     * Support method for getCombinedHandlers(): get alphabrowse options.
392     *
393     * @param string $activeHandler Current active search handler
394     * @param bool   $indent        Should we indent these options?
395     *
396     * @return array
397     */
398    protected function getAlphabrowseHandlers($activeHandler, $indent = true)
399    {
400        $alphaBrowseBase = ($this->getView()->plugin('url'))('alphabrowse-home');
401        $labelPrefix = $this->getView()->translate('Browse Alphabetically') . ': ';
402        $handlers = [];
403        foreach ($this->alphabrowseConfig as $source => $label) {
404            $alphaBrowseUrl = $alphaBrowseBase . '?source=' . urlencode($source)
405                . '&from=';
406            $handlers[] = [
407                'value' => 'External:' . $alphaBrowseUrl,
408                'label' => $labelPrefix . $this->getView()->translate($label),
409                'indent' => $indent,
410                'selected' => $activeHandler == 'AlphaBrowse:' . $source,
411                'group' => $this->config['General']['alphaBrowseGroup'] ?? false,
412            ];
413        }
414        return $handlers;
415    }
416
417    /**
418     * Support method for getHandlers() -- load combined settings.
419     *
420     * @param string $activeSearchClass Active search class ID
421     * @param string $activeHandler     Active search handler
422     *
423     * @return array
424     */
425    protected function getCombinedHandlers($activeSearchClass, $activeHandler)
426    {
427        // Build settings:
428        $handlers = [];
429        $backupSelectedIndex = false;
430        $addedBrowseHandlers = false;
431        $settings = $this->getCombinedHandlerConfig($activeSearchClass);
432        $typeCount = count($settings['type']);
433        for ($i = 0; $i < $typeCount; $i++) {
434            $type = $settings['type'][$i];
435            $target = $settings['target'][$i];
436            $label = $settings['label'][$i];
437
438            if ($type == 'VuFind') {
439                $options = $this->optionsManager->get($target);
440                $j = 0;
441                $basic = $options->getBasicHandlers();
442                if (empty($basic)) {
443                    $basic = ['' => ''];
444                }
445                foreach ($basic as $searchVal => $searchDesc) {
446                    $j++;
447                    $selected = $target == $activeSearchClass
448                        && $activeHandler == $searchVal;
449                    if (
450                        !$selected
451                        && $backupSelectedIndex === false
452                        && $target == $activeSearchClass
453                    ) {
454                        $backupSelectedIndex = count($handlers);
455                    }
456                    // Depending on whether or not the current section has a label,
457                    // we'll either want to override the first label and indent
458                    // subsequent ones, or else use all default labels without
459                    // any indentation.
460                    if (empty($label)) {
461                        $finalLabel = $searchDesc;
462                        $indent = false;
463                    } else {
464                        $finalLabel = $j == 1 ? $label : $searchDesc;
465                        $indent = $j == 1 ? false : true;
466                    }
467                    $handlers[] = [
468                        'value' => $type . ':' . $target . '|' . $searchVal,
469                        'label' => $finalLabel,
470                        'indent' => $indent,
471                        'selected' => $selected,
472                        'group' => $settings['group'][$i],
473                    ];
474                }
475
476                // Should we add alphabrowse links?
477                if ($target === 'Solr' && $this->alphaBrowseOptionsEnabled()) {
478                    $addedBrowseHandlers = true;
479                    $handlers = array_merge(
480                        $handlers,
481                        // Only indent alphabrowse handlers if label is non-empty:
482                        $this->getAlphaBrowseHandlers($activeHandler, !empty($label))
483                    );
484                }
485            } elseif ($type == 'External') {
486                $handlers[] = [
487                    'value' => $type . ':' . $target, 'label' => $label,
488                    'indent' => false, 'selected' => false,
489                    'group' => $settings['group'][$i],
490                ];
491            }
492        }
493
494        // If we didn't add alphabrowse links above as part of the Solr section
495        // but we are configured to include them, we should add them now:
496        if (!$addedBrowseHandlers && $this->alphaBrowseOptionsEnabled()) {
497            $handlers = array_merge(
498                $handlers,
499                $this->getAlphaBrowseHandlers($activeHandler, false)
500            );
501        }
502
503        // If we didn't find an exact match for a selected index, use a fuzzy
504        // match (do the check here since it could be an AlphaBrowse index too):
505        $selectedFound = in_array(true, array_column($handlers, 'selected'), true);
506        if (!$selectedFound && $backupSelectedIndex !== false) {
507            $handlers[$backupSelectedIndex]['selected'] = true;
508        }
509        return $handlers;
510    }
511}