Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
CollectionsController
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 12
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 bytitleAction
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 homeAction
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getBrowseDelimiter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showBrowseAlphabetic
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 showBrowseIndex
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
72
 sortFindKeyLocation
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 normalizeAndSortFacets
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 normalizeForBrowse
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getAlphabetList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBrowseLimit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCollectionsFromTitle
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Collections Controller
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010, 2022.
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  Controller
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 Site
28 */
29
30namespace VuFind\Controller;
31
32use Laminas\Config\Config;
33use Laminas\ServiceManager\ServiceLocatorInterface;
34use VuFindSearch\Command\SearchCommand;
35use VuFindSearch\Query\Query;
36
37use function array_slice;
38use function count;
39
40/**
41 * Collections Controller
42 *
43 * @category VuFind
44 * @package  Controller
45 * @author   Demian Katz <demian.katz@villanova.edu>
46 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
47 * @link     https://vufind.org Main Site
48 */
49class CollectionsController extends AbstractBase implements
50    \VuFind\I18n\HasSorterInterface
51{
52    use Feature\AlphaBrowseTrait;
53    use \VuFind\I18n\HasSorterTrait;
54
55    /**
56     * VuFind configuration
57     *
58     * @var \Laminas\Config\Config
59     */
60    protected $config;
61
62    /**
63     * Constructor
64     *
65     * @param ServiceLocatorInterface $sm     Service manager
66     * @param Config                  $config VuFind configuration
67     */
68    public function __construct(ServiceLocatorInterface $sm, Config $config)
69    {
70        $this->config = $config;
71        $this->setSorter($sm->get(\VuFind\I18n\Sorter::class));
72        parent::__construct($sm);
73    }
74
75    /**
76     * Search by title action
77     *
78     * @return mixed
79     */
80    public function bytitleAction()
81    {
82        $collections = $this->getCollectionsFromTitle(
83            $this->params()->fromQuery('title')
84        );
85        if (count($collections) != 1) {
86            return $this->createViewModel(['collections' => $collections]);
87        }
88        return $this->redirect()
89            ->toRoute('collection', ['id' => $collections[0]->getUniqueId()]);
90    }
91
92    /**
93     * Browse action
94     *
95     * @return mixed
96     */
97    public function homeAction()
98    {
99        $browseType = $this->config->Collections->browseType ?? 'Index';
100        return ($browseType == 'Alphabetic')
101            ? $this->showBrowseAlphabetic() : $this->showBrowseIndex();
102    }
103
104    /**
105     * Get the delimiter used to separate title from ID in the browse strings.
106     *
107     * @return string
108     */
109    protected function getBrowseDelimiter()
110    {
111        return $this->config->Collections->browseDelimiter ?? '{{{_ID_}}}';
112    }
113
114    /**
115     * Show the Browse Menu
116     *
117     * @return mixed
118     */
119    protected function showBrowseAlphabetic()
120    {
121        // Process incoming parameters:
122        $source = 'hierarchy';
123        $from = $this->params()->fromQuery('from', '');
124        $page = $this->params()->fromQuery('page', 0);
125        $limit = $this->getBrowseLimit();
126
127        // Load Solr data or die trying:
128        $result = $this->alphabeticBrowse($source, $from, $page, $limit);
129
130        // No results?  Try the previous page just in case we've gone past the
131        // end of the list....
132        if ($result['Browse']['totalCount'] == 0) {
133            $page--;
134            $result = $this->alphabeticBrowse($source, $from, $page, $limit);
135        }
136
137        // Begin building view model:
138        $view = $this->createViewModel();
139
140        // Only display next/previous page links when applicable:
141        if ($result['Browse']['totalCount'] > $limit) {
142            $view->nextpage = $page + 1;
143        }
144        if ($result['Browse']['offset'] + $result['Browse']['startRow'] > 1) {
145            $view->prevpage = $page - 1;
146        }
147
148        // Send other relevant values to the template:
149        $view->from = $from;
150        $view->letters = $this->getAlphabetList();
151
152        // Format the results for proper display:
153        $finalresult = [];
154        $delimiter = $this->getBrowseDelimiter();
155        foreach ($result['Browse']['items'] as $rkey => $collection) {
156            $collectionIdNamePair
157                = explode($delimiter, $collection['heading']);
158            $finalresult[$rkey]['displayText'] = $collectionIdNamePair[0];
159            $finalresult[$rkey]['count'] = $collection['count'];
160            $finalresult[$rkey]['value'] = $collectionIdNamePair[1];
161        }
162        $view->result = $finalresult;
163
164        // Display the page:
165        return $view;
166    }
167
168    /**
169     * Show the Browse Menu
170     *
171     * @return mixed
172     */
173    protected function showBrowseIndex()
174    {
175        // Process incoming parameters:
176        $from = $this->params()->fromQuery('from', '');
177        $page = $this->params()->fromQuery('page', 0);
178        $appliedFilters = $this->params()->fromQuery('filter', []);
179        $limit = $this->getBrowseLimit();
180
181        $browseField = 'hierarchy_browse';
182
183        $searchObject = $this->serviceLocator
184            ->get(\VuFind\Search\Results\PluginManager::class)->get('Solr');
185        foreach ($appliedFilters as $filter) {
186            $searchObject->getParams()->addFilter($filter);
187        }
188
189        // Only grab 150,000 facet values to avoid out-of-memory errors:
190        $result = $searchObject->getFullFieldFacets(
191            [$browseField],
192            false,
193            150000,
194            'index'
195        );
196        $result = $result[$browseField]['data']['list'] ?? [];
197
198        $delimiter = $this->getBrowseDelimiter();
199        foreach ($result as $rkey => $collection) {
200            [$name, $id] = explode($delimiter, $collection['value'], 2);
201            $result[$rkey]['displayText'] = $name;
202            $result[$rkey]['value'] = $id;
203        }
204
205        // Sort the $results and get the position of the from string once sorted
206        $key = $this->sortFindKeyLocation($result, $from);
207
208        // Offset the key by how many pages in we are
209        $key += ($limit * $page);
210
211        // Catch out of range keys
212        if ($key < 0) {
213            $key = 0;
214        }
215        if ($key >= count($result)) {
216            $key = count($result) - 1;
217        }
218
219        // Begin building view model:
220        $view = $this->createViewModel();
221
222        // Only display next/previous page links when applicable:
223        if (count($result) > $key + $limit) {
224            $view->nextpage = $page + 1;
225        }
226        if ($key > 0) {
227            $view->prevpage = $page - 1;
228        }
229
230        // Select just the records to display
231        $result = array_slice(
232            $result,
233            $key,
234            count($result) > $key + $limit ? $limit : null
235        );
236
237        // Send other relevant values to the template:
238        $view->from = $from;
239        $view->result = $result;
240        $view->letters = $this->getAlphabetList();
241        $view->filters = $searchObject->getParams()->getFilterList(true);
242
243        // Display the page:
244        return $view;
245    }
246
247    /**
248     * Function to sort the results and find the position of the from
249     * value in the result set; if the value doesn't exist, it's inserted.
250     *
251     * @param array  $result Array to sort
252     * @param string $from   Position to find
253     *
254     * @return int
255     */
256    protected function sortFindKeyLocation(&$result, $from)
257    {
258        // Normalize the from value so it matches the values we are looking up
259        $from = $this->normalizeForBrowse($from);
260
261        // Push the from value into the array so we can find the matching position:
262        array_push($result, ['displayText' => $from, 'placeholder' => true]);
263
264        // Declare array to hold the $result array in the right sort order
265        $sorted = [];
266        foreach (array_keys($this->normalizeAndSortFacets($result)) as $i) {
267            // If this is the placeholder we added earlier, we have found the
268            // array position we want to use as our start; otherwise, it is an
269            // element that needs to be moved into the sorted version of the
270            // array:
271            if (isset($result[$i]['placeholder'])) {
272                $key = count($sorted);
273            } else {
274                $sorted[] = $result[$i];
275                unset($result[$i]); //clear this out of memory
276            }
277        }
278        $result = $sorted;
279
280        return $key ?? 0;
281    }
282
283    /**
284     * Function to normalize the names so they sort properly
285     *
286     * @param array $result Array to sort (passed by reference to use less
287     * memory)
288     *
289     * @return array $resultOut
290     */
291    protected function normalizeAndSortFacets(&$result)
292    {
293        $valuesSorted = [];
294        foreach ($result as $resKey => $resVal) {
295            $valuesSorted[$resKey]
296                = $this->normalizeForBrowse($resVal['displayText']);
297        }
298        $this->getSorter()->asort($valuesSorted);
299
300        // Now the $valuesSorted is in the right order
301        return $valuesSorted;
302    }
303
304    /**
305     * Normalize the value for the browse sort
306     *
307     * @param string $val Value to normalize
308     *
309     * @return string $valNormalized
310     */
311    protected function normalizeForBrowse($val)
312    {
313        $valNormalized = iconv('UTF-8', 'US-ASCII//TRANSLIT//IGNORE', $val);
314        $valNormalized = strtolower($valNormalized);
315        $valNormalized = preg_replace("/[^a-zA-Z0-9\s]/", '', $valNormalized);
316        $valNormalized = trim($valNormalized);
317        return $valNormalized;
318    }
319
320    /**
321     * Get a list of initial letters to display.
322     *
323     * @return array
324     */
325    protected function getAlphabetList()
326    {
327        return array_merge(range('0', '9'), range('A', 'Z'));
328    }
329
330    /**
331     * Get the collection browse page size
332     *
333     * @return int
334     */
335    protected function getBrowseLimit()
336    {
337        return $this->config->Collections->browseLimit ?? 20;
338    }
339
340    /**
341     * Get collection information matching a given title:
342     *
343     * @param string $title Title to search for
344     *
345     * @return array
346     */
347    protected function getCollectionsFromTitle($title)
348    {
349        $title = addcslashes($title, '"');
350        $query = new Query("is_hierarchy_title:\"$title\"", 'AllFields');
351        $searchService = $this->serviceLocator->get(\VuFindSearch\Service::class);
352        $command = new SearchCommand(
353            'Solr',
354            $query,
355            0,
356            $this->getBrowseLimit()
357        );
358        $result = $searchService->invoke($command)->getResult();
359        return $result->getRecords();
360    }
361}