Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 227
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchController
0.00% covered (danger)
0.00%
0 / 227
0.00% covered (danger)
0.00%
0 / 14
3080
0.00% covered (danger)
0.00%
0 / 1
 collectionfacetlistAction
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 editmemoryAction
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
72
 emailAction
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
110
 historyAction
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 newitemAction
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 newitemresultsAction
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 reservesAction
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 reservesfacetlistAction
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 reservessearchAction
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 reservesresultsAction
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 resultsAction
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 opensearchAction
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 suggestAction
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 resultScrollerActive
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Default Controller
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  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 VuFind\Exception\Mail as MailException;
33use VuFind\Search\Factory\UrlQueryHelperFactory;
34
35use function array_slice;
36use function count;
37
38/**
39 * Redirects the user to the appropriate default VuFind action.
40 *
41 * @category VuFind
42 * @package  Controller
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 Main Site
46 */
47class SearchController extends AbstractSolrSearch
48{
49    /**
50     * Show facet list for Solr-driven collections.
51     *
52     * @return mixed
53     */
54    public function collectionfacetlistAction()
55    {
56        $this->searchClassId = 'SolrCollection';
57        return $this->facetListAction();
58    }
59
60    /**
61     * Edit search memory action.
62     *
63     * @return mixed
64     */
65    public function editmemoryAction()
66    {
67        // Get the user's referer, with the home page as a fallback; we'll
68        // redirect here after the work is done.
69        $from = $this->getRequest()->getServer()->get('HTTP_REFERER') ?? null;
70        if (empty($from) || !$this->isLocalUrl($from)) {
71            $from = $this->url()->fromRoute('home');
72        }
73
74        // Get parameters:
75        $searchClassId = $this->params()
76            ->fromQuery('searchClassId', DEFAULT_SEARCH_BACKEND);
77        $removeAllFilters = $this->params()->fromQuery('removeAllFilters');
78        $removeFacet = $this->params()->fromQuery('removeFacet');
79        $removeFilter = $this->params()->fromQuery('removeFilter');
80
81        // Retrieve and manipulate the parameters:
82        $searchHelper = $this->getViewRenderer()->plugin('searchMemory');
83        $params = $searchHelper->getLastSearchParams($searchClassId);
84        $factory = $this->serviceLocator->get(UrlQueryHelperFactory::class);
85        $initialParams = $factory->fromParams($params);
86
87        if ($removeAllFilters) {
88            $defaultFilters = $params->getOptions()->getDefaultFilters();
89            $query = $initialParams->removeAllFilters();
90            foreach ($defaultFilters as $filter) {
91                $query = $query->addFilter($filter);
92            }
93        } elseif ($removeFacet) {
94            $query = $initialParams->removeFacet(
95                $removeFacet['field'] ?? '',
96                $removeFacet['value'] ?? '',
97                $removeFacet['operator'] ?? 'AND'
98            );
99        } elseif ($removeFilter) {
100            $query = $initialParams->removeFilter($removeFilter);
101        } else {
102            $query = null;
103        }
104
105        // Remember the altered parameters:
106        if ($query) {
107            $base = $this->url()
108                ->fromRoute($params->getOptions()->getSearchAction());
109            $this->getSearchMemory()
110                ->rememberSearch($base . $query->getParams(false));
111        }
112
113        // Send the user back where they came from:
114        return $this->redirect()->toUrl($from);
115    }
116
117    /**
118     * Email action - Allows the email form to appear.
119     *
120     * @return mixed
121     */
122    public function emailAction()
123    {
124        // If a URL was explicitly passed in, use that; otherwise, try to
125        // find the HTTP referrer.
126        $mailer = $this->serviceLocator->get(\VuFind\Mailer\Mailer::class);
127        $view = $this->createEmailViewModel(null, $mailer->getDefaultLinkSubject());
128        $mailer->setMaxRecipients($view->maxRecipients);
129        // Set up Captcha
130        $view->useCaptcha = $this->captcha()->active('email');
131        $view->url = $this->params()->fromPost('url')
132            ?? $this->params()->fromQuery('url')
133            ?? $this->getRequest()->getServer()->get('HTTP_REFERER');
134        if (!$this->isLocalUrl($view->url)) {
135            throw new \Exception('Unexpected value passed to emailAction: ' . $view->url);
136        }
137
138        // Force login if necessary:
139        $config = $this->getConfig();
140        if (($config->Mail->require_login ?? true) && !$this->getUser()) {
141            return $this->forceLogin(null, ['emailurl' => $view->url]);
142        }
143
144        // Check if we have a URL in login followup data -- this should override
145        // any existing referer to avoid emailing a login-related URL!
146        $followupUrl = $this->followup()->retrieveAndClear('emailurl');
147        if (!empty($followupUrl)) {
148            $view->url = $followupUrl;
149        }
150
151        // Fail if we can't figure out a URL to share:
152        if (empty($view->url)) {
153            throw new \Exception('Cannot determine URL to share.');
154        }
155
156        // Process form submission:
157        if ($this->formWasSubmitted(useCaptcha: $view->useCaptcha)) {
158            // Attempt to send the email and show an appropriate flash message:
159            try {
160                // If we got this far, we're ready to send the email:
161                $cc = $this->params()->fromPost('ccself') && $view->from != $view->to
162                    ? $view->from : null;
163                $mailer->sendLink(
164                    $view->to,
165                    $view->from,
166                    $view->message,
167                    $view->url,
168                    $this->getViewRenderer(),
169                    $view->subject,
170                    $cc
171                );
172                $this->flashMessenger()->addMessage('email_success', 'success');
173                return $this->redirect()->toUrl($view->url);
174            } catch (MailException $e) {
175                $this->flashMessenger()->addMessage($e->getDisplayMessage(), 'error');
176            }
177        }
178        return $view;
179    }
180
181    /**
182     * Handle search history display && purge
183     *
184     * @return mixed
185     */
186    public function historyAction()
187    {
188        // Force login if necessary
189        $user = $this->getUser();
190        if ($this->params()->fromQuery('require_login', 'no') !== 'no') {
191            // If user is already logged in, drop the require_login parameter to
192            // allow for a cleaner log-out experience.
193            return $user
194                ? $this->redirect()->toRoute('search-history')
195                : $this->forceLogin();
196        }
197        $userId = $user?->getId();
198
199        $searchHistoryHelper = $this->serviceLocator
200            ->get(\VuFind\Search\History::class);
201
202        if ($this->params()->fromQuery('purge')) {
203            $searchHistoryHelper->purgeSearchHistory($userId);
204
205            // We don't want to remember the last search after a purge:
206            $this->getSearchMemory()->forgetSearch();
207        }
208        $viewData = $searchHistoryHelper->getSearchHistory($userId);
209        // Eliminate schedule settings if scheduled searches are disabled; add
210        // user email data if scheduled searches are enabled.
211        $scheduleOptions = $this->serviceLocator
212            ->get(\VuFind\Search\History::class)
213            ->getScheduleOptions();
214        if (empty($scheduleOptions)) {
215            unset($viewData['schedule']);
216        } else {
217            $viewData['scheduleOptions'] = $scheduleOptions;
218            $viewData['alertemail'] = $user?->getEmail();
219        }
220        return $this->createViewModel($viewData);
221    }
222
223    /**
224     * New item search form
225     *
226     * @return mixed
227     */
228    public function newitemAction()
229    {
230        // Search parameters set?  Process results.
231        if ($this->params()->fromQuery('range') !== null) {
232            return $this->forwardTo('Search', 'NewItemResults');
233        }
234
235        $view = $this->createViewModel(
236            [
237                'defaultSort' => $this->newItems()->getDefaultSort(),
238                'fundList' => $this->newItems()->getFundList(),
239                'ranges' => $this->newItems()->getRanges(),
240            ]
241        );
242        if ($this->newItems()->includeFacets()) {
243            $view->options = $this->serviceLocator
244                ->get(\VuFind\Search\Options\PluginManager::class)
245                ->get($this->searchClassId);
246            $this->addFacetDetailsToView($view, 'NewItems');
247        }
248        return $view;
249    }
250
251    /**
252     * New item result list
253     *
254     * @return mixed
255     */
256    public function newitemresultsAction()
257    {
258        // Retrieve new item list:
259        $range = $this->params()->fromQuery('range');
260        $dept = $this->params()->fromQuery('department');
261
262        // Validate the range parameter -- it should not exceed the greatest
263        // configured value:
264        $maxAge = $this->newItems()->getMaxAge();
265        if ($maxAge > 0 && $range > $maxAge) {
266            $range = $maxAge;
267        }
268
269        // Are there "new item" filter queries specified in the config file?
270        // If so, load them now; we may add more values. These will be applied
271        // later after the whole list is collected.
272        $hiddenFilters = $this->newItems()->getHiddenFilters();
273
274        // Depending on whether we're in ILS or Solr mode, we need to do some
275        // different processing here to retrieve the correct items:
276        if ($this->newItems()->getMethod() == 'ils') {
277            // Use standard search action with override parameter to show results:
278            $bibIDs = $this->newItems()->getBibIDsFromCatalog(
279                $this->getILS(),
280                $this->getResultsManager()->get('Solr')->getParams(),
281                $range,
282                $dept,
283                $this->flashMessenger()
284            );
285            $this->getRequest()->getQuery()->set('overrideIds', $bibIDs);
286        } else {
287            // Use a Solr filter to show results:
288            $hiddenFilters[] = $this->newItems()->getSolrFilter($range);
289        }
290
291        // If we found hidden filters above, apply them now:
292        if (!empty($hiddenFilters)) {
293            $this->getRequest()->getQuery()->set('hiddenFilters', $hiddenFilters);
294        }
295
296        // Don't save to history -- history page doesn't handle correctly:
297        $this->saveToHistory = false;
298
299        // Call rather than forward, so we can use custom template
300        $view = $this->resultsAction();
301
302        // Customize the URL helper to make sure it builds proper new item URLs
303        // (check it's set first -- RSS feed will return a response model rather
304        // than a view model):
305        if (isset($view->results)) {
306            $view->results->getUrlQuery()
307                ->setDefaultParameter('range', $range)
308                ->setDefaultParameter('department', $dept)
309                ->setSuppressQuery(true);
310        }
311
312        // We don't want new items hidden filters to propagate to other searches:
313        $view->ignoreHiddenFilterMemory = true;
314        $view->ignoreHiddenFiltersInRequest = true;
315
316        return $view;
317    }
318
319    /**
320     * Course reserves
321     *
322     * @return mixed
323     */
324    public function reservesAction()
325    {
326        // Search parameters set?  Process results.
327        if (
328            $this->params()->fromQuery('inst') !== null
329            || $this->params()->fromQuery('course') !== null
330            || $this->params()->fromQuery('dept') !== null
331        ) {
332            return $this->forwardTo('Search', 'ReservesResults');
333        }
334
335        // No params?  Show appropriate form (varies depending on whether we're
336        // using driver-based or Solr-based reserves searching).
337        if ($this->reserves()->useIndex()) {
338            return $this->forwardTo('Search', 'ReservesSearch');
339        }
340
341        // If we got this far, we're using driver-based searching and need to
342        // send options to the view:
343        $catalog = $this->getILS();
344        return $this->createViewModel(
345            [
346                'deptList' => $catalog->getDepartments(),
347                'instList' => $catalog->getInstructors(),
348                'courseList' =>  $catalog->getCourses(),
349            ]
350        );
351    }
352
353    /**
354     * Show facet list for Solr-driven reserves.
355     *
356     * @return mixed
357     */
358    public function reservesfacetlistAction()
359    {
360        $this->searchClassId = 'SolrReserves';
361        return $this->facetListAction();
362    }
363
364    /**
365     * Show search form for Solr-driven reserves.
366     *
367     * @return mixed
368     */
369    public function reservessearchAction()
370    {
371        $request = new \Laminas\Stdlib\Parameters(
372            $this->getRequest()->getQuery()->toArray()
373            + $this->getRequest()->getPost()->toArray()
374        );
375        $view = $this->createViewModel();
376        $runner = $this->serviceLocator->get(\VuFind\Search\SearchRunner::class);
377        $view->results = $runner->run(
378            $request,
379            'SolrReserves',
380            $this->getSearchSetupCallback()
381        );
382        $view->params = $view->results->getParams();
383        return $view;
384    }
385
386    /**
387     * Show results of reserves search.
388     *
389     * @return mixed
390     */
391    public function reservesresultsAction()
392    {
393        // Retrieve course reserves item list:
394        $course = $this->params()->fromQuery('course');
395        $inst = $this->params()->fromQuery('inst');
396        $dept = $this->params()->fromQuery('dept');
397        $result = $this->reserves()->findReserves($course, $inst, $dept);
398
399        // Build a list of unique IDs
400        $callback = function ($i) {
401            return $i['BIB_ID'];
402        };
403        $bibIDs = array_unique(array_map($callback, $result));
404
405        // Truncate the list if it is too long:
406        $limit = $this->getResultsManager()->get('Solr')->getParams()
407            ->getQueryIDLimit();
408        if (count($bibIDs) > $limit) {
409            $bibIDs = array_slice($bibIDs, 0, $limit);
410            $this->flashMessenger()->addMessage('too_many_reserves', 'info');
411        }
412
413        // Use standard search action with override parameter to show results:
414        $this->getRequest()->getQuery()->set('overrideIds', $bibIDs);
415
416        // Don't save to history -- history page doesn't handle correctly:
417        $this->saveToHistory = false;
418
419        // Set up RSS feed title just in case:
420        $this->getViewRenderer()->plugin('resultfeed')
421            ->setOverrideTitle('Reserves Search Results');
422
423        // Call rather than forward, so we can use custom template
424        $view = $this->resultsAction();
425
426        // Pass some key values to the view, if found:
427        if (isset($result[0]['instructor']) && !empty($result[0]['instructor'])) {
428            $view->instructor = $result[0]['instructor'];
429        }
430        if (isset($result[0]['course']) && !empty($result[0]['course'])) {
431            $view->course = $result[0]['course'];
432        }
433
434        // Customize the URL helper to make sure it builds proper reserves URLs
435        // (but only do this if we have access to a results object, which we
436        // won't in RSS mode):
437        if (isset($view->results)) {
438            $view->results->getUrlQuery()
439                ->setDefaultParameter('course', $course)
440                ->setDefaultParameter('inst', $inst)
441                ->setDefaultParameter('dept', $dept)
442                ->setSuppressQuery(true);
443        }
444        return $view;
445    }
446
447    /**
448     * Results action.
449     *
450     * @return mixed
451     */
452    public function resultsAction()
453    {
454        // Special case -- redirect tag searches.
455        $tag = $this->params()->fromQuery('tag');
456        if (!empty($tag)) {
457            $query = $this->getRequest()->getQuery();
458            $query->set('lookfor', $tag);
459            $query->set('type', 'tag');
460        }
461        if ($this->params()->fromQuery('type') == 'tag') {
462            // Because we're coming in from a search, we want to do a fuzzy
463            // tag search, not an exact search like we would when linking to a
464            // specific tag name.
465            $query = $this->getRequest()->getQuery()->set('fuzzy', 'true');
466            return $this->forwardTo('Tag', 'Home');
467        }
468
469        // Default case -- standard behavior.
470        return parent::resultsAction();
471    }
472
473    /**
474     * Handle OpenSearch.
475     *
476     * @return \Laminas\Http\Response
477     */
478    public function opensearchAction()
479    {
480        switch ($this->params()->fromQuery('method')) {
481            case 'describe':
482                $config = $this->getConfig();
483                $xml = $this->getViewRenderer()->render(
484                    'search/opensearch-describe.phtml',
485                    ['site' => $config->Site]
486                );
487                break;
488            default:
489                $xml = $this->getViewRenderer()
490                    ->render('search/opensearch-error.phtml');
491                break;
492        }
493
494        $response = $this->getResponse();
495        $headers = $response->getHeaders();
496        $headers->addHeaderLine('Content-type', 'text/xml');
497        $response->setContent($xml);
498        return $response;
499    }
500
501    /**
502     * Provide OpenSearch suggestions as specified at
503     * http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.0
504     *
505     * @return \Laminas\Http\Response
506     */
507    public function suggestAction()
508    {
509        // Always use 'AllFields' as our autosuggest type:
510        $query = $this->getRequest()->getQuery();
511        $query->set('type', 'AllFields');
512
513        // Get suggestions and make sure they are an array (we don't want to JSON
514        // encode them into an object):
515        $suggester = $this->serviceLocator
516            ->get(\VuFind\Autocomplete\Suggester::class);
517        $suggestions = $suggester->getSuggestions($query, 'type', 'lookfor');
518
519        // Send the JSON response:
520        $response = $this->getResponse();
521        $headers = $response->getHeaders();
522        $headers->addHeaderLine('Content-type', 'application/json');
523        $response->setContent(
524            json_encode([$query->get('lookfor', ''), $suggestions])
525        );
526        return $response;
527    }
528
529    /**
530     * Is the result scroller active?
531     *
532     * @return bool
533     */
534    protected function resultScrollerActive()
535    {
536        $config = $this->serviceLocator->get(\VuFind\Config\PluginManager::class)
537            ->get('config');
538        return $config->Record->next_prev_navigation ?? false;
539    }
540}